@signalwire/js 4.0.0-dev-20260309151622 → 4.0.0-dev-20260311174824

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.mjs CHANGED
@@ -6915,7 +6915,7 @@ var require_startWith = /* @__PURE__ */ __commonJSMin(((exports) => {
6915
6915
  var concat_1$2 = require_concat$1();
6916
6916
  var args_1$2 = require_args();
6917
6917
  var lift_1$14 = require_lift();
6918
- function startWith() {
6918
+ function startWith$1() {
6919
6919
  var values = [];
6920
6920
  for (var _i = 0; _i < arguments.length; _i++) values[_i] = arguments[_i];
6921
6921
  var scheduler = args_1$2.popScheduler(values);
@@ -6923,7 +6923,7 @@ var require_startWith = /* @__PURE__ */ __commonJSMin(((exports) => {
6923
6923
  (scheduler ? concat_1$2.concat(values, source, scheduler) : concat_1$2.concat(values, source)).subscribe(subscriber);
6924
6924
  });
6925
6925
  }
6926
- exports.startWith = startWith;
6926
+ exports.startWith = startWith$1;
6927
6927
  }));
6928
6928
 
6929
6929
  //#endregion
@@ -9669,7 +9669,7 @@ var PreferencesContainer = class PreferencesContainer {
9669
9669
  skipDeviceMonitoring: false,
9670
9670
  savePreferences: false
9671
9671
  };
9672
- this.receiveVideo = true;
9672
+ this.receiveVideo = false;
9673
9673
  this.receiveAudio = true;
9674
9674
  this.preferredAudioInput = null;
9675
9675
  this.preferredAudioOutput = null;
@@ -12639,12 +12639,53 @@ var CallEventsManager = class extends Destroyable {
12639
12639
 
12640
12640
  //#endregion
12641
12641
  //#region src/helpers/SDPHelper.ts
12642
+ /** Valid SDP direction attribute values. */
12643
+ const SDP_DIRECTIONS = new Set([
12644
+ "sendrecv",
12645
+ "sendonly",
12646
+ "recvonly",
12647
+ "inactive"
12648
+ ]);
12642
12649
  /**
12643
- * SDPHelper - Utility functions for SDP (Session Description Protocol) parsing and validation.
12650
+ * Extracts the media directions (audio/video) from an SDP string.
12644
12651
  *
12645
- * This module provides helper functions to analyze and validate SDP content,
12646
- * particularly for ICE candidate validation in WebRTC connections.
12652
+ * Parses each media section (`m=audio` / `m=video`) and reads the `a=` direction
12653
+ * attribute (`sendrecv`, `sendonly`, `recvonly`, `inactive`).
12654
+ * If no explicit direction attribute is found for a media section, defaults to `sendrecv`
12655
+ * per RFC 4566.
12656
+ *
12657
+ * @param sdp - The SDP string to parse
12658
+ * @returns The extracted audio and video directions
12659
+ *
12660
+ * @example
12661
+ * ```typescript
12662
+ * 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`;
12663
+ * extractMediaDirectionsFromSDP(sdp);
12664
+ * // { audio: 'sendrecv', video: 'recvonly' }
12665
+ * ```
12647
12666
  */
12667
+ function extractMediaDirectionsFromSDP(sdp) {
12668
+ const result = {
12669
+ audio: "inactive",
12670
+ video: "inactive"
12671
+ };
12672
+ if (!sdp) return result;
12673
+ const lines = sdp.split(/\r?\n/);
12674
+ let currentMediaKind = null;
12675
+ let currentDirection = null;
12676
+ for (const line of lines) if (line.startsWith("m=")) {
12677
+ if (currentMediaKind) result[currentMediaKind] = currentDirection ?? "sendrecv";
12678
+ if (line.startsWith("m=audio")) currentMediaKind = "audio";
12679
+ else if (line.startsWith("m=video")) currentMediaKind = "video";
12680
+ else currentMediaKind = null;
12681
+ currentDirection = null;
12682
+ } else if (currentMediaKind && line.startsWith("a=")) {
12683
+ const attr = line.substring(2).trim();
12684
+ if (SDP_DIRECTIONS.has(attr)) currentDirection = attr;
12685
+ }
12686
+ if (currentMediaKind) result[currentMediaKind] = currentDirection ?? "sendrecv";
12687
+ return result;
12688
+ }
12648
12689
  /**
12649
12690
  * Validates that an SDP string has at least one non-host ICE candidate
12650
12691
  * for each media section (m= line).
@@ -12947,6 +12988,15 @@ var LocalStreamController = class extends Destroyable {
12947
12988
  track.addEventListener("ended", this.mediaTrackEndedHandler);
12948
12989
  }
12949
12990
  /**
12991
+ * Update the controller options (e.g., when media overrides are applied).
12992
+ */
12993
+ updateOptions(options) {
12994
+ this.options = {
12995
+ ...this.options,
12996
+ ...options
12997
+ };
12998
+ }
12999
+ /**
12950
13000
  * Stop all local tracks and clean up.
12951
13001
  */
12952
13002
  stopAllTracks() {
@@ -13290,6 +13340,7 @@ var RTCPeerConnectionController = class extends Destroyable {
13290
13340
  this._initialized$ = this.createReplaySubject(1);
13291
13341
  this._remoteDescription$ = this.createReplaySubject(1);
13292
13342
  this._remoteStream$ = this.createBehaviorSubject(null);
13343
+ this._remoteOfferMediaDirections = null;
13293
13344
  this.deviceController = deviceController ?? {};
13294
13345
  this.id = options.callId ?? v4_default();
13295
13346
  this._type = remoteSessionDescription ? "answer" : "offer";
@@ -13297,10 +13348,19 @@ var RTCPeerConnectionController = class extends Destroyable {
13297
13348
  type: "offer",
13298
13349
  sdp: remoteSessionDescription
13299
13350
  } : void 0;
13351
+ this._remoteOfferMediaDirections = remoteSessionDescription ? extractMediaDirectionsFromSDP(remoteSessionDescription) : null;
13352
+ const offerDefaults = this._remoteOfferMediaDirections ? {
13353
+ audio: this._remoteOfferMediaDirections.audio.includes("recv"),
13354
+ video: this._remoteOfferMediaDirections.video.includes("recv"),
13355
+ receiveAudio: this._remoteOfferMediaDirections.audio.includes("send"),
13356
+ receiveVideo: this._remoteOfferMediaDirections.video.includes("send")
13357
+ } : {};
13300
13358
  this.options = {
13301
- receiveAudio: options.receiveAudio ?? PreferencesContainer.instance.receiveAudio,
13302
- receiveVideo: options.receiveVideo ?? PreferencesContainer.instance.receiveVideo,
13303
- ...options
13359
+ ...options,
13360
+ audio: options.audio ?? offerDefaults.audio,
13361
+ video: options.video ?? offerDefaults.video,
13362
+ receiveAudio: options.receiveAudio ?? offerDefaults.receiveAudio ?? PreferencesContainer.instance.receiveAudio,
13363
+ receiveVideo: options.receiveVideo ?? offerDefaults.receiveVideo ?? PreferencesContainer.instance.receiveVideo
13304
13364
  };
13305
13365
  this.localStreamController = new LocalStreamController({
13306
13366
  propose: this.propose,
@@ -13467,12 +13527,12 @@ var RTCPeerConnectionController = class extends Destroyable {
13467
13527
  default: return {
13468
13528
  ...options,
13469
13529
  offerToReceiveAudio: true,
13470
- offerToReceiveVideo: Boolean(this.inputVideoDeviceConstraints)
13530
+ offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
13471
13531
  };
13472
13532
  }
13473
13533
  }
13474
13534
  get answerOptions() {
13475
- return {};
13535
+ return { iceRestart: this.firstSDPExchangeCompleted ? true : void 0 };
13476
13536
  }
13477
13537
  /**
13478
13538
  * Initialize the RTCPeerConnection and setup event listeners.
@@ -13507,11 +13567,15 @@ var RTCPeerConnectionController = class extends Destroyable {
13507
13567
  });
13508
13568
  await this.updateSelectedInputDevice(kind, deviceInfo);
13509
13569
  });
13510
- await this.setupTrackHandling();
13511
- this._initialized$.next(true);
13512
13570
  if (this.type === "answer" && this.sdpInit) {
13571
+ await this.setupRemoteTracks();
13572
+ this._initialized$.next(true);
13513
13573
  this.setupEventListeners();
13514
- await this.handleOfferReceived();
13574
+ this._isNegotiating$.next(true);
13575
+ await this._setRemoteDescription(this.sdpInit);
13576
+ } else {
13577
+ await this.setupTrackHandling();
13578
+ this._initialized$.next(true);
13515
13579
  }
13516
13580
  } catch (error) {
13517
13581
  logger$11.error("[RTCPeerConnectionController] Initialization error:", error);
@@ -13609,6 +13673,35 @@ var RTCPeerConnectionController = class extends Destroyable {
13609
13673
  default:
13610
13674
  }
13611
13675
  }
13676
+ /**
13677
+ * Accept an inbound call by creating the SDP answer.
13678
+ * Optionally override media options before the answer is generated.
13679
+ * Must be called after initialization for inbound (answer-type) connections.
13680
+ */
13681
+ async acceptInbound(mediaOverrides) {
13682
+ if (mediaOverrides) {
13683
+ const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
13684
+ this.options = {
13685
+ ...this.options,
13686
+ ...audio !== void 0 ? { audio } : {},
13687
+ ...video !== void 0 ? { video } : {},
13688
+ ...receiveAudio !== void 0 ? { receiveAudio } : {},
13689
+ ...receiveVideo !== void 0 ? { receiveVideo } : {}
13690
+ };
13691
+ this.transceiverController?.updateOptions({
13692
+ receiveAudio: this.receiveAudio,
13693
+ receiveVideo: this.receiveVideo
13694
+ });
13695
+ this.localStreamController.updateOptions({
13696
+ inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
13697
+ inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
13698
+ });
13699
+ }
13700
+ await this.setupLocalTracks();
13701
+ const { answerOptions } = this;
13702
+ logger$11.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
13703
+ await this.createAnswer(answerOptions);
13704
+ }
13612
13705
  async handleOfferReceived() {
13613
13706
  if (!this.sdpInit) throw new DependencyError("SDP initialization parameters are not set");
13614
13707
  this._isNegotiating$.next(true);
@@ -13829,7 +13922,7 @@ var RTCPeerConnectionController = class extends Destroyable {
13829
13922
  });
13830
13923
  }
13831
13924
  get mediaDirections() {
13832
- return this.transceiverController?.getMediaDirections() ?? {
13925
+ return this.transceiverController?.getMediaDirections() ?? this._remoteOfferMediaDirections ?? {
13833
13926
  audio: "inactive",
13834
13927
  video: "inactive"
13835
13928
  };
@@ -14160,6 +14253,36 @@ var WebRTCVertoManager = class extends VertoManager {
14160
14253
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
14161
14254
  this.onError?.(error);
14162
14255
  });
14256
+ if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
14257
+ }
14258
+ async handleInboundAnswer(rtcPeerConnController) {
14259
+ logger$10.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
14260
+ const vertoByeOrAccepted = await (0, import_cjs$10.firstValueFrom)((0, import_cjs$10.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, import_cjs$10.takeUntil)(this.destroyed$))).catch(() => null);
14261
+ if (vertoByeOrAccepted === null) {
14262
+ logger$10.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
14263
+ return;
14264
+ }
14265
+ if (isVertoByeMessage(vertoByeOrAccepted)) {
14266
+ logger$10.info("[WebRTCManager] Inbound call ended by remote before answer.");
14267
+ this.callSession?.destroy();
14268
+ } else if (!vertoByeOrAccepted) {
14269
+ logger$10.info("[WebRTCManager] Inbound call rejected by user.");
14270
+ try {
14271
+ await this.bye("USER_BUSY");
14272
+ } finally {
14273
+ this._signalingStatus$.next("disconnected");
14274
+ this.callSession?.destroy();
14275
+ }
14276
+ } else {
14277
+ logger$10.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
14278
+ const answerOptions = this.webRtcCallSession.answerMediaOptions;
14279
+ try {
14280
+ await rtcPeerConnController.acceptInbound(answerOptions);
14281
+ } catch (error) {
14282
+ logger$10.error("[WebRTCManager] Error creating inbound answer:", error);
14283
+ this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
14284
+ }
14285
+ }
14163
14286
  }
14164
14287
  setupVertoAttachHandler() {
14165
14288
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
@@ -14177,7 +14300,7 @@ var WebRTCVertoManager = class extends VertoManager {
14177
14300
  });
14178
14301
  }
14179
14302
  initObservables(rtcPeerConnController) {
14180
- this.mediaDirections$ = rtcPeerConnController.connectionState$.pipe((0, import_cjs$10.filter)((state) => state === "connected"), (0, import_cjs$10.takeUntil)(this.destroyed$), (0, import_cjs$10.map)(() => rtcPeerConnController.mediaDirections));
14303
+ this.mediaDirections$ = rtcPeerConnController.connectionState$.pipe((0, import_cjs$10.filter)((state) => state === "connected"), (0, import_cjs$10.map)(() => rtcPeerConnController.mediaDirections), (0, import_cjs$10.startWith)(rtcPeerConnController.mediaDirections), (0, import_cjs$10.takeUntil)(this.destroyed$));
14181
14304
  this.localStream$ = rtcPeerConnController.localStream$.pipe(filterNull(), (0, import_cjs$10.takeUntil)(this.destroyed$));
14182
14305
  this.remoteStream$ = rtcPeerConnController.remoteStream$.pipe(filterNull(), (0, import_cjs$10.takeUntil)(this.destroyed$));
14183
14306
  }
@@ -14859,10 +14982,15 @@ var WebRTCCall = class extends Destroyable {
14859
14982
  async sendDigits(dtmf) {
14860
14983
  return this.vertoManager.sendDigits(dtmf);
14861
14984
  }
14862
- /** Accepts an inbound call. */
14863
- answer() {
14985
+ /** Accepts an inbound call, optionally overriding media options for the answer. */
14986
+ answer(options) {
14987
+ this._answerMediaOptions = options;
14864
14988
  this._answered$.next(true);
14865
14989
  }
14990
+ /** Media options provided when answering. Used internally by the VertoManager. */
14991
+ get answerMediaOptions() {
14992
+ return this._answerMediaOptions;
14993
+ }
14866
14994
  /** Rejects an inbound call. */
14867
14995
  reject() {
14868
14996
  this._answered$.next(false);