react-jssip-kit 0.7.6 → 0.7.7

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.d.ts CHANGED
@@ -57,10 +57,6 @@ type SipConfiguration = Omit<UAConfiguration, "password" | "uri"> & {
57
57
  * Maximum allowed concurrent sessions. Additional sessions are rejected.
58
58
  */
59
59
  maxSessionCount?: number;
60
- /**
61
- * Milliseconds to keep enqueued outgoing media before dropping. Defaults to 30000.
62
- */
63
- pendingMediaTtlMs?: number;
64
60
  };
65
61
 
66
62
  type StartOpts = {
@@ -173,10 +169,6 @@ type SipClientOptions = {
173
169
  errorHandler?: SipErrorHandler;
174
170
  debug?: boolean | string;
175
171
  };
176
- type MicrophoneRecoveryOptions = {
177
- intervalMs?: number;
178
- maxRetries?: number;
179
- };
180
172
  declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
181
173
  readonly userAgent: SipUserAgent;
182
174
  readonly stateStore: SipStateStore;
@@ -189,8 +181,6 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
189
181
  private sessionManager;
190
182
  private lifecycle;
191
183
  private micRecovery;
192
- private micRecoveryEnabled;
193
- private micRecoveryDefaults;
194
184
  private unloadHandler?;
195
185
  private stateLogOff?;
196
186
  get state(): SipState;
@@ -229,8 +219,6 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
229
219
  sendDTMFSession(sessionId: string, tones: string | number, options?: DTMFOptions): boolean;
230
220
  transferSession(sessionId: string, target: string, options?: ReferOptions): boolean;
231
221
  setSessionMedia(sessionId: string, stream: MediaStream): void;
232
- enableMicrophoneRecovery(sessionId: string, options?: MicrophoneRecoveryOptions): () => void;
233
- disableMicrophoneRecovery(sessionId: string): boolean;
234
222
  switchCameraSession(sessionId: string, track: MediaStreamTrack): false | Promise<boolean>;
235
223
  enableVideoSession(sessionId: string): boolean;
236
224
  disableVideoSession(sessionId: string): boolean;
@@ -274,8 +262,6 @@ declare function useSipActions(): {
274
262
  session: jssip_src_RTCSession.RTCSession;
275
263
  }[];
276
264
  setSessionMedia: (sessionId: string, stream: MediaStream) => void;
277
- enableMicrophoneRecovery: (sessionId: string, options?: MicrophoneRecoveryOptions | undefined) => () => void;
278
- disableMicrophoneRecovery: (sessionId: string) => boolean;
279
265
  switchCamera: (sessionId: string, track: MediaStreamTrack) => false | Promise<boolean>;
280
266
  enableVideo: (sessionId: string) => boolean;
281
267
  disableVideo: (sessionId: string) => boolean;
package/dist/index.js CHANGED
@@ -476,7 +476,6 @@ function createSessionHandlers(deps) {
476
476
  rtc,
477
477
  detachSessionHandlers,
478
478
  onSessionFailed,
479
- onSessionConfirmed,
480
479
  sessionId
481
480
  } = deps;
482
481
  return {
@@ -497,7 +496,7 @@ function createSessionHandlers(deps) {
497
496
  },
498
497
  confirmed: (e) => {
499
498
  emitter.emit("confirmed", e);
500
- onSessionConfirmed?.(sessionId);
499
+ deps.enableMicrophoneRecovery?.(sessionId);
501
500
  },
502
501
  ended: (e) => {
503
502
  emitter.emit("ended", e);
@@ -609,8 +608,8 @@ var WebRTCSessionController = class {
609
608
  }
610
609
  cleanup(stopTracks = true) {
611
610
  const pc = this.getPC();
611
+ const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
612
612
  if (pc && typeof pc.getSenders === "function") {
613
- const isClosed = pc.connectionState === "closed" || pc.signalingState === "closed";
614
613
  if (!isClosed) {
615
614
  for (const s of pc.getSenders()) {
616
615
  try {
@@ -621,8 +620,14 @@ var WebRTCSessionController = class {
621
620
  }
622
621
  }
623
622
  if (stopTracks && this.mediaStream) {
624
- for (const t of this.mediaStream.getTracks())
623
+ const senderTracks = pc && !isClosed ? new Set(
624
+ pc.getSenders().map((s) => s.track).filter((t) => Boolean(t))
625
+ ) : null;
626
+ for (const t of this.mediaStream.getTracks()) {
627
+ if (senderTracks?.has(t))
628
+ continue;
625
629
  t.stop();
630
+ }
626
631
  }
627
632
  this.mediaStream = null;
628
633
  this.currentSession = null;
@@ -701,29 +706,14 @@ var WebRTCSessionController = class {
701
706
  var SessionManager = class {
702
707
  constructor() {
703
708
  this.entries = /* @__PURE__ */ new Map();
704
- this.pendingMediaQueue = [];
705
- this.pendingMediaTtlMs = 3e4;
706
- }
707
- setPendingMediaTtl(ms) {
708
- if (typeof ms === "number" && ms > 0)
709
- this.pendingMediaTtlMs = ms;
710
- }
711
- enqueueOutgoingMedia(stream) {
712
- this.pendingMediaQueue.push({ stream, addedAt: Date.now() });
713
- }
714
- dequeueOutgoingMedia() {
715
- const now = Date.now();
716
- while (this.pendingMediaQueue.length) {
717
- const next = this.pendingMediaQueue.shift();
718
- if (!next)
719
- break;
720
- if (now - next.addedAt <= this.pendingMediaTtlMs) {
721
- return next.stream;
722
- } else {
723
- next.stream.getTracks().forEach((t) => t.stop());
724
- }
709
+ }
710
+ stopMediaStream(stream) {
711
+ if (!stream)
712
+ return;
713
+ for (const t of stream.getTracks()) {
714
+ if (t.readyState !== "ended")
715
+ t.stop();
725
716
  }
726
- return null;
727
717
  }
728
718
  getOrCreateRtc(sessionId, session) {
729
719
  let entry = this.entries.get(sessionId);
@@ -765,6 +755,9 @@ var SessionManager = class {
765
755
  session: null,
766
756
  media: null
767
757
  };
758
+ if (entry.media && entry.media !== stream) {
759
+ this.stopMediaStream(entry.media);
760
+ }
768
761
  entry.media = stream;
769
762
  entry.rtc.setMediaStream(stream);
770
763
  this.entries.set(sessionId, entry);
@@ -794,15 +787,16 @@ var SessionManager = class {
794
787
  const entry = this.entries.get(sessionId);
795
788
  if (entry) {
796
789
  entry.rtc.cleanup();
790
+ this.stopMediaStream(entry.media);
797
791
  this.entries.delete(sessionId);
798
792
  }
799
793
  }
800
794
  cleanupAllSessions() {
801
795
  for (const [, entry] of this.entries.entries()) {
802
796
  entry.rtc.cleanup();
797
+ this.stopMediaStream(entry.media);
803
798
  }
804
799
  this.entries.clear();
805
- this.pendingMediaQueue = [];
806
800
  }
807
801
  answer(sessionId, options) {
808
802
  const rtc = this.getRtc(sessionId);
@@ -850,35 +844,102 @@ var SessionLifecycle = class {
850
844
  }
851
845
  handleNewRTCSession(e) {
852
846
  const session = e.session;
853
- const sessionId = String(session?.id ?? crypto.randomUUID?.() ?? Date.now());
847
+ const sessionId = String(
848
+ session?.id ?? crypto.randomUUID?.() ?? Date.now()
849
+ );
854
850
  const currentSessions = this.state.getState().sessions;
855
851
  if (currentSessions.length >= this.getMaxSessionCount()) {
856
852
  try {
857
- session.terminate?.({ status_code: 486, reason_phrase: "Busy Here" });
853
+ session.terminate?.({
854
+ status_code: 486,
855
+ reason_phrase: "Busy Here"
856
+ });
858
857
  } catch {
859
858
  }
860
859
  if (e.originator === "remote") {
861
860
  this.emit("missed", e);
861
+ } else {
862
+ this.emitError(
863
+ "max session count reached",
864
+ "MAX_SESSIONS_REACHED",
865
+ "max session count reached"
866
+ );
862
867
  }
863
- this.emitError("max session count reached", "MAX_SESSIONS_REACHED", "max session count reached");
864
868
  return;
865
869
  }
866
- const outgoingMedia = e.originator === "local" ? this.sessionManager.dequeueOutgoingMedia() : null;
867
- if (outgoingMedia)
868
- this.sessionManager.setSessionMedia(sessionId, outgoingMedia);
869
870
  const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
870
- if (outgoingMedia)
871
- rtc.setMediaStream(outgoingMedia);
872
871
  this.sessionManager.setSession(sessionId, session);
873
872
  this.attachSessionHandlers(sessionId, session);
874
- holdOtherSessions(
875
- this.state,
876
- sessionId,
877
- (id) => {
878
- const otherRtc = this.sessionManager.getRtc(id);
879
- otherRtc?.hold();
873
+ if (e.originator === "local" && !rtc.mediaStream) {
874
+ const maxAttempts = 5;
875
+ const retryDelayMs = 500;
876
+ let attempts = 0;
877
+ let retryScheduled = false;
878
+ let retryTimer = null;
879
+ let stopped = false;
880
+ const tryBindFromPc = (pc) => {
881
+ if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
882
+ return false;
883
+ }
884
+ const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
885
+ const audioTrack = audioSender?.track;
886
+ if (!audioTrack)
887
+ return false;
888
+ const outgoingStream = new MediaStream([audioTrack]);
889
+ this.sessionManager.setSessionMedia(sessionId, outgoingStream);
890
+ return true;
891
+ };
892
+ const scheduleRetry = (pc) => {
893
+ if (stopped || retryScheduled || attempts >= maxAttempts) {
894
+ session.off?.("peerconnection", onPeer);
895
+ return;
896
+ }
897
+ retryScheduled = true;
898
+ attempts += 1;
899
+ retryTimer = setTimeout(() => {
900
+ retryScheduled = false;
901
+ retryTimer = null;
902
+ if (tryBindFromPc(pc)) {
903
+ session.off?.("peerconnection", onPeer);
904
+ return;
905
+ }
906
+ scheduleRetry(pc);
907
+ }, retryDelayMs);
908
+ };
909
+ const onPeer = (data) => {
910
+ if (stopped)
911
+ return;
912
+ if (tryBindFromPc(data.peerconnection)) {
913
+ session.off?.("peerconnection", onPeer);
914
+ return;
915
+ }
916
+ scheduleRetry(data.peerconnection);
917
+ };
918
+ const stopRetry = () => {
919
+ if (stopped)
920
+ return;
921
+ stopped = true;
922
+ if (retryTimer) {
923
+ clearTimeout(retryTimer);
924
+ retryTimer = null;
925
+ }
926
+ session.off?.("peerconnection", onPeer);
927
+ session.off?.("ended", stopRetry);
928
+ session.off?.("failed", stopRetry);
929
+ };
930
+ const existingPc = session?.connection;
931
+ if (!tryBindFromPc(existingPc)) {
932
+ if (existingPc)
933
+ scheduleRetry(existingPc);
934
+ session.on?.("peerconnection", onPeer);
880
935
  }
881
- );
936
+ session.on?.("ended", stopRetry);
937
+ session.on?.("failed", stopRetry);
938
+ }
939
+ holdOtherSessions(this.state, sessionId, (id) => {
940
+ const otherRtc = this.sessionManager.getRtc(id);
941
+ otherRtc?.hold();
942
+ });
882
943
  const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
883
944
  upsertSessionState(this.state, sessionId, {
884
945
  direction: e.originator,
@@ -892,6 +953,126 @@ var SessionLifecycle = class {
892
953
  }
893
954
  };
894
955
 
956
+ // src/jssip-lib/sip/micRecovery.ts
957
+ var MicRecoveryManager = class {
958
+ constructor(deps) {
959
+ this.enabled = false;
960
+ this.defaults = {
961
+ intervalMs: 2e3,
962
+ maxRetries: Infinity
963
+ };
964
+ this.active = /* @__PURE__ */ new Map();
965
+ this.deps = deps;
966
+ }
967
+ configure(config) {
968
+ if (typeof config.enabled === "boolean") {
969
+ this.enabled = config.enabled;
970
+ }
971
+ if (typeof config.intervalMs === "number") {
972
+ this.defaults.intervalMs = config.intervalMs;
973
+ }
974
+ if (typeof config.maxRetries === "number") {
975
+ this.defaults.maxRetries = config.maxRetries;
976
+ }
977
+ }
978
+ enable(sessionId, options = {}) {
979
+ if (!this.enabled)
980
+ return () => {
981
+ };
982
+ this.disable(sessionId);
983
+ const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
984
+ const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
985
+ let retries = 0;
986
+ let stopped = false;
987
+ const startedAt = Date.now();
988
+ const warmupMs = Math.max(intervalMs * 2, 2e3);
989
+ const tick = async () => {
990
+ if (stopped || retries >= maxRetries)
991
+ return;
992
+ const rtc = this.deps.getRtc(sessionId);
993
+ const session2 = this.deps.getSession(sessionId);
994
+ if (!rtc || !session2)
995
+ return;
996
+ const sessionState = this.deps.getSessionState(sessionId);
997
+ if (sessionState?.muted)
998
+ return;
999
+ const stream = rtc.mediaStream;
1000
+ const track = stream?.getAudioTracks?.()[0];
1001
+ const pc2 = session2?.connection;
1002
+ const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1003
+ if (!track && !sender)
1004
+ return;
1005
+ if (Date.now() - startedAt < warmupMs)
1006
+ return;
1007
+ if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
1008
+ return;
1009
+ }
1010
+ const trackLive = track?.readyState === "live";
1011
+ const senderLive = sender?.track?.readyState === "live";
1012
+ if (trackLive && senderLive)
1013
+ return;
1014
+ this.deps.emitError(
1015
+ {
1016
+ cause: "microphone dropped",
1017
+ trackLive,
1018
+ senderLive
1019
+ },
1020
+ "MICROPHONE_DROPPED",
1021
+ "microphone dropped"
1022
+ );
1023
+ retries += 1;
1024
+ if (trackLive && !senderLive && track) {
1025
+ await rtc.replaceAudioTrack(track);
1026
+ return;
1027
+ }
1028
+ let nextStream;
1029
+ try {
1030
+ const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1031
+ nextStream = await this.deps.requestMicrophoneStream(deviceId);
1032
+ } catch (err) {
1033
+ console.warn("[sip] mic recovery failed to get stream", err);
1034
+ return;
1035
+ }
1036
+ const nextTrack = nextStream.getAudioTracks()[0];
1037
+ if (!nextTrack)
1038
+ return;
1039
+ await rtc.replaceAudioTrack(nextTrack);
1040
+ this.deps.setSessionMedia(sessionId, nextStream);
1041
+ };
1042
+ const timer = setInterval(() => {
1043
+ void tick();
1044
+ }, intervalMs);
1045
+ void tick();
1046
+ const session = this.deps.getSession(sessionId);
1047
+ const pc = session?.connection;
1048
+ const onIceChange = () => {
1049
+ const state = pc?.iceConnectionState;
1050
+ if (state === "failed" || state === "disconnected")
1051
+ void tick();
1052
+ };
1053
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1054
+ const stop = () => {
1055
+ stopped = true;
1056
+ clearInterval(timer);
1057
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1058
+ };
1059
+ this.active.set(sessionId, { stop });
1060
+ return stop;
1061
+ }
1062
+ disable(sessionId) {
1063
+ const entry = this.active.get(sessionId);
1064
+ if (!entry)
1065
+ return false;
1066
+ entry.stop();
1067
+ this.active.delete(sessionId);
1068
+ return true;
1069
+ }
1070
+ cleanupAll() {
1071
+ this.active.forEach((entry) => entry.stop());
1072
+ this.active.clear();
1073
+ }
1074
+ };
1075
+
895
1076
  // src/jssip-lib/sip/client.ts
896
1077
  var SESSION_DEBUG_KEY = "sip-debug-enabled";
897
1078
  var SipClient = class extends EventTargetEmitter {
@@ -902,12 +1083,6 @@ var SipClient = class extends EventTargetEmitter {
902
1083
  this.sessionHandlers = /* @__PURE__ */ new Map();
903
1084
  this.maxSessionCount = Infinity;
904
1085
  this.sessionManager = new SessionManager();
905
- this.micRecovery = /* @__PURE__ */ new Map();
906
- this.micRecoveryEnabled = false;
907
- this.micRecoveryDefaults = {
908
- intervalMs: 2e3,
909
- maxRetries: Infinity
910
- };
911
1086
  this.errorHandler = options.errorHandler ?? new SipErrorHandler({
912
1087
  formatter: options.formatError,
913
1088
  messages: options.errorMessages
@@ -929,6 +1104,14 @@ var SipClient = class extends EventTargetEmitter {
929
1104
  attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
930
1105
  getMaxSessionCount: () => this.maxSessionCount
931
1106
  });
1107
+ this.micRecovery = new MicRecoveryManager({
1108
+ getRtc: (sessionId) => this.sessionManager.getRtc(sessionId),
1109
+ getSession: (sessionId) => this.sessionManager.getSession(sessionId),
1110
+ getSessionState: (sessionId) => this.stateStore.getState().sessions.find((s) => s.id === sessionId),
1111
+ setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream),
1112
+ emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1113
+ requestMicrophoneStream: (deviceId) => this.requestMicrophoneStreamInternal(deviceId)
1114
+ });
932
1115
  if (typeof window !== "undefined") {
933
1116
  window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
934
1117
  }
@@ -945,18 +1128,14 @@ var SipClient = class extends EventTargetEmitter {
945
1128
  micRecoveryIntervalMs,
946
1129
  micRecoveryMaxRetries,
947
1130
  maxSessionCount,
948
- pendingMediaTtlMs,
949
1131
  ...uaCfg
950
1132
  } = config;
951
1133
  this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
952
- this.micRecoveryEnabled = Boolean(enableMicRecovery);
953
- if (typeof micRecoveryIntervalMs === "number") {
954
- this.micRecoveryDefaults.intervalMs = micRecoveryIntervalMs;
955
- }
956
- if (typeof micRecoveryMaxRetries === "number") {
957
- this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
958
- }
959
- this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
1134
+ this.micRecovery.configure({
1135
+ enabled: Boolean(enableMicRecovery),
1136
+ intervalMs: micRecoveryIntervalMs,
1137
+ maxRetries: micRecoveryMaxRetries
1138
+ });
960
1139
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
961
1140
  this.userAgent.start(uri, password, uaCfg, { debug });
962
1141
  this.attachUAHandlers();
@@ -976,10 +1155,15 @@ var SipClient = class extends EventTargetEmitter {
976
1155
  call(target, callOptions = {}) {
977
1156
  try {
978
1157
  const opts = this.ensureMediaConstraints(callOptions);
979
- if (opts.mediaStream)
980
- this.sessionManager.enqueueOutgoingMedia(opts.mediaStream);
981
1158
  const ua = this.userAgent.getUA();
982
- ua?.call(target, opts);
1159
+ const session = ua?.call(target, opts);
1160
+ if (session && opts.mediaStream) {
1161
+ const sessionId = String(session?.id ?? "");
1162
+ if (sessionId) {
1163
+ this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
1164
+ this.sessionManager.setSession(sessionId, session);
1165
+ }
1166
+ }
983
1167
  } catch (e) {
984
1168
  const err = this.emitError(e, "CALL_FAILED", "call failed");
985
1169
  this.cleanupAllSessions();
@@ -1069,14 +1253,13 @@ var SipClient = class extends EventTargetEmitter {
1069
1253
  cleanupSession(sessionId, session) {
1070
1254
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1071
1255
  this.detachSessionHandlers(sessionId, targetSession);
1072
- this.disableMicrophoneRecovery(sessionId);
1256
+ this.micRecovery.disable(sessionId);
1073
1257
  this.sessionManager.cleanupSession(sessionId);
1074
1258
  removeSessionState(this.stateStore, sessionId);
1075
1259
  }
1076
1260
  cleanupAllSessions() {
1077
1261
  this.sessionManager.cleanupAllSessions();
1078
- this.micRecovery.forEach((entry) => entry.stop());
1079
- this.micRecovery.clear();
1262
+ this.micRecovery.cleanupAll();
1080
1263
  this.sessionHandlers.clear();
1081
1264
  this.stateStore.setState({
1082
1265
  sessions: [],
@@ -1090,12 +1273,9 @@ var SipClient = class extends EventTargetEmitter {
1090
1273
  state: this.stateStore,
1091
1274
  rtc,
1092
1275
  detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1276
+ emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1093
1277
  onSessionFailed: (err, event) => this.onSessionFailed(err, event),
1094
- onSessionConfirmed: (confirmedSessionId) => {
1095
- if (this.micRecoveryEnabled) {
1096
- this.enableMicrophoneRecovery(confirmedSessionId);
1097
- }
1098
- },
1278
+ enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
1099
1279
  sessionId
1100
1280
  });
1101
1281
  }
@@ -1203,102 +1383,6 @@ var SipClient = class extends EventTargetEmitter {
1203
1383
  setSessionMedia(sessionId, stream) {
1204
1384
  this.sessionManager.setSessionMedia(sessionId, stream);
1205
1385
  }
1206
- enableMicrophoneRecovery(sessionId, options = {}) {
1207
- const resolved = this.resolveExistingSessionId(sessionId);
1208
- if (!resolved)
1209
- return () => {
1210
- };
1211
- this.disableMicrophoneRecovery(resolved);
1212
- const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
1213
- const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
1214
- let retries = 0;
1215
- let stopped = false;
1216
- const startedAt = Date.now();
1217
- const warmupMs = Math.max(intervalMs * 2, 2e3);
1218
- const tick = async () => {
1219
- if (stopped || retries >= maxRetries)
1220
- return;
1221
- const rtc = this.sessionManager.getRtc(resolved);
1222
- const session2 = this.sessionManager.getSession(resolved);
1223
- if (!rtc || !session2)
1224
- return;
1225
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1226
- if (sessionState?.muted)
1227
- return;
1228
- const stream = rtc.mediaStream;
1229
- const track = stream?.getAudioTracks?.()[0];
1230
- const pc2 = session2?.connection;
1231
- const sender = pc2?.getSenders?.().find((s) => s.track?.kind === "audio");
1232
- if (!track && !sender)
1233
- return;
1234
- if (Date.now() - startedAt < warmupMs)
1235
- return;
1236
- if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
1237
- return;
1238
- }
1239
- const trackLive = track?.readyState === "live";
1240
- const senderLive = sender?.track?.readyState === "live";
1241
- if (trackLive && senderLive)
1242
- return;
1243
- this.emitError(
1244
- {
1245
- cause: "microphone dropped",
1246
- trackLive,
1247
- senderLive
1248
- },
1249
- "MICROPHONE_DROPPED",
1250
- "microphone dropped"
1251
- );
1252
- retries += 1;
1253
- if (trackLive && !senderLive && track) {
1254
- await rtc.replaceAudioTrack(track);
1255
- return;
1256
- }
1257
- let nextStream;
1258
- try {
1259
- const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1260
- nextStream = await this.requestMicrophoneStreamInternal(deviceId);
1261
- } catch (err) {
1262
- console.warn("[sip] mic recovery failed to get stream", err);
1263
- return;
1264
- }
1265
- const nextTrack = nextStream.getAudioTracks()[0];
1266
- if (!nextTrack)
1267
- return;
1268
- await rtc.replaceAudioTrack(nextTrack);
1269
- this.sessionManager.setSessionMedia(resolved, nextStream);
1270
- };
1271
- const timer = setInterval(() => {
1272
- void tick();
1273
- }, intervalMs);
1274
- void tick();
1275
- const session = this.sessionManager.getSession(resolved);
1276
- const pc = session?.connection;
1277
- const onIceChange = () => {
1278
- const state = pc?.iceConnectionState;
1279
- if (state === "failed" || state === "disconnected")
1280
- void tick();
1281
- };
1282
- pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1283
- const stop = () => {
1284
- stopped = true;
1285
- clearInterval(timer);
1286
- pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1287
- };
1288
- this.micRecovery.set(resolved, { stop });
1289
- return stop;
1290
- }
1291
- disableMicrophoneRecovery(sessionId) {
1292
- const resolved = this.resolveExistingSessionId(sessionId);
1293
- if (!resolved)
1294
- return false;
1295
- const entry = this.micRecovery.get(resolved);
1296
- if (!entry)
1297
- return false;
1298
- entry.stop();
1299
- this.micRecovery.delete(resolved);
1300
- return true;
1301
- }
1302
1386
  switchCameraSession(sessionId, track) {
1303
1387
  if (!this.sessionExists(sessionId))
1304
1388
  return false;
@@ -1464,8 +1548,6 @@ function useSipActions() {
1464
1548
  getSessionIds: () => client.getSessionIds(),
1465
1549
  getSessions: () => client.getSessions(),
1466
1550
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1467
- enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1468
- disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1469
1551
  switchCamera: (...args) => client.switchCameraSession(...args),
1470
1552
  enableVideo: (...args) => client.enableVideoSession(...args),
1471
1553
  disableVideo: (...args) => client.disableVideoSession(...args)
@@ -1539,6 +1621,7 @@ function createCallPlayer(audioEl) {
1539
1621
  return () => session.off("peerconnection", onPeer);
1540
1622
  };
1541
1623
  function bindToSession(session) {
1624
+ clearAudioStream(audioEl.srcObject);
1542
1625
  if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
1543
1626
  cleanupTrackListener = dispose(cleanupTrackListener);
1544
1627
  cleanupTrackListener = attachTracks(session.connection);
@@ -1555,6 +1638,7 @@ function createCallPlayer(audioEl) {
1555
1638
  const e = payload?.data;
1556
1639
  cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
1557
1640
  cleanupTrackListener = dispose(cleanupTrackListener);
1641
+ clearAudioStream(audioEl.srcObject);
1558
1642
  if (!e?.session)
1559
1643
  return;
1560
1644
  cleanupSessionPeerListener = listenSessionPeerconnection(e.session);