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

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.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { A as VertoPongError, C as StorageReadError, D as UnimplementedError, E as UnexpectedError, M as WebSocketTimeoutError, O as ValidationError, S as StorageNotAvailableError, T as TransportConnectionError, _ as MessageParseError, a as filterNull, b as RequestTimeoutError, c as CallCreateError, d as DependencyError, f as DeserializationError, g as MediaTrackError, h as JSONRPCError, i as getValueFrom, j as WebSocketConnectionError, k as VertoInviteHandlerError, l as CollectionFetchError, m as InvalidParams, n as filterAs, o as getLogger, p as InvalidCredentialsError, s as AuthStateHandlerError, t as throwOnRPCError, u as ConversationError, v as RPCTimeoutError, w as StorageWriteError, x as SerializationError, y as RequestError } from "./operators-BHQMSEzq.mjs";
1
+ import { A as VertoPongError, C as StorageReadError, D as UnimplementedError, E as UnexpectedError, M as WebSocketTimeoutError, O as ValidationError, S as StorageNotAvailableError, T as TransportConnectionError, _ as MessageParseError, a as filterNull, b as RequestTimeoutError, c as CallCreateError, d as DependencyError, f as DeserializationError, g as MediaTrackError, h as JSONRPCError, i as getValueFrom, j as WebSocketConnectionError, k as VertoInviteHandlerError, l as CollectionFetchError, m as InvalidParams, n as filterAs, o as getLogger, p as InvalidCredentialsError, s as AuthStateHandlerError, t as throwOnRPCError, u as ConversationError, v as RPCTimeoutError, w as StorageWriteError, x as SerializationError, y as RequestError } from "./operators-uT_fb8ba.mjs";
2
2
  import { jwtDecode } from "jwt-decode";
3
- import { BehaviorSubject, EMPTY, NEVER, Observable, ReplaySubject, Subject, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, firstValueFrom, from, interval, lastValueFrom, map, merge, of, pipe, race, share, shareReplay, skip, skipWhile, switchMap, take, takeUntil, tap, timeout, withLatestFrom } from "rxjs";
3
+ import { BehaviorSubject, EMPTY, NEVER, Observable, ReplaySubject, Subject, TimeoutError, asapScheduler, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, firstValueFrom, from, interval, lastValueFrom, map, merge, observeOn, of, pipe, race, share, shareReplay, skip, skipWhile, startWith, switchMap, take, takeUntil, tap, throwError, timeout, withLatestFrom } from "rxjs";
4
4
  import { v4 } from "uuid";
5
5
  import { distinctUntilChanged as distinctUntilChanged$1, map as map$1 } from "rxjs/operators";
6
6
 
@@ -27,6 +27,40 @@ var Destroyable = class {
27
27
  }
28
28
  return cached;
29
29
  }
30
+ /**
31
+ * Like `cachedObservable`, but defers emissions to the microtask queue
32
+ * via `observeOn(asapScheduler)`.
33
+ *
34
+ * Use ONLY for public-facing observable getters that external consumers
35
+ * subscribe to. Prevents a class of bugs where `BehaviorSubject` or
36
+ * `ReplaySubject` replays synchronously during `subscribe()`, before
37
+ * the subscription variable is assigned in the caller's scope.
38
+ *
39
+ * Do NOT use for observables consumed internally by the SDK — internal
40
+ * code using `subscribeTo()`, `firstValueFrom()`, or `withLatestFrom()`
41
+ * depends on synchronous emission delivery.
42
+ */
43
+ publicCachedObservable(key, factory) {
44
+ const publicKey = `public:${key}`;
45
+ this._observableCache ??= /* @__PURE__ */ new Map();
46
+ let cached = this._observableCache.get(publicKey);
47
+ if (!cached) {
48
+ cached = factory().pipe(observeOn(asapScheduler));
49
+ this._observableCache.set(publicKey, cached);
50
+ }
51
+ return cached;
52
+ }
53
+ /**
54
+ * Wraps an observable so emissions are deferred to the microtask queue.
55
+ *
56
+ * Use ONLY for public-facing getters that expose a subject via
57
+ * `.asObservable()` without going through `cachedObservable`.
58
+ *
59
+ * Do NOT use for observables consumed internally by the SDK.
60
+ */
61
+ deferEmission(observable) {
62
+ return observable.pipe(observeOn(asapScheduler));
63
+ }
30
64
  subscribeTo(observable, observerOrNext) {
31
65
  const subscription = observable.subscribe(observerOrNext);
32
66
  this.subscriptions.push(subscription);
@@ -295,7 +329,7 @@ var PreferencesContainer = class PreferencesContainer {
295
329
  skipDeviceMonitoring: false,
296
330
  savePreferences: false
297
331
  };
298
- this.receiveVideo = true;
332
+ this.receiveVideo = false;
299
333
  this.receiveAudio = true;
300
334
  this.preferredAudioInput = null;
301
335
  this.preferredAudioOutput = null;
@@ -607,7 +641,7 @@ var NavigatorDeviceController = class extends Destroyable {
607
641
  };
608
642
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
609
643
  this._selectedDevicesState$ = this.createBehaviorSubject(initialSelectedDevicesState);
610
- this._errors$ = this.createSubject();
644
+ this._errors$ = this.createReplaySubject(1);
611
645
  this.init();
612
646
  }
