@signalwire/js 4.0.0-dev-20260911182554 → 4.0.0-dev-20260911205444

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
@@ -9094,6 +9094,8 @@ const MEDIA_ACCESS_DENIAL_NAMES = [
9094
9094
  "SecurityError",
9095
9095
  "PermissionDeniedError"
9096
9096
  ];
9097
+ /** Error names browsers use when the capture hardware is already held exclusively. */
9098
+ const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
9097
9099
  /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9098
9100
  const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9099
9101
  /** Number of initial samples used to build a baseline for spike detection. */
@@ -9432,6 +9434,10 @@ var MediaTrackError = class extends Error {
9432
9434
  function isMediaAccessDenial(originalError) {
9433
9435
  return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9434
9436
  }
9437
+ /** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
9438
+ function isMediaDeviceInUse(originalError) {
9439
+ return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
9440
+ }
9435
9441
  /**
9436
9442
  * Failure to acquire local media (camera, microphone, or screen capture)
9437
9443
  * via `getUserMedia`/`getDisplayMedia`.
@@ -14248,6 +14254,8 @@ var SelfParticipant = class extends Participant {
14248
14254
  *
14249
14255
  * The call is unaffected when acquisition fails.
14250
14256
  *
14257
+ * @param options - Pass `{ audio: true }` to also request the shared
14258
+ * surface's audio. Defaults to video only.
14251
14259
  * @throws {ScreenShareAlreadyActiveError} When this call is already
14252
14260
  * sharing a screen. Call {@link stopScreenShare} before starting another.
14253
14261
  * @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
@@ -14256,9 +14264,9 @@ var SelfParticipant = class extends Participant {
14256
14264
  * permission denial rejects with a `NotAllowedError` `DOMException` —
14257
14265
  * inspect `error.name` to tell benign cancels apart from real failures.
14258
14266
  */
14259
- async startScreenShare() {
14267
+ async startScreenShare(options) {
14260
14268
  try {
14261
- await this.vertoManager.addScreenMedia();
14269
+ await this.vertoManager.addScreenMedia(options);
14262
14270
  } catch (error) {
14263
14271
  if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
14264
14272
  else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
@@ -15608,10 +15616,11 @@ var LocalStreamController = class extends Destroyable {
15608
15616
  stream = new MediaStream(tracks);
15609
15617
  this.tagTracks(tracks, "application");
15610
15618
  } else if (this.options.propose === "screenshare") {
15611
- logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15619
+ const audio = this.options.screenShareAudio ?? false;
15620
+ logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
15612
15621
  stream = await this.options.getDisplayMedia({
15613
15622
  video: true,
15614
- audio: Boolean(this.options.inputAudioDeviceConstraints)
15623
+ audio
15615
15624
  });
15616
15625
  logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
15617
15626
  this.tagTracks(stream.getTracks(), "display");
@@ -16047,38 +16056,32 @@ var RTCPeerConnectionController = class extends Destroyable {
16047
16056
  this.negotiationNeeded$.next();
16048
16057
  };
16049
16058
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
16059
+ const { localStream } = this;
16060
+ if (!localStream) {
16061
+ logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
16062
+ return;
16063
+ }
16064
+ const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
16065
+ if (!currentTrack) {
16066
+ logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
16067
+ return;
16068
+ }
16069
+ if (!deviceInfo) {
16070
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
16071
+ this.stopTrackSender(kind);
16072
+ return;
16073
+ }
16074
+ const constraints = {
16075
+ ...currentTrack.getConstraints(),
16076
+ ...this.deviceController.deviceInfoToConstraints(deviceInfo)
16077
+ };
16050
16078
  try {
16051
- const { localStream } = this;
16052
- if (!localStream) {
16053
- logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
16054
- return;
16055
- }
16056
- logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
16057
- const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
16058
- if (track) {
16059
- this.transceiverController?.stopTrackSender(kind);
16060
- this.localStreamController.removeTrack(track.id);
16061
- logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
16062
- if (!deviceInfo) {
16063
- logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
16064
- return;
16065
- }
16066
- const streamTrack = (await this.getUserMedia({ [kind]: {
16067
- ...track.getConstraints(),
16068
- ...this.deviceController.deviceInfoToConstraints(deviceInfo)
16069
- } })).getTracks().find((t) => t.kind === kind);
16070
- if (streamTrack) {
16071
- logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
16072
- this.localStreamController.addTrack(streamTrack);
16073
- await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
16074
- logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
16075
- }
16076
- }
16077
- logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
16079
+ const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
16080
+ await this.attachInputTrack(kind, newTrack, currentTrack);
16081
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
16078
16082
  } catch (error) {
16079
16083
  logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
16080
16084
  this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
16081
- throw error;
16082
16085
  }
16083
16086
  };
16084
16087
  this._isNegotiating$ = this.createBehaviorSubject(false);
@@ -16123,6 +16126,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16123
16126
  inputVideoStream: this.options.inputVideoStream,
16124
16127
  inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
16125
16128
  inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
16129
+ screenShareAudio: this.options.screenShareAudio,
16126
16130
  getUserMedia: async (constraints) => this.getUserMedia(constraints),
16127
16131
  getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
16128
16132
  });
@@ -16780,6 +16784,61 @@ var RTCPeerConnectionController = class extends Destroyable {
16780
16784
  this._localAudioPipeline.setInputTrack(newTrack);
16781
16785
  }
16782
16786
  /**
16787
+ * Capture the newly selected device, leaving the current capture running.
16788
+ *
16789
+ * A rejection must leave the current track sending, so nothing is released
16790
+ * until the replacement is in hand. The one exception is hardware that admits
16791
+ * a single opener — a phone's front and back cameras, typically — which
16792
+ * rejects the second capture until the first is closed.
16793
+ */
16794
+ async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
16795
+ try {
16796
+ return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
16797
+ } catch (error) {
16798
+ if (!isMediaDeviceInUse(error)) throw error;
16799
+ logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
16800
+ const previousDeviceId = currentTrack.getSettings().deviceId;
16801
+ this.stopTrackSender(kind);
16802
+ try {
16803
+ return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
16804
+ } catch (retryError) {
16805
+ await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
16806
+ throw retryError;
16807
+ }
16808
+ }
16809
+ }
16810
+ async captureTrack(kind, constraints, deviceId) {
16811
+ const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
16812
+ const track = stream.getTracks().find((t) => t.kind === kind);
16813
+ if (!track) {
16814
+ stream.getTracks().forEach((t) => t.stop());
16815
+ throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
16816
+ }
16817
+ if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
16818
+ return track;
16819
+ }
16820
+ /** Best-effort return to the device that was released for an exclusive retry. */
16821
+ async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
16822
+ try {
16823
+ const restored = await this.captureTrack(kind, constraints, previousDeviceId);
16824
+ await this.attachInputTrack(kind, restored, releasedTrack);
16825
+ } catch (error) {
16826
+ logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
16827
+ }
16828
+ }
16829
+ async attachInputTrack(kind, newTrack, oldTrack) {
16830
+ const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
16831
+ if (!pipelineOwnsAudio) try {
16832
+ await this.transceiverController?.replaceSenderTrack(kind, newTrack);
16833
+ } catch (error) {
16834
+ newTrack.stop();
16835
+ throw error;
16836
+ }
16837
+ this.localStreamController.removeTrack(oldTrack.id);
16838
+ this.localStreamController.addTrack(newTrack);
16839
+ if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
16840
+ }
16841
+ /**
16783
16842
  * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
16784
16843
  * first access. On creation the current audio sender's track is routed
16785
16844
  * through the pipeline (input → gain → analyser → destination) and the
@@ -17855,8 +17914,11 @@ var WebRTCVertoManager = class extends VertoManager {
17855
17914
  throw error;
17856
17915
  }
17857
17916
  }
17858
- async addScreenMedia(options = { audio: false }) {
17859
- await this.initAdditionalPeerConnection("screenshare", options);
17917
+ async addScreenMedia(options = {}) {
17918
+ await this.initAdditionalPeerConnection("screenshare", {
17919
+ audio: false,
17920
+ screenShareAudio: options.audio ?? false
17921
+ });
17860
17922
  }
17861
17923
  async initAdditionalPeerConnection(propose, options) {
17862
17924
  const isScreenShare = propose === "screenshare";