react-jssip-kit 0.7.5 → 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 {
@@ -485,7 +484,6 @@ function createSessionHandlers(deps) {
485
484
  },
486
485
  accepted: (e) => {
487
486
  emitter.emit("accepted", e);
488
- onSessionAccepted?.(sessionId);
489
487
  state.batchSet({
490
488
  sessions: state.getState().sessions.map(
491
489
  (s) => s.id === sessionId ? {
@@ -498,7 +496,7 @@ function createSessionHandlers(deps) {
498
496
  },
499
497
  confirmed: (e) => {
500
498
  emitter.emit("confirmed", e);
501
- onSessionConfirmed?.(sessionId);
499
+ deps.enableMicrophoneRecovery?.(sessionId);
502
500
  },
503
501
  ended: (e) => {
504
502
  emitter.emit("ended", e);
@@ -610,8 +608,8 @@ var WebRTCSessionController = class {
610
608
  }
611
609
  cleanup(stopTracks = true) {
612
610
  const pc = this.getPC();
611
+ const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
613
612
  if (pc && typeof pc.getSenders === "function") {
614
- const isClosed = pc.connectionState === "closed" || pc.signalingState === "closed";
615
613
  if (!isClosed) {
616
614
  for (const s of pc.getSenders()) {
617
615
  try {
@@ -622,8 +620,14 @@ var WebRTCSessionController = class {
622
620
  }
623
621
  }
624
622
  if (stopTracks && this.mediaStream) {
625
- 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;
626
629
  t.stop();
630
+ }
627
631
  }
628
632
  this.mediaStream = null;
629
633
  this.currentSession = null;
@@ -702,29 +706,14 @@ var WebRTCSessionController = class {
702
706
  var SessionManager = class {
703
707
  constructor() {
704
708
  this.entries = /* @__PURE__ */ new Map();
705
- this.pendingMediaQueue = [];
706
- this.pendingMediaTtlMs = 3e4;
707
- }
708
- setPendingMediaTtl(ms) {
709
- if (typeof ms === "number" && ms > 0)
710
- this.pendingMediaTtlMs = ms;
711
- }
712
- enqueueOutgoingMedia(stream) {
713
- this.pendingMediaQueue.push({ stream, addedAt: Date.now() });
714
- }
715
- dequeueOutgoingMedia() {
716
- const now = Date.now();
717
- while (this.pendingMediaQueue.length) {
718
- const next = this.pendingMediaQueue.shift();
719
- if (!next)
720
- break;
721
- if (now - next.addedAt <= this.pendingMediaTtlMs) {
722
- return next.stream;
723
- } else {
724
- next.stream.getTracks().forEach((t) => t.stop());
725
- }
709
+ }
710
+ stopMediaStream(stream) {
711
+ if (!stream)
712
+ return;
713
+ for (const t of stream.getTracks()) {
714
+ if (t.readyState !== "ended")
715
+ t.stop();
726
716
  }
727
- return null;
728
717
  }
729
718
  getOrCreateRtc(sessionId, session) {
730
719
  let entry = this.entries.get(sessionId);
@@ -766,6 +755,9 @@ var SessionManager = class {
766
755
  session: null,
767
756
  media: null
768
757
  };
758
+ if (entry.media && entry.media !== stream) {
759
+ this.stopMediaStream(entry.media);
760
+ }
769
761
  entry.media = stream;
770
762
  entry.rtc.setMediaStream(stream);
771
763
  this.entries.set(sessionId, entry);
@@ -795,15 +787,16 @@ var SessionManager = class {
795
787
  const entry = this.entries.get(sessionId);
796
788
  if (entry) {
797
789
  entry.rtc.cleanup();
790
+ this.stopMediaStream(entry.media);
798
791
  this.entries.delete(sessionId);
799
792
  }
800
793
  }
801
794
  cleanupAllSessions() {
802
795
  for (const [, entry] of this.entries.entries()) {
803
796
  entry.rtc.cleanup();
797
+ this.stopMediaStream(entry.media);
804
798
  }
805
799
  this.entries.clear();
806
- this.pendingMediaQueue = [];
807
800
  }
808
801
  answer(sessionId, options) {
809
802
  const rtc = this.getRtc(sessionId);
@@ -851,35 +844,102 @@ var SessionLifecycle = class {
851
844
  }
852
845
  handleNewRTCSession(e) {
853
846
  const session = e.session;
854
- const sessionId = String(session?.id ?? crypto.randomUUID?.() ?? Date.now());
847
+ const sessionId = String(
848
+ session?.id ?? crypto.randomUUID?.() ?? Date.now()
849
+ );
855
850
  const currentSessions = this.state.getState().sessions;
856
851
  if (currentSessions.length >= this.getMaxSessionCount()) {
857
852
  try {
858
- session.terminate?.({ status_code: 486, reason_phrase: "Busy Here" });
853
+ session.terminate?.({
854
+ status_code: 486,
855
+ reason_phrase: "Busy Here"
856
+ });
859
857
  } catch {
860
858
  }
861
859
  if (e.originator === "remote") {
862
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
+ );
863
867
  }
864
- this.emitError("max session count reached", "MAX_SESSIONS_REACHED", "max session count reached");
865
868
  return;
866
869
  }
867
- const outgoingMedia = e.originator === "local" ? this.sessionManager.dequeueOutgoingMedia() : null;
868
- if (outgoingMedia)
869
- this.sessionManager.setSessionMedia(sessionId, outgoingMedia);
870
870
  const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
871
- if (outgoingMedia)
872
- rtc.setMediaStream(outgoingMedia);
873
871
  this.sessionManager.setSession(sessionId, session);
874
872
  this.attachSessionHandlers(sessionId, session);
875
- holdOtherSessions(
876
- this.state,
877
- sessionId,
878
- (id) => {
879
- const otherRtc = this.sessionManager.getRtc(id);
880
- 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);
881
935
  }
882
- );
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
+ });
883
943
  const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
884
944
  upsertSessionState(this.state, sessionId, {
885
945
  direction: e.originator,
@@ -893,6 +953,126 @@ var SessionLifecycle = class {
893
953
  }
894
954
  };
895
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
+
896
1076
  // src/jssip-lib/sip/client.ts
897
1077
  var SESSION_DEBUG_KEY = "sip-debug-enabled";
898
1078
  var SipClient = class extends EventTargetEmitter {
@@ -903,12 +1083,6 @@ var SipClient = class extends EventTargetEmitter {
903
1083
  this.sessionHandlers = /* @__PURE__ */ new Map();
904
1084
  this.maxSessionCount = Infinity;
905
1085
  this.sessionManager = new SessionManager();
906
- this.micRecovery = /* @__PURE__ */ new Map();
907
- this.micRecoveryEnabled = false;
908
- this.micRecoveryDefaults = {
909
- intervalMs: 2e3,
910
- maxRetries: Infinity
911
- };
912
1086
  this.errorHandler = options.errorHandler ?? new SipErrorHandler({
913
1087
  formatter: options.formatError,
914
1088
  messages: options.errorMessages
@@ -930,6 +1104,14 @@ var SipClient = class extends EventTargetEmitter {
930
1104
  attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
931
1105
  getMaxSessionCount: () => this.maxSessionCount
932
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
+ });
933
1115
  if (typeof window !== "undefined") {
934
1116
  window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
935
1117
  }
@@ -946,18 +1128,14 @@ var SipClient = class extends EventTargetEmitter {
946
1128
  micRecoveryIntervalMs,
947
1129
  micRecoveryMaxRetries,
948
1130
  maxSessionCount,
949
- pendingMediaTtlMs,
950
1131
  ...uaCfg
951
1132
  } = config;
952
1133
  this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
953
- this.micRecoveryEnabled = Boolean(enableMicRecovery);
954
- if (typeof micRecoveryIntervalMs === "number") {
955
- this.micRecoveryDefaults.intervalMs = micRecoveryIntervalMs;
956
- }
957
- if (typeof micRecoveryMaxRetries === "number") {
958
- this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
959
- }
960
- this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
1134
+ this.micRecovery.configure({
1135
+ enabled: Boolean(enableMicRecovery),
1136
+ intervalMs: micRecoveryIntervalMs,
1137
+ maxRetries: micRecoveryMaxRetries
1138
+ });
961
1139
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
962
1140
  this.userAgent.start(uri, password, uaCfg, { debug });
963
1141
  this.attachUAHandlers();
@@ -977,10 +1155,15 @@ var SipClient = class extends EventTargetEmitter {
977
1155
  call(target, callOptions = {}) {
978
1156
  try {
979
1157
  const opts = this.ensureMediaConstraints(callOptions);
980
- if (opts.mediaStream)
981
- this.sessionManager.enqueueOutgoingMedia(opts.mediaStream);
982
1158
  const ua = this.userAgent.getUA();
983
- 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
+ }
984
1167
  } catch (e) {
985
1168
  const err = this.emitError(e, "CALL_FAILED", "call failed");
986
1169
  this.cleanupAllSessions();
@@ -1070,14 +1253,13 @@ var SipClient = class extends EventTargetEmitter {
1070
1253
  cleanupSession(sessionId, session) {
1071
1254
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1072
1255
  this.detachSessionHandlers(sessionId, targetSession);
1073
- this.disableMicrophoneRecovery(sessionId);
1256
+ this.micRecovery.disable(sessionId);
1074
1257
  this.sessionManager.cleanupSession(sessionId);
1075
1258
  removeSessionState(this.stateStore, sessionId);
1076
1259
  }
1077
1260
  cleanupAllSessions() {
1078
1261
  this.sessionManager.cleanupAllSessions();
1079
- this.micRecovery.forEach((entry) => entry.stop());
1080
- this.micRecovery.clear();
1262
+ this.micRecovery.cleanupAll();
1081
1263
  this.sessionHandlers.clear();
1082
1264
  this.stateStore.setState({
1083
1265
  sessions: [],
@@ -1091,7 +1273,9 @@ var SipClient = class extends EventTargetEmitter {
1091
1273
  state: this.stateStore,
1092
1274
  rtc,
1093
1275
  detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1276
+ emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1094
1277
  onSessionFailed: (err, event) => this.onSessionFailed(err, event),
1278
+ enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
1095
1279
  sessionId
1096
1280
  });
1097
1281
  }
@@ -1199,102 +1383,6 @@ var SipClient = class extends EventTargetEmitter {
1199
1383
  setSessionMedia(sessionId, stream) {
1200
1384
  this.sessionManager.setSessionMedia(sessionId, stream);
1201
1385
  }
1202
- enableMicrophoneRecovery(sessionId, options = {}) {
1203
- const resolved = this.resolveExistingSessionId(sessionId);
1204
- if (!resolved)
1205
- return () => {
1206
- };
1207
- this.disableMicrophoneRecovery(resolved);
1208
- const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
1209
- const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
1210
- let retries = 0;
1211
- let stopped = false;
1212
- const startedAt = Date.now();
1213
- const warmupMs = Math.max(intervalMs * 2, 2e3);
1214
- const tick = async () => {
1215
- if (stopped || retries >= maxRetries)
1216
- return;
1217
- const rtc = this.sessionManager.getRtc(resolved);
1218
- const session2 = this.sessionManager.getSession(resolved);
1219
- if (!rtc || !session2)
1220
- return;
1221
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1222
- if (sessionState?.muted)
1223
- return;
1224
- const stream = rtc.mediaStream;
1225
- const track = stream?.getAudioTracks?.()[0];
1226
- const pc2 = session2?.connection;
1227
- const sender = pc2?.getSenders?.().find((s) => s.track?.kind === "audio");
1228
- if (!track && !sender)
1229
- return;
1230
- if (Date.now() - startedAt < warmupMs)
1231
- return;
1232
- if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
1233
- return;
1234
- }
1235
- const trackLive = track?.readyState === "live";
1236
- const senderLive = sender?.track?.readyState === "live";
1237
- if (trackLive && senderLive)
1238
- return;
1239
- this.emitError(
1240
- {
1241
- cause: "microphone dropped",
1242
- trackLive,
1243
- senderLive
1244
- },
1245
- "MICROPHONE_DROPPED",
1246
- "microphone dropped"
1247
- );
1248
- retries += 1;
1249
- if (trackLive && !senderLive && track) {
1250
- await rtc.replaceAudioTrack(track);
1251
- return;
1252
- }
1253
- let nextStream;
1254
- try {
1255
- const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1256
- nextStream = await this.requestMicrophoneStreamInternal(deviceId);
1257
- } catch (err) {
1258
- console.warn("[sip] mic recovery failed to get stream", err);
1259
- return;
1260
- }
1261
- const nextTrack = nextStream.getAudioTracks()[0];
1262
- if (!nextTrack)
1263
- return;
1264
- await rtc.replaceAudioTrack(nextTrack);
1265
- this.sessionManager.setSessionMedia(resolved, nextStream);
1266
- };
1267
- const timer = setInterval(() => {
1268
- void tick();
1269
- }, intervalMs);
1270
- void tick();
1271
- const session = this.sessionManager.getSession(resolved);
1272
- const pc = session?.connection;
1273
- const onIceChange = () => {
1274
- const state = pc?.iceConnectionState;
1275
- if (state === "failed" || state === "disconnected")
1276
- void tick();
1277
- };
1278
- pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1279
- const stop = () => {
1280
- stopped = true;
1281
- clearInterval(timer);
1282
- pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1283
- };
1284
- this.micRecovery.set(resolved, { stop });
1285
- return stop;
1286
- }
1287
- disableMicrophoneRecovery(sessionId) {
1288
- const resolved = this.resolveExistingSessionId(sessionId);
1289
- if (!resolved)
1290
- return false;
1291
- const entry = this.micRecovery.get(resolved);
1292
- if (!entry)
1293
- return false;
1294
- entry.stop();
1295
- this.micRecovery.delete(resolved);
1296
- return true;
1297
- }
1298
1386
  switchCameraSession(sessionId, track) {
1299
1387
  if (!this.sessionExists(sessionId))
1300
1388
  return false;
@@ -1460,8 +1548,6 @@ function useSipActions() {
1460
1548
  getSessionIds: () => client.getSessionIds(),
1461
1549
  getSessions: () => client.getSessions(),
1462
1550
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1463
- enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1464
- disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1465
1551
  switchCamera: (...args) => client.switchCameraSession(...args),
1466
1552
  enableVideo: (...args) => client.enableVideoSession(...args),
1467
1553
  disableVideo: (...args) => client.disableVideoSession(...args)
@@ -1535,6 +1621,7 @@ function createCallPlayer(audioEl) {
1535
1621
  return () => session.off("peerconnection", onPeer);
1536
1622
  };
1537
1623
  function bindToSession(session) {
1624
+ clearAudioStream(audioEl.srcObject);
1538
1625
  if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
1539
1626
  cleanupTrackListener = dispose(cleanupTrackListener);
1540
1627
  cleanupTrackListener = attachTracks(session.connection);
@@ -1551,6 +1638,7 @@ function createCallPlayer(audioEl) {
1551
1638
  const e = payload?.data;
1552
1639
  cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
1553
1640
  cleanupTrackListener = dispose(cleanupTrackListener);
1641
+ clearAudioStream(audioEl.srcObject);
1554
1642
  if (!e?.session)
1555
1643
  return;
1556
1644
  cleanupSessionPeerListener = listenSessionPeerconnection(e.session);