613
647
  get selectedAudioInputDeviceConstraints() {
@@ -1087,7 +1121,7 @@ const RPCExecute = ({ method, params }) => {
1087
1121
 
1088
1122
  //#endregion
1089
1123
  //#region src/core/RPCMessages/VertoMessages.ts
1090
- const tmpMap = {
1124
+ const SDK_TO_VERTO_FIELD_MAP = {
1091
1125
  id: "callID",
1092
1126
  destinationNumber: "destination_number",
1093
1127
  remoteCallerName: "remote_caller_id_name",
@@ -1096,19 +1130,31 @@ const tmpMap = {
1096
1130
  callerNumber: "caller_id_number",
1097
1131
  fromCallAddressId: "from_fabric_address_id"
1098
1132
  };
1133
+ const EXCLUDED_DIALOG_PARAMS = new Set([
1134
+ "remoteSdp",
1135
+ "localStream",
1136
+ "remoteStream"
1137
+ ]);
1099
1138
  /**
1100
- * Translate SDK fields into verto variables
1139
+ * Translate SDK fields into verto variables.
1140
+ * Returns a new object — the input is never mutated.
1101
1141
  */
1142
+ /** @internal Exported for testing only. */
1102
1143
  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;
1144
+ if (!Object.prototype.hasOwnProperty.call(params, "dialogParams")) return params;
1145
+ const sourceDialogParams = params.dialogParams;
1146
+ const filteredDialogParams = Object.entries(sourceDialogParams).reduce((acc, [key, value]) => {
1147
+ if (EXCLUDED_DIALOG_PARAMS.has(key)) return acc;
1148
+ const mappedKey = SDK_TO_VERTO_FIELD_MAP[key] ?? key;
1149
+ return {
1150
+ ...acc,
1151
+ [mappedKey]: value
1152
+ };
1153
+ }, {});
1154
+ return {
1155
+ ...params,
1156
+ dialogParams: filteredDialogParams
1157
+ };
1112
1158
  };
1113
1159
  const buildVertoRPCMessage = (method) => {
1114
1160
  return (params = {}) => {
@@ -1219,17 +1265,21 @@ var AttachManager = class {
1219
1265
  buildCallOptions(attachment) {
1220
1266
  const { audio: audioDirection, video: videoDirection } = attachment.mediaDirections;
1221
1267
  const { audioInputDevice, videoInputDevice } = attachment;
1268
+ const receiveAudio = audioDirection.includes("recv");
1269
+ const receiveVideo = videoDirection.includes("recv");
1270
+ const sendAudio = audioDirection.includes("send");
1271
+ const sendVideo = videoDirection.includes("send");
1222
1272
  return {
1223
- receiveAudio: audioDirection.includes("recv"),
1224
- receiveVideo: videoDirection.includes("recv"),
1225
- inputAudioDeviceConstraints: {
1226
- audio: audioDirection.includes("send"),
1273
+ receiveAudio,
1274
+ receiveVideo,
1275
+ inputAudioDeviceConstraints: sendAudio ? {
1276
+ audio: true,
1227
1277
  ...this.deviceController.deviceInfoToConstraints(audioInputDevice)
1228
- },
1229
- inputVideoDeviceConstraints: {
1230
- video: videoDirection.includes("send"),
1278
+ } : void 0,
1279
+ inputVideoDeviceConstraints: sendVideo ? {
1280
+ video: true,
1231
1281
  ...this.deviceController.deviceInfoToConstraints(videoInputDevice)
1232
- },
1282
+ } : void 0,
1233
1283
  reattach: true
1234
1284
  };
1235
1285
  }
@@ -1759,7 +1809,13 @@ var Participant = class extends Destroyable {
1759
1809
  }
1760
1810
  /** Removes this participant from the call. */
1761
1811
  async remove() {
1762
- await this.executeMethod(this.id, "call.member.remove", {});
1812
+ const state = this._state$.value;
1813
+ const target = {
1814
+ member_id: this.id,
1815
+ call_id: state.call_id ?? "",
1816
+ node_id: state.node_id ?? ""
1817
+ };
1818
+ await this.executeMethod(target, "call.member.remove", {});
1763
1819
  }
1764
1820
  /** Ends the call for this participant. */
1765
1821
  async end() {
@@ -1929,6 +1985,9 @@ function isJSONRPCRequest(value) {
1929
1985
  function isJSONRPCResponse(value) {
1930
1986
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "id") && typeof value.id === "string" && (hasProperty(value, "result") || hasProperty(value, "error"));
1931
1987
  }
1988
+ function isJSONRPCErrorResponse(value) {
1989
+ return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "id") && typeof value.id === "string" && (hasProperty(value, "error") && isObject(value.error) && hasProperty(value.error, "code") && hasProperty(value.error, "message") || hasProperty(value, "result") && isObject(value.result) && hasProperty(value.result, "code") && value.result.code !== "200" && hasProperty(value.result, "message"));
1990
+ }
1932
1991
 
1933
1992
  //#endregion
1934
1993
  //#region src/core/RPCMessages/guards/events.guards.ts
@@ -2001,7 +2060,6 @@ var CallEventsManager = class extends Destroyable {
2001
2060
  this.options = options;
2002
2061
  this.callIds = /* @__PURE__ */ new Set();
2003
2062
  this.roomSessionIds = /* @__PURE__ */ new Set();
2004
- this._status$ = this.createBehaviorSubject("trying");
2005
2063
  this._participants$ = this.createBehaviorSubject({});
2006
2064
  this._self$ = this.createBehaviorSubject(null);
2007
2065
  this._sessionState$ = this.createBehaviorSubject(initialSessionState);
@@ -2010,15 +2068,12 @@ var CallEventsManager = class extends Destroyable {
2010
2068
  get participants$() {
2011
2069
  return this.cachedObservable("participants$", () => this._participants$.asObservable().pipe(map((participantsRecord) => Object.values(participantsRecord))));
2012
2070
  }
2071
+ get participants() {
2072
+ return Object.values(this._participants$.value);
2073
+ }
2013
2074
  get self$() {
2014
2075
  return this.cachedObservable("self$", () => this._self$.asObservable().pipe(filterNull()));
2015
2076
  }
2016
- get status$() {
2017
- return this._status$.asObservable();
2018
- }
2019
- get status() {
2020
- return this._status$.value;
2021
- }
2022
2077
  isRoomSessionIdValid(roomSessionId) {
2023
2078
  return this.roomSessionIds.has(roomSessionId);
2024
2079
  }
@@ -2103,7 +2158,6 @@ var CallEventsManager = class extends Destroyable {
2103
2158
  callId: callJoinedEvent.call_id,
2104
2159
  roomSessionId: callJoinedEvent.room_session_id
2105
2160
  });
2106
- this._status$.next("connected");
2107
2161
  const sessionState = callJoinedEvent.room_session;
2108
2162
  const { capabilities } = callJoinedEvent;
2109
2163
  this.selfId = this.selfId ?? callJoinedEvent.member_id;
@@ -2230,12 +2284,53 @@ var CallEventsManager = class extends Destroyable {
2230
2284
 
2231
2285
  //#endregion
2232
2286
  //#region src/helpers/SDPHelper.ts
2287
+ /** Valid SDP direction attribute values. */
2288
+ const SDP_DIRECTIONS = new Set([
2289
+ "sendrecv",
2290
+ "sendonly",
2291
+ "recvonly",
2292
+ "inactive"
2293
+ ]);
2233
2294
  /**
2234
- * SDPHelper - Utility functions for SDP (Session Description Protocol) parsing and validation.
2295
+ * Extracts the media directions (audio/video) from an SDP string.
2296
+ *
2297
+ * Parses each media section (`m=audio` / `m=video`) and reads the `a=` direction
2298
+ * attribute (`sendrecv`, `sendonly`, `recvonly`, `inactive`).
2299
+ * If no explicit direction attribute is found for a media section, defaults to `sendrecv`
2300
+ * per RFC 4566.
2235
2301
  *
2236
- * This module provides helper functions to analyze and validate SDP content,
2237
- * particularly for ICE candidate validation in WebRTC connections.
2302
+ * @param sdp - The SDP string to parse
2303
+ * @returns The extracted audio and video directions
2304
+ *
2305
+ * @example
2306
+ * ```typescript
2307
+ * 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`;
2308
+ * extractMediaDirectionsFromSDP(sdp);
2309
+ * // { audio: 'sendrecv', video: 'recvonly' }
2310
+ * ```
2238
2311
  */
2312
+ function extractMediaDirectionsFromSDP(sdp) {
2313
+ const result = {
2314
+ audio: "inactive",
2315
+ video: "inactive"
2316
+ };
2317
+ if (!sdp) return result;
2318
+ const lines = sdp.split(/\r?\n/);
2319
+ let currentMediaKind = null;
2320
+ let currentDirection = null;
2321
+ for (const line of lines) if (line.startsWith("m=")) {
2322
+ if (currentMediaKind) result[currentMediaKind] = currentDirection ?? "sendrecv";
2323
+ if (line.startsWith("m=audio")) currentMediaKind = "audio";
2324
+ else if (line.startsWith("m=video")) currentMediaKind = "video";
2325
+ else currentMediaKind = null;
2326
+ currentDirection = null;
2327
+ } else if (currentMediaKind && line.startsWith("a=")) {
2328
+ const attr = line.substring(2).trim();
2329
+ if (SDP_DIRECTIONS.has(attr)) currentDirection = attr;
2330
+ }
2331
+ if (currentMediaKind) result[currentMediaKind] = currentDirection ?? "sendrecv";
2332
+ return result;
2333
+ }
2239
2334
  /**
2240
2335
  * Validates that an SDP string has at least one non-host ICE candidate
2241
2336
  * for each media section (m= line).
@@ -2536,6 +2631,15 @@ var LocalStreamController = class extends Destroyable {
2536
2631
  track.addEventListener("ended", this.mediaTrackEndedHandler);
2537
2632
  }
2538
2633
  /**
2634
+ * Update the controller options (e.g., when media overrides are applied).
2635
+ */
2636
+ updateOptions(options) {
2637
+ this.options = {
2638
+ ...this.options,
2639
+ ...options
2640
+ };
2641
+ }
2642
+ /**
2539
2643
  * Stop all local tracks and clean up.
2540
2644
  */
2541
2645
  stopAllTracks() {
@@ -2873,11 +2977,12 @@ var RTCPeerConnectionController = class extends Destroyable {
2873
2977
  this._connectionState$ = this.createReplaySubject(1);
2874
2978
  this._signalingState$ = this.createReplaySubject(1);
2875
2979
  this._iceGatheringState$ = this.createReplaySubject(1);
2876
- this._errors$ = this.createSubject();
2980
+ this._errors$ = this.createReplaySubject(1);
2877
2981
  this._iceCandidates$ = this.createReplaySubject(1);
2878
2982
  this._initialized$ = this.createReplaySubject(1);
2879
2983
  this._remoteDescription$ = this.createReplaySubject(1);
2880
2984
  this._remoteStream$ = this.createBehaviorSubject(null);
2985
+ this._remoteOfferMediaDirections = null;
2881
2986
  this.deviceController = deviceController ?? {};
2882
2987
  this.id = options.callId ?? v4();
2883
2988
  this._type = remoteSessionDescription ? "answer" : "offer";
@@ -2885,10 +2990,19 @@ var RTCPeerConnectionController = class extends Destroyable {
2885
2990
  type: "offer",
2886
2991
  sdp: remoteSessionDescription
2887
2992
  } : void 0;
2993
+ this._remoteOfferMediaDirections = remoteSessionDescription ? extractMediaDirectionsFromSDP(remoteSessionDescription) : null;
2994
+ const offerDefaults = this._remoteOfferMediaDirections ? {
2995
+ audio: this._remoteOfferMediaDirections.audio.includes("recv"),
2996
+ video: this._remoteOfferMediaDirections.video.includes("recv"),
2997
+ receiveAudio: this._remoteOfferMediaDirections.audio.includes("send"),
2998
+ receiveVideo: this._remoteOfferMediaDirections.video.includes("send")
2999
+ } : {};
2888
3000
  this.options = {
2889
- receiveAudio: options.receiveAudio ?? PreferencesContainer.instance.receiveAudio,
2890
- receiveVideo: options.receiveVideo ?? PreferencesContainer.instance.receiveVideo,
2891
- ...options
3001
+ ...options,
3002
+ audio: options.audio ?? offerDefaults.audio,
3003
+ video: options.video ?? offerDefaults.video,
3004
+ receiveAudio: options.receiveAudio ?? offerDefaults.receiveAudio ?? PreferencesContainer.instance.receiveAudio,
3005
+ receiveVideo: options.receiveVideo ?? offerDefaults.receiveVideo ?? PreferencesContainer.instance.receiveVideo
2892
3006
  };
2893
3007
  this.localStreamController = new LocalStreamController({
2894
3008
  propose: this.propose,
@@ -3033,7 +3147,7 @@ var RTCPeerConnectionController = class extends Destroyable {
3033
3147
  };
3034
3148
  }
3035
3149
  get inputVideoDeviceConstraints() {
3036
- if (this.options.video === false && !this.options.inputVideoDeviceConstraints) return false;
3150
+ if (!this.options.video && !this.options.inputVideoDeviceConstraints) return false;
3037
3151
  return {
3038
3152
  ...this.options.inputVideoDeviceConstraints,
3039
3153
  ...this.deviceController.selectedVideoInputDeviceConstraints
@@ -3055,12 +3169,12 @@ var RTCPeerConnectionController = class extends Destroyable {
3055
3169
  default: return {
3056
3170
  ...options,
3057
3171
  offerToReceiveAudio: true,
3058
- offerToReceiveVideo: Boolean(this.inputVideoDeviceConstraints)
3172
+ offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
3059
3173
  };
3060
3174
  }
3061
3175
  }
3062
3176
  get answerOptions() {
3063
- return {};
3177
+ return { iceRestart: this.firstSDPExchangeCompleted ? true : void 0 };
3064
3178
  }
3065
3179
  /**
3066
3180
  * Initialize the RTCPeerConnection and setup event listeners.
@@ -3095,11 +3209,15 @@ var RTCPeerConnectionController = class extends Destroyable {
3095
3209
  });
3096
3210
  await this.updateSelectedInputDevice(kind, deviceInfo);
3097
3211
  });
3098
- await this.setupTrackHandling();
3099
- this._initialized$.next(true);
3100
3212
  if (this.type === "answer" && this.sdpInit) {
3213
+ await this.setupRemoteTracks();
3214
+ this._initialized$.next(true);
3101
3215
  this.setupEventListeners();
3102
- await this.handleOfferReceived();
3216
+ this._isNegotiating$.next(true);
3217
+ await this._setRemoteDescription(this.sdpInit);
3218
+ } else {
3219
+ await this.setupTrackHandling();
3220
+ this._initialized$.next(true);
3103
3221
  }
3104
3222
  } catch (error) {
3105
3223
  logger$11.error("[RTCPeerConnectionController] Initialization error:", error);
@@ -3197,6 +3315,35 @@ var RTCPeerConnectionController = class extends Destroyable {
3197
3315
  default:
3198
3316
  }
3199
3317
  }
3318
+ /**
3319
+ * Accept an inbound call by creating the SDP answer.
3320
+ * Optionally override media options before the answer is generated.
3321
+ * Must be called after initialization for inbound (answer-type) connections.
3322
+ */
3323
+ async acceptInbound(mediaOverrides) {
3324
+ if (mediaOverrides) {
3325
+ const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
3326
+ this.options = {
3327
+ ...this.options,
3328
+ ...audio !== void 0 ? { audio } : {},
3329
+ ...video !== void 0 ? { video } : {},
3330
+ ...receiveAudio !== void 0 ? { receiveAudio } : {},
3331
+ ...receiveVideo !== void 0 ? { receiveVideo } : {}
3332
+ };
3333
+ this.transceiverController?.updateOptions({
3334
+ receiveAudio: this.receiveAudio,
3335
+ receiveVideo: this.receiveVideo
3336
+ });
3337
+ this.localStreamController.updateOptions({
3338
+ inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
3339
+ inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
3340
+ });
3341
+ }
3342
+ await this.setupLocalTracks();
3343
+ const { answerOptions } = this;
3344
+ logger$11.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
3345
+ await this.createAnswer(answerOptions);
3346
+ }
3200
3347
  async handleOfferReceived() {
3201
3348
  if (!this.sdpInit) throw new DependencyError("SDP initialization parameters are not set");
3202
3349
  this._isNegotiating$.next(true);
@@ -3271,37 +3418,29 @@ var RTCPeerConnectionController = class extends Destroyable {
3271
3418
  }
3272
3419
  async setupLocalTracks() {
3273
3420
  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;
3421
+ const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
3422
+ if (this.transceiverController?.useAddStream ?? false) {
3423
+ logger$11.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
3424
+ this.peerConnection?.addStream(localStream);
3425
+ if (!this.isNegotiating) {
3426
+ logger$11.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
3427
+ this.negotiationNeeded$.next();
3290
3428
  }
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
- }
3429
+ return;
3430
+ }
3431
+ for (const kind of ["audio", "video"]) {
3432
+ const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
3433
+ index,
3434
+ track
3435
+ }));
3436
+ for (const { index, track } of tracks) {
3437
+ this.localStreamController.addTrackEndedListener(track);
3438
+ if (this.transceiverController?.useAddTransceivers ?? false) {
3439
+ const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
3440
+ await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
3441
+ } else {
3442
+ logger$11.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
3443
+ this.peerConnection?.addTrack(track, localStream);
3305
3444
  }
3306
3445
  }
3307
3446
  }
@@ -3318,7 +3457,12 @@ var RTCPeerConnectionController = class extends Destroyable {
3318
3457
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
3319
3458
  this.peerConnection.ontrack = (event) => {
3320
3459
  logger$11.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
3321
- this._remoteStream$.next(event.streams[0]);
3460
+ if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
3461
+ else {
3462
+ const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
3463
+ const newStream = new MediaStream([...existingTracks, event.track]);
3464
+ this._remoteStream$.next(newStream);
3465
+ }
3322
3466
  };
3323
3467
  await this.transceiverController?.setupRemoteTransceivers(this.type);
3324
3468
  }
@@ -3417,7 +3561,7 @@ var RTCPeerConnectionController = class extends Destroyable {
3417
3561
  });
3418
3562
  }
3419
3563
  get mediaDirections() {
3420
- return this.transceiverController?.getMediaDirections() ?? {
3564
+ return this.transceiverController?.getMediaDirections() ?? this._remoteOfferMediaDirections ?? {
3421
3565
  audio: "inactive",
3422
3566
  video: "inactive"
3423
3567
  };
@@ -3565,6 +3709,11 @@ var WebRTCVertoManager = class extends VertoManager {
3565
3709
  ].includes(connectionState)))));
3566
3710
  }
3567
3711
  initSubscriptions() {
3712
+ this.subscribeTo(this.callJoinedEvent$, (event) => {
3713
+ const memberNodeId = event.room_session.members.find((m) => m.call_id === event.call_id)?.node_id;
3714
+ if (memberNodeId) this.setNodeIdIfNull(memberNodeId);
3715
+ if (event.member_id) this.setSelfIdIfNull(event.member_id);
3716
+ });
3568
3717
  this.subscribeTo(this.vertoMedia$, (event) => {
3569
3718
  logger$10.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
3570
3719
  this._signalingStatus$.next("ringing");
@@ -3576,6 +3725,7 @@ var WebRTCVertoManager = class extends VertoManager {
3576
3725
  });
3577
3726
  this.subscribeTo(this.vertoAnswer$, (event) => {
3578
3727
  logger$10.debug("[WebRTCManager] Received Verto answer event:", event);
3728
+ this._signalingStatus$.next("connecting");
3579
3729
  const { sdp, callID } = event;
3580
3730
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
3581
3731
  status: "received",
@@ -3595,6 +3745,28 @@ var WebRTCVertoManager = class extends VertoManager {
3595
3745
  this.sendVertoPong(vertoPing);
3596
3746
  });
3597
3747
  }
3748
+ /**
3749
+ * Set node_id/selfId only when the current value is null.
3750
+ *
3751
+ * During reattach, `call.joined` and `verto.answer` events can deliver
3752
+ * these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
3753
+ * arrives. These methods let early events populate them eagerly so that
3754
+ * downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
3755
+ * identifiers. `processInviteResponse()` remains the authoritative source
3756
+ * and always overwrites unconditionally.
3757
+ */
3758
+ setNodeIdIfNull(nodeId) {
3759
+ if (!this._nodeId$.value && nodeId) {
3760
+ logger$10.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
3761
+ this._nodeId$.next(nodeId);
3762
+ }
3763
+ }
3764
+ setSelfIdIfNull(selfId) {
3765
+ if (!this._selfId$.value && selfId) {
3766
+ logger$10.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
3767
+ this._selfId$.next(selfId);
3768
+ }
3769
+ }
3598
3770
  async sendVertoPong(vertoPing) {
3599
3771
  try {
3600
3772
  const vertoPongMessage = VertoPong({ ...vertoPing });
@@ -3618,6 +3790,9 @@ var WebRTCVertoManager = class extends VertoManager {
3618
3790
  get selfId() {
3619
3791
  return this._selfId$.value;
3620
3792
  }
3793
+ get callJoinedEvent$() {
3794
+ return this.webRtcCallSession.callEvent$.pipe(filter(isCallJoinedPayload), takeUntil(this.destroyed$));
3795
+ }
3621
3796
  get vertoMedia$() {
3622
3797
  return this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaInnerParams, "params"), takeUntil(this.destroyed$));
3623
3798
  }
@@ -3647,13 +3822,13 @@ var WebRTCVertoManager = class extends VertoManager {
3647
3822
  if (response.error) {
3648
3823
  const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
3649
3824
  this.onError?.(error);
3650
- throw error;
3825
+ return response;
3651
3826
  }
3652
3827
  const innerResult = getValueFrom(response, "result.result");
3653
3828
  if (innerResult?.error) {
3654
3829
  const error = new JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
3655
3830
  this.onError?.(error);
3656
- throw error;
3831
+ return response;
3657
3832
  }
3658
3833
  return response;
3659
3834
  }
@@ -3673,7 +3848,7 @@ var WebRTCVertoManager = class extends VertoManager {
3673
3848
  }
3674
3849
  } catch (error) {
3675
3850
  logger$10.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
3676
- throw error;
3851
+ this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
3677
3852
  }
3678
3853
  }
3679
3854
  async processModifyResponse(response, rtcPeerConnController) {
@@ -3687,12 +3862,14 @@ var WebRTCVertoManager = class extends VertoManager {
3687
3862
  });
3688
3863
  } catch (error) {
3689
3864
  logger$10.warn("[WebRTCManager] Error processing modify response:", error);
3690
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
3865
+ const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
3866
+ this.onError?.(modifyError);
3691
3867
  }
3692
3868
  }
3693
3869
  }
3694
3870
  processInviteResponse(response, rtcPeerConnController) {
3695
3871
  if (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
3872
+ this._signalingStatus$.next("trying");
3696
3873
  this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
3697
3874
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
3698
3875
  const callId = getValueFrom(response, "result.result.result.callID") ?? null;
@@ -3747,6 +3924,36 @@ var WebRTCVertoManager = class extends VertoManager {
3747
3924
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
3748
3925
  this.onError?.(error);
3749
3926
  });
3927
+ if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
3928
+ }
3929
+ async handleInboundAnswer(rtcPeerConnController) {
3930
+ logger$10.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
3931
+ const vertoByeOrAccepted = await firstValueFrom(race(this.vertoBye$, this.webRtcCallSession.answered$).pipe(takeUntil(this.destroyed$))).catch(() => null);
3932
+ if (vertoByeOrAccepted === null) {
3933
+ logger$10.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
3934
+ return;
3935
+ }
3936
+ if (isVertoByeMessage(vertoByeOrAccepted)) {
3937
+ logger$10.info("[WebRTCManager] Inbound call ended by remote before answer.");
3938
+ this.callSession?.destroy();
3939
+ } else if (!vertoByeOrAccepted) {
3940
+ logger$10.info("[WebRTCManager] Inbound call rejected by user.");
3941
+ try {
3942
+ await this.bye("USER_BUSY");
3943
+ } finally {
3944
+ this._signalingStatus$.next("disconnected");
3945
+ this.callSession?.destroy();
3946
+ }
3947
+ } else {
3948
+ logger$10.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
3949
+ const answerOptions = this.webRtcCallSession.answerMediaOptions;
3950
+ try {
3951
+ await rtcPeerConnController.acceptInbound(answerOptions);
3952
+ } catch (error) {
3953
+ logger$10.error("[WebRTCManager] Error creating inbound answer:", error);
3954
+ this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
3955
+ }
3956
+ }
3750
3957
  }
3751
3958
  setupVertoAttachHandler() {
3752
3959
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
@@ -3764,7 +3971,7 @@ var WebRTCVertoManager = class extends VertoManager {
3764
3971
  });
3765
3972
  }
3766
3973
  initObservables(rtcPeerConnController) {
3767
- this.mediaDirections$ = rtcPeerConnController.connectionState$.pipe(filter((state) => state === "connected"), takeUntil(this.destroyed$), map(() => rtcPeerConnController.mediaDirections));
3974
+ this.mediaDirections$ = rtcPeerConnController.connectionState$.pipe(filter((state) => state === "connected"), map(() => rtcPeerConnController.mediaDirections), startWith(rtcPeerConnController.mediaDirections), takeUntil(this.destroyed$));
3768
3975
  this.localStream$ = rtcPeerConnController.localStream$.pipe(filterNull(), takeUntil(this.destroyed$));
3769
3976
  this.remoteStream$ = rtcPeerConnController.remoteStream$.pipe(filterNull(), takeUntil(this.destroyed$));
3770
3977
  }
@@ -3780,7 +3987,6 @@ var WebRTCVertoManager = class extends VertoManager {
3780
3987
  });
3781
3988
  this.sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnController);
