@signalwire/js 4.0.0-dev-20260911195739 → 4.0.0-dev-20260914173015

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`.
@@ -16050,38 +16056,32 @@ var RTCPeerConnectionController = class extends Destroyable {
16050
16056
  this.negotiationNeeded$.next();
16051
16057
  };
16052
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
+ };
16053
16078
  try {
16054
- const { localStream } = this;
16055
- if (!localStream) {
16056
- logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
16057
- return;
16058
- }
16059
- logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
16060
- const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
16061
- if (track) {
16062
- this.transceiverController?.stopTrackSender(kind);
16063
- this.localStreamController.removeTrack(track.id);
16064
- logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
16065
- if (!deviceInfo) {
16066
- logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
16067
- return;
16068
- }
16069
- const streamTrack = (await this.getUserMedia({ [kind]: {
16070
- ...track.getConstraints(),
16071
- ...this.deviceController.deviceInfoToConstraints(deviceInfo)
16072
- } })).getTracks().find((t) => t.kind === kind);
16073
- if (streamTrack) {
16074
- logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
16075
- this.localStreamController.addTrack(streamTrack);
16076
- await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
16077
- logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
16078
- }
16079
- }
16080
- 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);
16081
16082
  } catch (error) {
16082
16083
  logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
16083
16084
  this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
16084
- throw error;
16085
16085
  }
16086
16086
  };
16087
16087
  this._isNegotiating$ = this.createBehaviorSubject(false);
@@ -16784,6 +16784,61 @@ var RTCPeerConnectionController = class extends Destroyable {
16784
16784
  this._localAudioPipeline.setInputTrack(newTrack);
16785
16785
  }
16786
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
+ /**
16787
16842
  * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
16788
16843
  * first access. On creation the current audio sender's track is routed
16789
16844
  * through the pipeline (input → gain → analyser → destination) and the
@@ -17519,16 +17574,12 @@ var WebRTCVertoManager = class extends VertoManager {
17519
17574
  const webrtcVertoMessage = WebrtcVerto(params);
17520
17575
  const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
17521
17576
  const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
17522
- if (response.error) {
17523
- const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
17524
- this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
17525
- return response;
17526
- }
17527
17577
  const innerResult = getValueFrom(response, "result.result");
17528
- if (innerResult?.error) {
17529
- const error = new JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
17578
+ const failure = response.error ?? innerResult?.error;
17579
+ if (failure) {
17580
+ const error = new JSONRPCError(failure.code, failure.message, failure.data);
17581
+ if (message.method === "verto.invite") throw error;
17530
17582
  this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
17531
- return response;
17532
17583
  }
17533
17584
  return response;
17534
17585
  }
@@ -17583,7 +17634,7 @@ var WebRTCVertoManager = class extends VertoManager {
17583
17634
  if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
17584
17635
  }
17585
17636
  processInviteResponse(response, rtcPeerConnController) {
17586
- if (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
17637
+ if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
17587
17638
  this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
17588
17639
  const nodeId = getValueFrom(response, "result.node_id") ?? null;
17589
17640
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
@@ -17610,8 +17661,7 @@ var WebRTCVertoManager = class extends VertoManager {
17610
17661
  logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
17611
17662
  } else {
17612
17663
  logger$16.error("[WebRTCManager] Verto invite failed:", response);
17613
- const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
17614
- this.reportLegError(inviteError, rtcPeerConnController);
17664
+ this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
17615
17665
  }
17616
17666
  }
17617
17667
  get RTCPeerConnectionConfig() {