3782
3989
  } else if (initial) {
3783
- this._signalingStatus$.next("trying");
3784
3990
  const vertoMessageRequest = VertoInvite({
3785
3991
  dialogParams,
3786
3992
  sdp
@@ -4086,8 +4292,8 @@ var WebRTCCall = class extends Destroyable {
4086
4292
  this.clientSession = clientSession;
4087
4293
  this.options = options;
4088
4294
  this.address = address;
4089
- this.participantsMap = /* @__PURE__ */ new Map();
4090
- this._errors$ = this.createSubject();
4295
+ this._errors$ = this.createReplaySubject(1);
4296
+ this._lastMergedStatus = "new";
4091
4297
  this._answered$ = this.createReplaySubject();
4092
4298
  this._holdState = false;
4093
4299
  this._userVariables$ = this.createBehaviorSubject({ ...PreferencesContainer.instance.userVariables });
@@ -4108,18 +4314,28 @@ var WebRTCCall = class extends Destroyable {
4108
4314
  const managers = initialization.initializeManagers(this);
4109
4315
  this.vertoManager = managers.vertoManager;
4110
4316
  this.callEventsManager = managers.callEventsManager;
4111
- if (options.initOffer) this._status$ = this.createBehaviorSubject("ringing");
4112
- else this._status$ = this.createBehaviorSubject("new");
4317
+ if (options.initOffer) {
4318
+ this._status$ = this.createBehaviorSubject("ringing");
4319
+ this._lastMergedStatus = "ringing";
4320
+ } else this._status$ = this.createBehaviorSubject("new");
4113
4321
  const { deviceController } = initialization;
4114
4322
  this.participantFactory = new ParticipantFactory(this.executeMethod.bind(this), this.vertoManager, deviceController);
4115
4323
  }
4116
4324
  /** Observable stream of errors from media, signaling, and peer connection layers. */
4117
4325
  get errors$() {
4118
- return this._errors$.asObservable();
4326
+ return this.deferEmission(this._errors$.asObservable());
4119
4327
  }
4120
- /** @internal Push an error to the call's error stream. */
4121
- emitError(error) {
4122
- this._errors$.next(error);
4328
+ /**
4329
+ * @internal Push an error to the call's error stream.
4330
+ * Fatal errors automatically transition the call to `'failed'` and destroy it.
4331
+ */
4332
+ emitError(callError) {
4333
+ if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
4334
+ this._errors$.next(callError);
4335
+ if (callError.fatal) {
4336
+ this._status$.next("failed");
4337
+ this.destroy();
4338
+ }
4123
4339
  }
4124
4340
  /** Whether this call is `'inbound'` or `'outbound'`. */
4125
4341
  get direction() {
@@ -4127,7 +4343,7 @@ var WebRTCCall = class extends Destroyable {
4127
4343
  }
4128
4344
  /** Observable of the address associated with this call. */
4129
4345
  get address$() {
4130
- return from([this.address]);
4346
+ return this.deferEmission(from([this.address])).pipe(takeUntil(this._destroyed$));
4131
4347
  }
4132
4348
  /** Display name of the caller. */
4133
4349
  get fromName() {
@@ -4159,7 +4375,7 @@ var WebRTCCall = class extends Destroyable {
4159
4375
  }
4160
4376
  /** Current snapshot of all participants in the call. */
4161
4377
  get participants() {
4162
- return Array.from(this.participantsMap.values());
4378
+ return this.callEventsManager.participants;
4163
4379
  }
4164
4380
  /** The local participant, or `null` if not yet joined. */
4165
4381
  get self() {
@@ -4168,7 +4384,6 @@ var WebRTCCall = class extends Destroyable {
4168
4384
  async toggleLock() {
4169
4385
  const method = this.locked ? "call.unlock" : "call.lock";
4170
4386
  await this.executeMethod(this.selfId ?? "", method, {});
4171
- throw new UnimplementedError();
4172
4387
  }
4173
4388
  async toggleHold() {
4174
4389
  if (this._holdState) await this.vertoManager.unhold();
@@ -4189,7 +4404,7 @@ var WebRTCCall = class extends Destroyable {
4189
4404
  }
4190
4405
  /** Observable of layout layer positions for all participants. */
4191
4406
  get layoutLayers$() {
4192
- return this.callEventsManager.layoutLayers$;
4407
+ return this.deferEmission(this.callEventsManager.layoutLayers$).pipe(takeUntil(this._destroyed$));
4193
4408
  }
4194
4409
  /** Current snapshot of layout layers. */
4195
4410
  get layoutLayers() {
@@ -4203,72 +4418,80 @@ var WebRTCCall = class extends Destroyable {
4203
4418
  params
4204
4419
  });
4205
4420
  try {
4206
- return await this.clientSession.execute(request);
4421
+ const response = await this.clientSession.execute(request);
4422
+ if (isJSONRPCErrorResponse(response)) throw new JSONRPCError(parseInt(response.result?.code ?? "0"), `Error response from method ${method}: ${response.result?.code} ${response.result?.message}`, void 0, void 0, request.id);
4423
+ return response;
4207
4424
  } catch (error) {
4208
4425
  logger$9.error(`[Call] Error executing method ${method} with params`, params, error);
4209
4426
  throw error;
4210
4427
  }
4211
4428
  }
4212
4429
  buildMethodParams(target, args) {
4213
- const reference = {
4214
- node_id: this.nodeId,
4215
- call_id: this.id
4430
+ const self = {
4431
+ node_id: this.nodeId ?? "",
4432
+ call_id: this.id,
4433
+ member_id: this.vertoManager.selfId ?? ""
4434
+ };
4435
+ if (typeof target === "object") return {
4436
+ ...args,
4437
+ self,
4438
+ targets: [target]
4216
4439
  };
4217
4440
  return {
4218
4441
  ...args,
4219
- self: {
4220
- ...reference,
4221
- member_id: this.vertoManager.selfId
4222
- },
4442
+ self,
4223
4443
  target: {
4224
- ...reference,
4444
+ node_id: this.nodeId ?? "",
4445
+ call_id: this.id,
4225
4446
  member_id: target
4226
4447
  }
4227
4448
  };
4228
4449
  }
4229
4450
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
4230
4451
  get status$() {
4231
- return this.cachedObservable("status$", () => merge(this._status$.asObservable(), this.vertoManager.signalingStatus$));
4452
+ return this.publicCachedObservable("status$", () => merge(this._status$.asObservable(), this.vertoManager.signalingStatus$).pipe(distinctUntilChanged(), tap((status) => {
4453
+ this._lastMergedStatus = status;
4454
+ })));
4232
4455
  }
4233
4456
  /** Observable of the participants list, emits on join/leave/update. */
4234
4457
  get participants$() {
4235
- return this.callEventsManager.participants$;
4458
+ return this.deferEmission(this.callEventsManager.participants$).pipe(takeUntil(this._destroyed$));
4236
4459
  }
4237
4460
  /** Observable of the local (self) participant. */
4238
4461
  get self$() {
4239
- return this.callEventsManager.self$;
4462
+ return this.deferEmission(this.callEventsManager.self$).pipe(takeUntil(this._destroyed$));
4240
4463
  }
4241
4464
  /** Observable indicating whether the call is being recorded. */
4242
4465
  get recording$() {
4243
- return this.callEventsManager.recording$;
4466
+ return this.deferEmission(this.callEventsManager.recording$).pipe(takeUntil(this._destroyed$));
4244
4467
  }
4245
4468
  /** Observable indicating whether the call is being streamed. */
4246
4469
  get streaming$() {
4247
- return this.callEventsManager.streaming$;
4470
+ return this.deferEmission(this.callEventsManager.streaming$).pipe(takeUntil(this._destroyed$));
4248
4471
  }
4249
4472
  /** Observable indicating whether raise-hand priority is active. */
4250
4473
  get raiseHandPriority$() {
4251
- return this.callEventsManager.raiseHandPriority$;
4474
+ return this.deferEmission(this.callEventsManager.raiseHandPriority$).pipe(takeUntil(this._destroyed$));
4252
4475
  }
4253
4476
  /** Observable indicating whether the call room is locked. */
4254
4477
  get locked$() {
4255
- return this.callEventsManager.locked$;
4478
+ return this.deferEmission(this.callEventsManager.locked$).pipe(takeUntil(this._destroyed$));
4256
4479
  }
4257
4480
  /** Observable of custom metadata associated with the call. */
4258
4481
  get meta$() {
4259
- return this.callEventsManager.meta$;
4482
+ return this.deferEmission(this.callEventsManager.meta$).pipe(takeUntil(this._destroyed$));
4260
4483
  }
4261
4484
  /** Observable of the call's capability flags. */
4262
4485
  get capabilities$() {
4263
- return this.callEventsManager.capabilities$;
4486
+ return this.deferEmission(this.callEventsManager.capabilities$).pipe(takeUntil(this._destroyed$));
4264
4487
  }
4265
4488
  /** Observable of the current layout name. */
4266
4489
  get layout$() {
4267
- return this.callEventsManager.layout$;
4490
+ return this.deferEmission(this.callEventsManager.layout$).pipe(takeUntil(this._destroyed$));
4268
4491
  }
4269
4492
  /** Current call status. */
4270
4493
  get status() {
4271
- return this._status$.value;
4494
+ return this._lastMergedStatus;
4272
4495
  }
4273
4496
  /** Whether the call is currently being recorded. */
4274
4497
  get recording() {
@@ -4296,7 +4519,7 @@ var WebRTCCall = class extends Destroyable {
4296
4519
  }
4297
4520
  /** Observable of available layout names. */
4298
4521
  get layouts$() {
4299
- return this.callEventsManager.layouts$;
4522
+ return this.deferEmission(this.callEventsManager.layouts$).pipe(takeUntil(this._destroyed$));
4300
4523
  }
4301
4524
  /** Current snapshot of available layout names. */
4302
4525
  get layouts() {
@@ -4304,7 +4527,7 @@ var WebRTCCall = class extends Destroyable {
4304
4527
  }
4305
4528
  /** Observable of the local media stream (camera/microphone). */
4306
4529
  get localStream$() {
4307
- return this.vertoManager.localStream$;
4530
+ return this.deferEmission(this.vertoManager.localStream$).pipe(takeUntil(this._destroyed$));
4308
4531
  }
4309
4532
  /** Current local media stream, or `null` if not available. */
4310
4533
  get localStream() {
@@ -4312,7 +4535,7 @@ var WebRTCCall = class extends Destroyable {
4312
4535
  }
4313
4536
  /** Observable of the remote media stream from the far end. */
4314
4537
  get remoteStream$() {
4315
- return this.vertoManager.remoteStream$;
4538
+ return this.deferEmission(this.vertoManager.remoteStream$).pipe(takeUntil(this._destroyed$));
4316
4539
  }
4317
4540
  /** Current remote media stream, or `null` if not available. */
4318
4541
  get remoteStream() {
@@ -4320,7 +4543,7 @@ var WebRTCCall = class extends Destroyable {
4320
4543
  }
4321
4544
  /** Observable of custom user variables associated with the call. */
4322
4545
  get userVariables$() {
4323
- return this._userVariables$.asObservable();
4546
+ return this.deferEmission(this._userVariables$.asObservable());
4324
4547
  }
4325
4548
  /** a copy of the current custom user variables of the call. */
4326
4549
  get userVariables() {
@@ -4340,7 +4563,7 @@ var WebRTCCall = class extends Destroyable {
4340
4563
  }
4341
4564
  /** Observable of the current audio/video send/receive directions. */
4342
4565
  get mediaDirections$() {
4343
- return this.vertoManager.mediaDirections$;
4566
+ return this.deferEmission(this.vertoManager.mediaDirections$).pipe(takeUntil(this._destroyed$));
4344
4567
  }
4345
4568
  /** Current audio/video send/receive directions. */
4346
4569
  get mediaDirections() {
@@ -4386,31 +4609,31 @@ var WebRTCCall = class extends Destroyable {
4386
4609
  }
4387
4610
  /** Observable of call-updated events. */
4388
4611
  get callUpdated$() {
4389
- return this.cachedObservable("callUpdated$", () => this.callSessionEvents$.pipe(filterAs(isCallUpdatedMetadata, "params"), takeUntil(this.destroyed$)));
4612
+ return this.publicCachedObservable("callUpdated$", () => this.callSessionEvents$.pipe(filterAs(isCallUpdatedMetadata, "params"), takeUntil(this.destroyed$)));
4390
4613
  }
4391
4614
  /** Observable of member-joined events. */
4392
4615
  get memberJoined$() {
4393
- return this.cachedObservable("memberJoined$", () => this.callSessionEvents$.pipe(filterAs(isMemberJoinedMetadata, "params"), takeUntil(this.destroyed$)));
4616
+ return this.publicCachedObservable("memberJoined$", () => this.callSessionEvents$.pipe(filterAs(isMemberJoinedMetadata, "params"), takeUntil(this.destroyed$)));
4394
4617
  }
4395
4618
  /** Observable of member-left events. */
4396
4619
  get memberLeft$() {
4397
- return this.cachedObservable("memberLeft$", () => this.callSessionEvents$.pipe(filterAs(isMemberLeftMetadata, "params"), takeUntil(this.destroyed$)));
4620
+ return this.publicCachedObservable("memberLeft$", () => this.callSessionEvents$.pipe(filterAs(isMemberLeftMetadata, "params"), takeUntil(this.destroyed$)));
4398
4621
  }
4399
4622
  /** Observable of member-updated events (mute, volume, etc.). */
4400
4623
  get memberUpdated$() {
4401
- return this.cachedObservable("memberUpdated$", () => this.callSessionEvents$.pipe(filterAs(isMemberUpdatedMetadata, "params"), takeUntil(this.destroyed$)));
4624
+ return this.publicCachedObservable("memberUpdated$", () => this.callSessionEvents$.pipe(filterAs(isMemberUpdatedMetadata, "params"), takeUntil(this.destroyed$)));
4402
4625
  }
4403
4626
  /** Observable of member-talking events (speech start/stop). */
4404
4627
  get memberTalking$() {
4405
- return this.cachedObservable("memberTalking$", () => this.callSessionEvents$.pipe(filterAs(isMemberTalkingMetadata, "params"), takeUntil(this.destroyed$)));
4628
+ return this.publicCachedObservable("memberTalking$", () => this.callSessionEvents$.pipe(filterAs(isMemberTalkingMetadata, "params"), takeUntil(this.destroyed$)));
4406
4629
  }
4407
4630
  /** Observable of call state-change events. */
4408
4631
  get callStates$() {
4409
- return this.cachedObservable("callStates$", () => this.callSessionEvents$.pipe(filterAs(isCallStateMetadata, "params"), takeUntil(this.destroyed$)));
4632
+ return this.publicCachedObservable("callStates$", () => this.callSessionEvents$.pipe(filterAs(isCallStateMetadata, "params"), takeUntil(this.destroyed$)));
4410
4633
  }
4411
4634
  /** Observable of layout-changed events. */
4412
4635
  get layoutUpdates$() {
4413
- return this.cachedObservable("layoutUpdates$", () => this.callSessionEvents$.pipe(filterAs(isLayoutChangedMetadata, "params"), takeUntil(this.destroyed$)));
4636
+ return this.publicCachedObservable("layoutUpdates$", () => this.callSessionEvents$.pipe(filterAs(isLayoutChangedMetadata, "params"), takeUntil(this.destroyed$)));
4414
4637
  }
4415
4638
  /** Underlying `RTCPeerConnection`, for advanced use cases. */
4416
4639
  get rtcPeerConnection() {
@@ -4418,7 +4641,7 @@ var WebRTCCall = class extends Destroyable {
4418
4641
  }
4419
4642
  /** Observable of raw signaling events as plain objects. */
4420
4643
  get signalingEvent$() {
4421
- return this.cachedObservable("signalingEvent$", () => this.callEvent$.pipe(map((event) => JSON.parse(JSON.stringify(event)))));
4644
+ return this.publicCachedObservable("signalingEvent$", () => this.callEvent$.pipe(map((event) => JSON.parse(JSON.stringify(event)))));
4422
4645
  }
4423
4646
  /** Observable of WebRTC-specific signaling messages. */
4424
4647
  get webrtcMessages$() {
@@ -4445,17 +4668,22 @@ var WebRTCCall = class extends Destroyable {
4445
4668
  async sendDigits(dtmf) {
4446
4669
  return this.vertoManager.sendDigits(dtmf);
4447
4670
  }
4448
- /** Accepts an inbound call. */
4449
- answer() {
4671
+ /** Accepts an inbound call, optionally overriding media options for the answer. */
4672
+ answer(options) {
4673
+ this._answerMediaOptions = options;
4450
4674
  this._answered$.next(true);
4451
4675
  }
4676
+ /** Media options provided when answering. Used internally by the VertoManager. */
4677
+ get answerMediaOptions() {
4678
+ return this._answerMediaOptions;
4679
+ }
4452
4680
  /** Rejects an inbound call. */
4453
4681
  reject() {
4454
4682
  this._answered$.next(false);
4455
4683
  }
4456
4684
  /** Observable that emits `true` when answered, `false` when rejected. */
4457
4685
  get answered$() {
4458
- return this._answered$.asObservable();
4686
+ return this.deferEmission(this._answered$.asObservable());
4459
4687
  }
4460
4688
  /**
4461
4689
  * Sets the call layout and participant positions.
@@ -4476,10 +4704,10 @@ var WebRTCCall = class extends Destroyable {
4476
4704
  }
4477
4705
  /** Destroys the call, releasing all resources and subscriptions. */
4478
4706
  destroy() {
4707
+ if (this._status$.value === "destroyed") return;
4479
4708
  this._status$.next("destroyed");
4480
4709
  this.vertoManager.destroy();
4481
4710
  this.callEventsManager.destroy();
4482
- this.participantsMap.clear();
4483
4711
  super.destroy();
4484
4712
  }
4485
4713
  };
@@ -4487,6 +4715,23 @@ var WebRTCCall = class extends Destroyable {
4487
4715
  //#endregion
4488
4716
  //#region src/managers/CallFactory.ts
4489
4717
  /**
4718
+ * Infers the semantic error category from a raw Error thrown by VertoManager
4719
+ * or an RTCPeerConnection layer.
4720
+ */
4721
+ function inferCallErrorKind(error) {
4722
+ if (error instanceof RPCTimeoutError) return "timeout";
4723
+ if (error instanceof JSONRPCError) return "signaling";
4724
+ if (error instanceof MediaTrackError) return "media";
4725
+ if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
4726
+ return "internal";
4727
+ }
4728
+ /** Determines whether an error should be fatal (destroy the call). */
4729
+ function isFatalError(error) {
4730
+ if (error instanceof VertoPongError) return false;
4731
+ if (error instanceof MediaTrackError) return false;
4732
+ return true;
4733
+ }
4734
+ /**
4490
4735
  * Factory for creating WebRTCCall instances with proper manager wiring.
4491
4736
  * Eliminates circular dependencies by centralizing Call and Manager creation.
4492
4737
  */
@@ -4507,7 +4752,13 @@ var CallFactory = class {
4507
4752
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
4508
4753
  nodeId: options.nodeId,
4509
4754
  onError: (error) => {
4510
- callInstance.emitError(error);
4755
+ const callError = {
4756
+ kind: inferCallErrorKind(error),
4757
+ fatal: isFatalError(error),
4758
+ error,
4759
+ callId: callInstance.id
4760
+ };
4761
+ callInstance.emitError(callError);
4511
4762
  }
4512
4763
  }),
4513
4764
  callEventsManager: new CallEventsManager(callInstance)
@@ -4884,9 +5135,15 @@ var PendingRPC = class PendingRPC {
4884
5135
  return () => signal.removeEventListener("abort", abortHandler);
4885
5136
  }) : NEVER).subscribe({
4886
5137
  next: (response) => {
4887
- logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
4888
5138
  isSettled = true;
4889
- resolve(response);
5139
+ if (response.error) {
5140
+ const rpcError = new JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
5141
+ logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
5142
+ reject(rpcError);
5143
+ } else {
5144
+ logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
5145
+ resolve(response);
5146
+ }
4890
5147
  subscription.unsubscribe();
4891
5148
  },
4892
5149
  error: (error) => {
@@ -4942,7 +5199,7 @@ var ClientSessionManager = class extends Destroyable {
4942
5199
  revision: 0
4943
5200
  };
4944
5201
  this._authorization$ = this.createBehaviorSubject(void 0);
4945
- this._errors$ = this.createSubject();
5202
+ this._errors$ = this.createReplaySubject(1);
4946
5203
  this._authenticated$ = this.createBehaviorSubject(false);
4947
5204
  this._subscriberInfo$ = this.createBehaviorSubject(null);
4948
5205
  this._calls$ = this.createBehaviorSubject({});
@@ -5170,6 +5427,7 @@ var ClientSessionManager = class extends Destroyable {
5170
5427
  ...params,
5171
5428
  ...persistedParams
5172
5429
  });
5430
+ this.transport.resetSessionEpoch();
5173
5431
  const response = await lastValueFrom(from(this.transport.execute(rpcConnectRequest)).pipe(throwOnRPCError(), map((res) => res.result), filter(isRPCConnectResult), tap(() => {
5174
5432
  logger$6.debug("[Session] Response passed filter, processing authentication result");
5175
5433
  }), take(1), catchError((err) => {
@@ -5212,12 +5470,13 @@ var ClientSessionManager = class extends Destroyable {
5212
5470
  }
5213
5471
  async createOutboundCall(destination, options = {}) {
5214
5472
  const destinationURI = destination instanceof Address ? destination.defaultChannel : destination;
5473
+ let callSession;
5215
5474
  try {
5216
- const callSession = await this.createCall({
5475
+ callSession = await this.createCall({
5217
5476
  to: destinationURI,
5218
5477
  ...options
5219
5478
  });
5220
- await firstValueFrom(callSession.selfId$.pipe(filter((id) => Boolean(id)), take(1), timeout(this.callCreateTimeout)));
5479
+ await firstValueFrom(race(callSession.selfId$.pipe(filter((id) => Boolean(id)), take(1), timeout(this.callCreateTimeout)), callSession.errors$.pipe(take(1), switchMap((callError) => throwError(() => callError.error)))));
5221
5480
  this._calls$.next({
5222
5481
  [`${callSession.id}`]: callSession,
5223
5482
  ...this._calls$.value
@@ -5225,7 +5484,8 @@ var ClientSessionManager = class extends Destroyable {
5225
5484
  return callSession;
5226
5485
  } catch (error) {
5227
5486
  logger$6.error("[Session] Error creating outbound call:", error);
5228
- const callError = new CallCreateError("Call create timeout", error);
5487
+ callSession?.destroy();
5488
+ const callError = new CallCreateError(error instanceof TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
5229
5489
  this._errors$.next(callError);
5230
5490
  throw callError;
5231
5491
  }
@@ -5251,7 +5511,7 @@ var ClientSessionManager = class extends Destroyable {
5251
5511
  return callSession;
5252
5512
  } catch (error) {
5253
5513
  logger$6.error("[Session] Error creating call session:", error);
5254
- throw new CallCreateError("Call create error", error);
5514
+ throw new CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
5255
5515
  }
5256
5516
  }
5257
5517
  destroy() {
@@ -5497,7 +5757,7 @@ var WebSocketController = class WebSocketController extends Destroyable {
5497
5757
  this.shouldReconnect = false;
5498
5758
  this._status$ = this.createBehaviorSubject("disconnected");
5499
5759
  this._incomingMessages$ = this.createSubject();
5500
- this._errors$ = this.createSubject();
5760
+ this._errors$ = this.createReplaySubject(1);
5501
5761
  this.reconnectDelayMin = options.reconnectDelayMin ?? WebSocketController.DEFAULT_RECONNECT_DELAY_MIN_MS;
5502
5762
  this.reconnectDelayMax = options.reconnectDelayMax ?? WebSocketController.DEFAULT_RECONNECT_DELAY_MAX_MS;
5503
5763
  this.connectionTimeout = options.connectionTimeout ?? WebSocketController.DEFAULT_CONNECTION_TIMEOUT_MS;
@@ -5647,7 +5907,30 @@ function isSignalwirePingRequest(value) {
5647
5907
  //#endregion
5648
5908
  //#region src/managers/TransportManager.ts
5649
5909
  const logger$2 = getLogger();
5650
- var TransportManager = class extends Destroyable {
5910
+ var TransportManager = class TransportManager extends Destroyable {
5911
+ /**
5912
+ * Normalise a server event timestamp to epoch seconds.
5913
+ *
5914
+ * The server uses two formats:
5915
+ * - `webrtc.message`: float epoch seconds (e.g. 1774372099.022817)
5916
+ * - all other events: int epoch microseconds (e.g. 1774372099925857)
5917
+ *
5918
+ * Values above 1e12 are treated as microseconds and divided by 1e6.
5919
+ */
5920
+ static toEpochSeconds(ts) {
5921
+ const n = typeof ts === "string" ? Number(ts) : ts;
5922
+ return n > 0xe8d4a51000 ? n / 1e6 : n;
5923
+ }
5924
+ /**
5925
+ * Extract the event timestamp from a signalwire.event message.
5926
+ * Returns `null` for messages that have no timestamp
5927
+ * (e.g. signalwire.authorization.state, RPC responses).
5928
+ */
5929
+ static extractEventTimestamp(message) {
5930
+ if (!isSignalwireRequest(message)) return null;
5931
+ if (message.params.event_type === "signalwire.authorization.state") return null;
5932
+ return TransportManager.toEpochSeconds(message.params.timestamp);
5933
+ }
5651
5934
  constructor(storage, protocolKey, webSocketConstructor, relayHost, onError) {
5652
5935
  super();
5653
5936
  this.storage = storage;
@@ -5680,6 +5963,23 @@ var TransportManager = class extends Destroyable {
5680
5963
  return true;
5681
5964
  });
5682
5965
  };
5966
+ this.discardStaleEvents = () => {
5967
+ return filter((message) => {
5968
+ const ts = TransportManager.extractEventTimestamp(message);
5969
+ if (ts === null) return true;
5970
+ if (this._sessionEpoch === null) {
5971
+ this._sessionEpoch = ts;
5972
+ return true;
5973
+ }
5974
+ if (ts < this._sessionEpoch) {
5975
+ const eventType = isSignalwireRequest(message) ? message.params.event_type : "unknown";
5976
+ logger$2.warn(`[Transport] Discarding stale event: ${eventType} (timestamp=${ts.toFixed(3)}, sessionEpoch=${this._sessionEpoch.toFixed(3)}, delta=${(this._sessionEpoch - ts).toFixed(3)}s)`);
5977
+ return false;
5978
+ }
5979
+ return true;
5980
+ });
5981
+ };
5982
+ this._sessionEpoch = null;
5683
5983
  this._outgoingMessages$ = this.createSubject();
5684
5984
  this._webSocketConnections = new WebSocketController(webSocketConstructor, relayHost, this._outgoingMessages$.asObservable(), {
5685
5985
  connectionTimeout: PreferencesContainer.instance.connectionTimeout,
@@ -5704,7 +6004,14 @@ var TransportManager = class extends Destroyable {
5704
6004
  return EMPTY;
5705
6005
  }), share(), takeUntil(this.destroyed$));
5706
6006
  this._jsonRPCResponse$ = this._jsonRPCMessage$.pipe(filter(isJSONRPCResponse));
5707
- this._incomingEvent$ = this._jsonRPCMessage$.pipe(this.ackEvent(), this.replySignalwirePing(), filter((message) => !isJSONRPCResponse(message)), share(), takeUntil(this.destroyed$));
6007
+ this._incomingEvent$ = this._jsonRPCMessage$.pipe(this.ackEvent(), this.replySignalwirePing(), filter((message) => !isJSONRPCResponse(message)), this.discardStaleEvents(), share(), takeUntil(this.destroyed$));
6008
+ }
6009
+ /**
6010
+ * Reset the session epoch. Call this before each signalwire.connect
6011
+ * so that the first event after authentication establishes the new baseline.
6012
+ */
6013
+ resetSessionEpoch() {
6014
+ this._sessionEpoch = null;
5708
6015
  }
5709
6016
  async setProtocol(protocol) {
5710
6017
  this.protocol$.next(protocol);
@@ -5822,7 +6129,8 @@ const buildOptionsFromDestination = (destination) => {
5822
6129
  const channel = new URLSearchParams(queryString).get("channel");
5823
6130
  if (channel === "video") return {
5824
6131
  audio: true,
5825
- video: true
6132
+ video: true,
6133
+ receiveVideo: true
5826
6134
  };
5827
6135
  else if (channel === "audio") return {
5828
6136
  audio: true,
@@ -5852,7 +6160,7 @@ var SignalWire = class extends Destroyable {
5852
6160
  this._directory$ = this.createBehaviorSubject(void 0);
5853
6161
  this._isConnected$ = this.createBehaviorSubject(false);
5854
6162
  this._isRegistered$ = this.createBehaviorSubject(false);
5855
- this._errors$ = this.createSubject();
6163
+ this._errors$ = this.createReplaySubject(1);
5856
6164
  this._options = {};
5857
6165
  this._deps = new DependencyContainer();
5858
6166
  this._options = {
@@ -5929,7 +6237,12 @@ var SignalWire = class extends Destroyable {
5929
6237
  this._subscriber$.next(new Subscriber(this._deps.http));
5930
6238
  if (!this._options.skipConnection) await this.connect();
5931
6239
  if (!this._options.reconnectAttachedCalls && this._attachManager) await this._attachManager.flush();
5932
- if (!this._options.skipRegister) this.register();
6240
+ if (!this._options.skipRegister) try {
6241
+ await this.register();
6242
+ } catch (error) {
6243
+ logger$1.error("[SignalWire] Registration failed:", error);
6244
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
6245
+ }
5933
6246
  this.handleAttachments();
5934
6247
  }
5935
6248
  async handleAttachments() {
@@ -6021,7 +6334,7 @@ var SignalWire = class extends Destroyable {
6021
6334
  * ```
6022
6335
  */
6023
6336
  get subscriber$() {
6024
- return this._subscriber$.asObservable();
6337
+ return this.deferEmission(this._subscriber$.asObservable());
6025
6338
  }
6026
6339
  /** Current subscriber snapshot, or `undefined` if not yet authenticated. */
6027
6340
  get subscriber() {
@@ -6040,7 +6353,7 @@ var SignalWire = class extends Destroyable {
6040
6353
  * ```
6041
6354
  */
6042
6355
  get directory$() {
6043
- return this._directory$.asObservable();
6356
+ return this.deferEmission(this._directory$.asObservable());
6044
6357
  }
6045
6358
  /**
6046
6359
  * Current directory snapshot, or `undefined` if the client is not yet connected.
@@ -6051,7 +6364,7 @@ var SignalWire = class extends Destroyable {
6051
6364
  }
6052
6365
  /** Observable that emits when the subscriber registration state changes. */
6053
6366
  get isRegistered$() {
6054
- return this._isRegistered$.asObservable();
6367
+ return this.deferEmission(this._isRegistered$.asObservable());
6055
6368
  }
6056
6369
  /** Whether the subscriber is currently registered. */
6057
6370
  get isRegistered() {
@@ -6063,15 +6376,15 @@ var SignalWire = class extends Destroyable {
6063
6376
  }
6064
6377
  /** Observable that emits when the connection state changes. */
6065
6378
  get isConnected$() {
6066
- return this._isConnected$.asObservable();
6379
+ return this.deferEmission(this._isConnected$.asObservable());
6067
6380
  }
6068
6381
  /** Observable that emits `true` when the client is both connected and authenticated. */
6069
6382
  get ready$() {
6070
- return this.cachedObservable("ready$", () => this._isConnected$.pipe(switchMap((connected) => connected ? this._clientSession.authenticated$ : of(false))));
6383
+ return this.publicCachedObservable("ready$", () => this._isConnected$.pipe(switchMap((connected) => connected ? this._clientSession.authenticated$ : of(false))));
6071
6384
  }
6072
6385
  /** Observable stream of errors from transport, authentication, and devices. */
6073
6386
  get errors$() {
6074
- return this._errors$.asObservable();
6387
+ return this.deferEmission(this._errors$.asObservable());
6075
6388
  }
6076
6389
  /** Disconnects the WebSocket and tears down the session. */
6077
6390
  async disconnect() {
@@ -6092,8 +6405,22 @@ var SignalWire = class extends Destroyable {
6092
6405
  }));
6093
6406
  this._isRegistered$.next(true);
6094
6407
  } catch (error) {
6095
- logger$1.error("[SignalWire] Failed to register subscriber:", error);
6408
+ logger$1.debug("[SignalWire] Failed to register subscriber, trying reauthentication...");
6409
+ if (this._deps.credential.token) this._clientSession.reauthenticate(this._deps.credential.token).then(async () => {
6410
+ logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
6411
+ await this._transport.execute(RPCExecute({
6412
+ method: "subscriber.online",
6413
+ params: {}
6414
+ }));
6415
+ this._isRegistered$.next(true);
6416
+ }).catch((reauthError) => {
6417
+ logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
6418
+ const registerError = new 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 }) });
6419
+ this._errors$.next(registerError);
6420
+ throw registerError;
6421
+ });
6096
6422
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
6423
+ throw error;
6097
6424
  }
6098
6425
  }
6099
6426
  /** Unregisters the subscriber, going offline for inbound calls. */
@@ -6132,7 +6459,7 @@ var SignalWire = class extends Destroyable {
6132
6459
  }
6133
6460
  /** Observable list of available audio input (microphone) devices. */
6134
6461
  get audioInputDevices$() {
6135
- return this._deviceController.audioInputDevices$;
6462
+ return this.deferEmission(this._deviceController.audioInputDevices$);
6136
6463
  }
6137
6464
  /** Current snapshot of available audio input devices. */
6138
6465
  get audioInputDevices() {
@@ -6140,7 +6467,7 @@ var SignalWire = class extends Destroyable {
6140
6467
  }
6141
6468
  /** Observable list of available audio output (speaker) devices. */
6142
6469
  get audioOutputDevices$() {
6143
- return this._deviceController.audioOutputDevices$;
6470
+ return this.deferEmission(this._deviceController.audioOutputDevices$);
6144
6471
  }
6145
6472
  /** Current snapshot of available audio output devices. */
6146
6473
  get audioOutputDevices() {
@@ -6148,7 +6475,7 @@ var SignalWire = class extends Destroyable {
6148
6475
  }
6149
6476
  /** Observable list of available video input (camera) devices. */
6150
6477
  get videoInputDevices$() {
6151
- return this._deviceController.videoInputDevices$;
6478
+ return this.deferEmission(this._deviceController.videoInputDevices$);
6152
6479
  }
6153
6480
  /** Current snapshot of available video input devices. */
6154
6481
  get videoInputDevices() {
@@ -6156,15 +6483,15 @@ var SignalWire = class extends Destroyable {
6156
6483
  }
6157
6484
  /** Observable of the currently selected audio input device. */
6158
6485
  get selectedAudioInputDevice$() {
6159
- return this._deviceController.selectedAudioInputDevice$;
6486
+ return this.deferEmission(this._deviceController.selectedAudioInputDevice$);
6160
6487
  }
6161
6488
  /** Observable of the currently selected audio output device. */
6162
6489
  get selectedAudioOutputDevice$() {
6163
- return this._deviceController.selectedAudioOutputDevice$;
6490
+ return this.deferEmission(this._deviceController.selectedAudioOutputDevice$);
6164
6491
  }
6165
6492
  /** Observable of the currently selected video input device. */
6166
6493
  get selectedVideoInputDevice$() {
6167
- return this._deviceController.selectedVideoInputDevice$;
6494
+ return this.deferEmission(this._deviceController.selectedVideoInputDevice$);
6168
6495
  }
6169
6496
  /** Currently selected audio input device, or `null` if none. */
6170
6497
  get selectedAudioInputDevice() {