react-jssip-kit 0.7.6 → 0.7.8

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.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);
@@ -838,6 +832,165 @@ var SessionManager = class {
838
832
  }
839
833
  };
840
834
 
835
+ // src/jssip-lib/sip/debugLogging.ts
836
+ var describePc = (pc) => ({
837
+ connectionState: pc?.connectionState,
838
+ signalingState: pc?.signalingState,
839
+ iceConnectionState: pc?.iceConnectionState
840
+ });
841
+ var SipDebugLogger = class {
842
+ constructor() {
843
+ this.enabled = false;
844
+ this.statsStops = /* @__PURE__ */ new Map();
845
+ }
846
+ setEnabled(enabled) {
847
+ this.enabled = enabled;
848
+ if (!enabled) {
849
+ this.statsStops.forEach((stop) => stop());
850
+ this.statsStops.clear();
851
+ }
852
+ }
853
+ isEnabled() {
854
+ return this.enabled;
855
+ }
856
+ logLocalAudioError(sessionId, message, pc, extra) {
857
+ if (!this.enabled)
858
+ return;
859
+ console.error(message, {
860
+ sessionId,
861
+ pc: describePc(pc),
862
+ ...extra
863
+ });
864
+ void this.logOutboundStats(sessionId, pc, message);
865
+ }
866
+ logRemoteAudioError(sessionId, message, pc, extra) {
867
+ if (!this.enabled)
868
+ return;
869
+ console.error(message, {
870
+ sessionId,
871
+ pc: describePc(pc),
872
+ ...extra
873
+ });
874
+ void this.logInboundStats(sessionId, pc, message);
875
+ }
876
+ logMicRecoveryDrop(payload) {
877
+ if (!this.enabled)
878
+ return;
879
+ console.error("[sip] microphone dropped", payload);
880
+ }
881
+ startCallStatsLogging(sessionId, session) {
882
+ if (!this.enabled || this.statsStops.has(sessionId))
883
+ return;
884
+ let pc = session?.connection ?? null;
885
+ const onPeer = (data) => {
886
+ pc = data.peerconnection;
887
+ };
888
+ session.on?.("peerconnection", onPeer);
889
+ const intervalMs = 3e3;
890
+ const logStats = async () => {
891
+ if (!this.enabled || !pc?.getStats)
892
+ return;
893
+ try {
894
+ const report = await pc.getStats();
895
+ const { outboundAudio, inboundAudio } = collectAudioStats(report);
896
+ console.info("[sip] call stats", {
897
+ sessionId,
898
+ pc: describePc(pc),
899
+ outboundAudio,
900
+ inboundAudio
901
+ });
902
+ } catch (err) {
903
+ console.error("[sip] call stats failed", { sessionId, error: err });
904
+ }
905
+ };
906
+ const timer = setInterval(() => {
907
+ void logStats();
908
+ }, intervalMs);
909
+ void logStats();
910
+ const stop = () => {
911
+ clearInterval(timer);
912
+ session.off?.("peerconnection", onPeer);
913
+ this.statsStops.delete(sessionId);
914
+ };
915
+ this.statsStops.set(sessionId, stop);
916
+ }
917
+ stopCallStatsLogging(sessionId) {
918
+ const stop = this.statsStops.get(sessionId);
919
+ if (stop)
920
+ stop();
921
+ }
922
+ async logOutboundStats(sessionId, pc, context) {
923
+ if (!pc?.getStats)
924
+ return;
925
+ try {
926
+ const report = await pc.getStats();
927
+ const { outboundAudio } = collectAudioStats(report);
928
+ if (outboundAudio.length) {
929
+ console.info("[sip] outgoing audio stats", {
930
+ sessionId,
931
+ context,
932
+ outboundAudio
933
+ });
934
+ }
935
+ } catch (err) {
936
+ console.error("[sip] outgoing audio stats failed", {
937
+ sessionId,
938
+ context,
939
+ error: err
940
+ });
941
+ }
942
+ }
943
+ async logInboundStats(sessionId, pc, context) {
944
+ if (!pc?.getStats)
945
+ return;
946
+ try {
947
+ const report = await pc.getStats();
948
+ const { inboundAudio } = collectAudioStats(report);
949
+ if (inboundAudio.length) {
950
+ console.error("[sip] incoming audio stats", {
951
+ sessionId,
952
+ context,
953
+ inboundAudio
954
+ });
955
+ }
956
+ } catch (err) {
957
+ console.error("[sip] incoming audio stats failed", {
958
+ sessionId,
959
+ context,
960
+ error: err
961
+ });
962
+ }
963
+ }
964
+ };
965
+ var sipDebugLogger = new SipDebugLogger();
966
+ function collectAudioStats(report) {
967
+ const outboundAudio = [];
968
+ const inboundAudio = [];
969
+ report.forEach((stat) => {
970
+ const kind = stat.kind ?? stat.mediaType;
971
+ if (stat.type === "outbound-rtp" && kind === "audio") {
972
+ outboundAudio.push({
973
+ id: stat.id,
974
+ packetsSent: stat.packetsSent,
975
+ bytesSent: stat.bytesSent,
976
+ jitter: stat.jitter,
977
+ roundTripTime: stat.roundTripTime
978
+ });
979
+ }
980
+ if (stat.type === "inbound-rtp" && kind === "audio") {
981
+ inboundAudio.push({
982
+ id: stat.id,
983
+ packetsReceived: stat.packetsReceived,
984
+ packetsLost: stat.packetsLost,
985
+ bytesReceived: stat.bytesReceived,
986
+ jitter: stat.jitter,
987
+ roundTripTime: stat.roundTripTime
988
+ });
989
+ }
990
+ });
991
+ return { outboundAudio, inboundAudio };
992
+ }
993
+
841
994
  // src/jssip-lib/sip/sessionLifecycle.ts
842
995
  var SessionLifecycle = class {
843
996
  constructor(deps) {
@@ -848,37 +1001,48 @@ var SessionLifecycle = class {
848
1001
  this.attachSessionHandlers = deps.attachSessionHandlers;
849
1002
  this.getMaxSessionCount = deps.getMaxSessionCount;
850
1003
  }
1004
+ setDebugEnabled(enabled) {
1005
+ sipDebugLogger.setEnabled(enabled);
1006
+ }
851
1007
  handleNewRTCSession(e) {
852
1008
  const session = e.session;
853
- const sessionId = String(session?.id ?? crypto.randomUUID?.() ?? Date.now());
1009
+ const sessionId = String(
1010
+ session?.id ?? crypto.randomUUID?.() ?? Date.now()
1011
+ );
854
1012
  const currentSessions = this.state.getState().sessions;
855
1013
  if (currentSessions.length >= this.getMaxSessionCount()) {
856
1014
  try {
857
- session.terminate?.({ status_code: 486, reason_phrase: "Busy Here" });
1015
+ session.terminate?.({
1016
+ status_code: 486,
1017
+ reason_phrase: "Busy Here"
1018
+ });
858
1019
  } catch {
859
1020
  }
860
1021
  if (e.originator === "remote") {
861
1022
  this.emit("missed", e);
1023
+ } else {
1024
+ this.emitError(
1025
+ "max session count reached",
1026
+ "MAX_SESSIONS_REACHED",
1027
+ "max session count reached"
1028
+ );
862
1029
  }
863
- this.emitError("max session count reached", "MAX_SESSIONS_REACHED", "max session count reached");
864
1030
  return;
865
1031
  }
866
- const outgoingMedia = e.originator === "local" ? this.sessionManager.dequeueOutgoingMedia() : null;
867
- if (outgoingMedia)
868
- this.sessionManager.setSessionMedia(sessionId, outgoingMedia);
869
1032
  const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
870
- if (outgoingMedia)
871
- rtc.setMediaStream(outgoingMedia);
872
1033
  this.sessionManager.setSession(sessionId, session);
873
1034
  this.attachSessionHandlers(sessionId, session);
874
- holdOtherSessions(
875
- this.state,
876
- sessionId,
877
- (id) => {
878
- const otherRtc = this.sessionManager.getRtc(id);
879
- otherRtc?.hold();
880
- }
881
- );
1035
+ this.attachCallStatsLogging(sessionId, session);
1036
+ if (e.originator === "local" && !rtc.mediaStream) {
1037
+ this.bindLocalOutgoingAudio(sessionId, session);
1038
+ }
1039
+ if (e.originator === "remote") {
1040
+ this.bindRemoteIncomingAudio(sessionId, session);
1041
+ }
1042
+ holdOtherSessions(this.state, sessionId, (id) => {
1043
+ const otherRtc = this.sessionManager.getRtc(id);
1044
+ otherRtc?.hold();
1045
+ });
882
1046
  const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
883
1047
  upsertSessionState(this.state, sessionId, {
884
1048
  direction: e.originator,
@@ -890,6 +1054,530 @@ var SessionLifecycle = class {
890
1054
  });
891
1055
  this.emit("newRTCSession", e);
892
1056
  }
1057
+ bindLocalOutgoingAudio(sessionId, session) {
1058
+ const maxAttempts = 50;
1059
+ const retryDelayMs = 500;
1060
+ let attempts = 0;
1061
+ let retryScheduled = false;
1062
+ let retryTimer = null;
1063
+ let stopped = false;
1064
+ let exhausted = false;
1065
+ let exhaustedCheckUsed = false;
1066
+ let attachedPc = null;
1067
+ const logLocalAudioError = (message, pc, extra) => {
1068
+ sipDebugLogger.logLocalAudioError(sessionId, message, pc, extra);
1069
+ };
1070
+ const tryBindFromPc = (pc) => {
1071
+ if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
1072
+ return false;
1073
+ }
1074
+ const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1075
+ const audioTrack = audioSender?.track;
1076
+ if (!audioTrack) {
1077
+ logLocalAudioError(
1078
+ "[sip] outgoing audio bind failed: no audio track",
1079
+ pc
1080
+ );
1081
+ return false;
1082
+ }
1083
+ const outgoingStream = new MediaStream([audioTrack]);
1084
+ this.sessionManager.setSessionMedia(sessionId, outgoingStream);
1085
+ return true;
1086
+ };
1087
+ const onPcStateChange = () => {
1088
+ if (stopped)
1089
+ return;
1090
+ if (exhausted) {
1091
+ if (exhaustedCheckUsed)
1092
+ return;
1093
+ exhaustedCheckUsed = true;
1094
+ if (tryBindFromPc(attachedPc))
1095
+ stopRetry();
1096
+ return;
1097
+ }
1098
+ if (tryBindFromPc(attachedPc))
1099
+ stopRetry();
1100
+ };
1101
+ const attachPcListeners = (pc) => {
1102
+ if (!pc || pc === attachedPc)
1103
+ return;
1104
+ if (attachedPc) {
1105
+ attachedPc.removeEventListener?.(
1106
+ "signalingstatechange",
1107
+ onPcStateChange
1108
+ );
1109
+ attachedPc.removeEventListener?.(
1110
+ "connectionstatechange",
1111
+ onPcStateChange
1112
+ );
1113
+ attachedPc.removeEventListener?.(
1114
+ "iceconnectionstatechange",
1115
+ onPcStateChange
1116
+ );
1117
+ }
1118
+ attachedPc = pc;
1119
+ attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1120
+ attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1121
+ attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1122
+ };
1123
+ const clearRetryTimer = () => {
1124
+ if (!retryTimer)
1125
+ return;
1126
+ clearTimeout(retryTimer);
1127
+ retryTimer = null;
1128
+ };
1129
+ const stopRetry = () => {
1130
+ if (stopped)
1131
+ return;
1132
+ stopped = true;
1133
+ clearRetryTimer();
1134
+ if (attachedPc) {
1135
+ attachedPc.removeEventListener?.(
1136
+ "signalingstatechange",
1137
+ onPcStateChange
1138
+ );
1139
+ attachedPc.removeEventListener?.(
1140
+ "connectionstatechange",
1141
+ onPcStateChange
1142
+ );
1143
+ attachedPc.removeEventListener?.(
1144
+ "iceconnectionstatechange",
1145
+ onPcStateChange
1146
+ );
1147
+ attachedPc = null;
1148
+ }
1149
+ session.off?.("peerconnection", onPeer);
1150
+ session.off?.("confirmed", onConfirmed);
1151
+ session.off?.("ended", stopRetry);
1152
+ session.off?.("failed", stopRetry);
1153
+ };
1154
+ const scheduleRetry = (pc) => {
1155
+ if (stopped || retryScheduled || exhausted)
1156
+ return;
1157
+ if (attempts >= maxAttempts) {
1158
+ logLocalAudioError(
1159
+ "[sip] outgoing audio bind failed: max retries reached",
1160
+ pc,
1161
+ { attempts }
1162
+ );
1163
+ exhausted = true;
1164
+ clearRetryTimer();
1165
+ return;
1166
+ }
1167
+ if (!pc) {
1168
+ logLocalAudioError(
1169
+ "[sip] outgoing audio bind failed: missing peerconnection",
1170
+ pc
1171
+ );
1172
+ }
1173
+ retryScheduled = true;
1174
+ attempts += 1;
1175
+ retryTimer = setTimeout(() => {
1176
+ retryScheduled = false;
1177
+ retryTimer = null;
1178
+ if (tryBindFromPc(pc)) {
1179
+ stopRetry();
1180
+ return;
1181
+ }
1182
+ scheduleRetry(pc);
1183
+ }, retryDelayMs);
1184
+ };
1185
+ const onPeer = (data) => {
1186
+ if (stopped)
1187
+ return;
1188
+ attachPcListeners(data.peerconnection);
1189
+ if (exhausted) {
1190
+ if (exhaustedCheckUsed)
1191
+ return;
1192
+ exhaustedCheckUsed = true;
1193
+ if (tryBindFromPc(data.peerconnection))
1194
+ stopRetry();
1195
+ return;
1196
+ }
1197
+ if (tryBindFromPc(data.peerconnection)) {
1198
+ stopRetry();
1199
+ return;
1200
+ }
1201
+ scheduleRetry(data.peerconnection);
1202
+ };
1203
+ const onConfirmed = () => {
1204
+ if (stopped)
1205
+ return;
1206
+ const currentPc = session?.connection ?? attachedPc;
1207
+ if (exhausted) {
1208
+ if (exhaustedCheckUsed)
1209
+ return;
1210
+ exhaustedCheckUsed = true;
1211
+ if (tryBindFromPc(currentPc))
1212
+ stopRetry();
1213
+ return;
1214
+ }
1215
+ if (tryBindFromPc(currentPc)) {
1216
+ stopRetry();
1217
+ return;
1218
+ }
1219
+ scheduleRetry(currentPc);
1220
+ };
1221
+ const existingPc = session?.connection;
1222
+ if (!tryBindFromPc(existingPc)) {
1223
+ if (existingPc) {
1224
+ attachPcListeners(existingPc);
1225
+ scheduleRetry(existingPc);
1226
+ }
1227
+ session.on?.("peerconnection", onPeer);
1228
+ }
1229
+ session.on?.("confirmed", onConfirmed);
1230
+ session.on?.("ended", stopRetry);
1231
+ session.on?.("failed", stopRetry);
1232
+ }
1233
+ bindRemoteIncomingAudio(sessionId, session) {
1234
+ const maxAttempts = 50;
1235
+ const retryDelayMs = 500;
1236
+ let attempts = 0;
1237
+ let retryScheduled = false;
1238
+ let retryTimer = null;
1239
+ let stopped = false;
1240
+ let exhausted = false;
1241
+ let exhaustedCheckUsed = false;
1242
+ let attachedPc = null;
1243
+ let attachedTrack = null;
1244
+ const logRemoteAudioError = (message, pc, extra) => {
1245
+ sipDebugLogger.logRemoteAudioError(sessionId, message, pc, extra);
1246
+ };
1247
+ const logMissingReceiver = (pc, note) => {
1248
+ logRemoteAudioError(
1249
+ "[sip] incoming audio bind failed: no remote track",
1250
+ pc,
1251
+ { note }
1252
+ );
1253
+ };
1254
+ const getRemoteAudioTrack = (pc) => {
1255
+ const receiver = pc?.getReceivers?.()?.find((r) => r.track?.kind === "audio");
1256
+ return receiver?.track ?? null;
1257
+ };
1258
+ const attachTrackListeners = (track) => {
1259
+ if (!track || track === attachedTrack)
1260
+ return;
1261
+ if (attachedTrack) {
1262
+ attachedTrack.removeEventListener?.("ended", onRemoteEnded);
1263
+ attachedTrack.removeEventListener?.("mute", onRemoteMuted);
1264
+ }
1265
+ attachedTrack = track;
1266
+ attachedTrack.addEventListener?.("ended", onRemoteEnded);
1267
+ attachedTrack.addEventListener?.("mute", onRemoteMuted);
1268
+ };
1269
+ const checkRemoteTrack = (pc) => {
1270
+ if (stopped || !pc)
1271
+ return false;
1272
+ const track = getRemoteAudioTrack(pc);
1273
+ if (!track)
1274
+ return false;
1275
+ attachTrackListeners(track);
1276
+ if (track.readyState !== "live") {
1277
+ logRemoteAudioError("[sip] incoming audio track not live", pc, {
1278
+ trackState: track.readyState
1279
+ });
1280
+ }
1281
+ return true;
1282
+ };
1283
+ const onRemoteEnded = () => {
1284
+ logRemoteAudioError("[sip] incoming audio track ended", attachedPc);
1285
+ };
1286
+ const onRemoteMuted = () => {
1287
+ logRemoteAudioError("[sip] incoming audio track muted", attachedPc);
1288
+ };
1289
+ const onPcStateChange = () => {
1290
+ if (stopped)
1291
+ return;
1292
+ if (exhausted) {
1293
+ if (exhaustedCheckUsed)
1294
+ return;
1295
+ exhaustedCheckUsed = true;
1296
+ if (checkRemoteTrack(attachedPc))
1297
+ stopRetry();
1298
+ return;
1299
+ }
1300
+ if (checkRemoteTrack(attachedPc))
1301
+ stopRetry();
1302
+ };
1303
+ const attachPcListeners = (pc) => {
1304
+ if (!pc || pc === attachedPc)
1305
+ return;
1306
+ if (attachedPc) {
1307
+ attachedPc.removeEventListener?.(
1308
+ "signalingstatechange",
1309
+ onPcStateChange
1310
+ );
1311
+ attachedPc.removeEventListener?.(
1312
+ "connectionstatechange",
1313
+ onPcStateChange
1314
+ );
1315
+ attachedPc.removeEventListener?.(
1316
+ "iceconnectionstatechange",
1317
+ onPcStateChange
1318
+ );
1319
+ attachedPc.removeEventListener?.("track", onTrack);
1320
+ }
1321
+ attachedPc = pc;
1322
+ attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1323
+ attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1324
+ attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1325
+ attachedPc.addEventListener?.("track", onTrack);
1326
+ };
1327
+ const clearRetryTimer = () => {
1328
+ if (!retryTimer)
1329
+ return;
1330
+ clearTimeout(retryTimer);
1331
+ retryTimer = null;
1332
+ };
1333
+ const stopRetry = () => {
1334
+ if (stopped)
1335
+ return;
1336
+ stopped = true;
1337
+ clearRetryTimer();
1338
+ if (attachedPc) {
1339
+ attachedPc.removeEventListener?.(
1340
+ "signalingstatechange",
1341
+ onPcStateChange
1342
+ );
1343
+ attachedPc.removeEventListener?.(
1344
+ "connectionstatechange",
1345
+ onPcStateChange
1346
+ );
1347
+ attachedPc.removeEventListener?.(
1348
+ "iceconnectionstatechange",
1349
+ onPcStateChange
1350
+ );
1351
+ attachedPc.removeEventListener?.("track", onTrack);
1352
+ attachedPc = null;
1353
+ }
1354
+ if (attachedTrack) {
1355
+ attachedTrack.removeEventListener?.("ended", onRemoteEnded);
1356
+ attachedTrack.removeEventListener?.("mute", onRemoteMuted);
1357
+ attachedTrack = null;
1358
+ }
1359
+ session.off?.("peerconnection", onPeer);
1360
+ session.off?.("confirmed", onConfirmed);
1361
+ session.off?.("ended", stopRetry);
1362
+ session.off?.("failed", stopRetry);
1363
+ };
1364
+ const scheduleRetry = (pc) => {
1365
+ if (stopped || retryScheduled || exhausted)
1366
+ return;
1367
+ if (attempts >= maxAttempts) {
1368
+ logRemoteAudioError(
1369
+ "[sip] incoming audio bind failed: max retries reached",
1370
+ pc,
1371
+ { attempts }
1372
+ );
1373
+ exhausted = true;
1374
+ clearRetryTimer();
1375
+ return;
1376
+ }
1377
+ retryScheduled = true;
1378
+ attempts += 1;
1379
+ retryTimer = setTimeout(() => {
1380
+ retryScheduled = false;
1381
+ retryTimer = null;
1382
+ if (checkRemoteTrack(pc)) {
1383
+ stopRetry();
1384
+ return;
1385
+ }
1386
+ if (!pc)
1387
+ logMissingReceiver(pc, "missing peerconnection");
1388
+ scheduleRetry(pc);
1389
+ }, retryDelayMs);
1390
+ };
1391
+ const onTrack = () => {
1392
+ if (stopped)
1393
+ return;
1394
+ if (exhausted) {
1395
+ if (exhaustedCheckUsed)
1396
+ return;
1397
+ exhaustedCheckUsed = true;
1398
+ if (checkRemoteTrack(attachedPc))
1399
+ stopRetry();
1400
+ return;
1401
+ }
1402
+ if (checkRemoteTrack(attachedPc))
1403
+ stopRetry();
1404
+ };
1405
+ const onPeer = (data) => {
1406
+ if (stopped)
1407
+ return;
1408
+ attachPcListeners(data.peerconnection);
1409
+ if (exhausted) {
1410
+ if (exhaustedCheckUsed)
1411
+ return;
1412
+ exhaustedCheckUsed = true;
1413
+ if (checkRemoteTrack(data.peerconnection))
1414
+ stopRetry();
1415
+ return;
1416
+ }
1417
+ if (checkRemoteTrack(data.peerconnection)) {
1418
+ stopRetry();
1419
+ return;
1420
+ }
1421
+ scheduleRetry(data.peerconnection);
1422
+ };
1423
+ const onConfirmed = () => {
1424
+ if (stopped)
1425
+ return;
1426
+ const currentPc = session?.connection ?? attachedPc;
1427
+ if (exhausted) {
1428
+ if (exhaustedCheckUsed)
1429
+ return;
1430
+ exhaustedCheckUsed = true;
1431
+ if (checkRemoteTrack(currentPc))
1432
+ stopRetry();
1433
+ return;
1434
+ }
1435
+ if (checkRemoteTrack(currentPc)) {
1436
+ stopRetry();
1437
+ return;
1438
+ }
1439
+ logMissingReceiver(currentPc, "confirmed without remote track");
1440
+ scheduleRetry(currentPc);
1441
+ };
1442
+ const existingPc = session?.connection;
1443
+ if (!checkRemoteTrack(existingPc)) {
1444
+ if (existingPc) {
1445
+ attachPcListeners(existingPc);
1446
+ scheduleRetry(existingPc);
1447
+ }
1448
+ session.on?.("peerconnection", onPeer);
1449
+ }
1450
+ session.on?.("confirmed", onConfirmed);
1451
+ session.on?.("ended", stopRetry);
1452
+ session.on?.("failed", stopRetry);
1453
+ }
1454
+ attachCallStatsLogging(sessionId, session) {
1455
+ const onConfirmed = () => {
1456
+ sipDebugLogger.startCallStatsLogging(sessionId, session);
1457
+ };
1458
+ const onEnd = () => {
1459
+ sipDebugLogger.stopCallStatsLogging(sessionId);
1460
+ };
1461
+ session.on?.("confirmed", onConfirmed);
1462
+ session.on?.("ended", onEnd);
1463
+ session.on?.("failed", onEnd);
1464
+ }
1465
+ };
1466
+
1467
+ // src/jssip-lib/sip/micRecovery.ts
1468
+ var MicRecoveryManager = class {
1469
+ constructor(deps) {
1470
+ this.enabled = false;
1471
+ this.defaults = {
1472
+ intervalMs: 2e3,
1473
+ maxRetries: Infinity
1474
+ };
1475
+ this.active = /* @__PURE__ */ new Map();
1476
+ this.deps = deps;
1477
+ }
1478
+ configure(config) {
1479
+ if (typeof config.enabled === "boolean") {
1480
+ this.enabled = config.enabled;
1481
+ }
1482
+ if (typeof config.intervalMs === "number") {
1483
+ this.defaults.intervalMs = config.intervalMs;
1484
+ }
1485
+ if (typeof config.maxRetries === "number") {
1486
+ this.defaults.maxRetries = config.maxRetries;
1487
+ }
1488
+ }
1489
+ enable(sessionId, options = {}) {
1490
+ if (!this.enabled)
1491
+ return () => {
1492
+ };
1493
+ this.disable(sessionId);
1494
+ const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
1495
+ const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
1496
+ let retries = 0;
1497
+ let stopped = false;
1498
+ const startedAt = Date.now();
1499
+ const warmupMs = Math.max(intervalMs * 2, 2e3);
1500
+ const tick = async () => {
1501
+ if (stopped || retries >= maxRetries)
1502
+ return;
1503
+ const rtc = this.deps.getRtc(sessionId);
1504
+ const session2 = this.deps.getSession(sessionId);
1505
+ if (!rtc || !session2)
1506
+ return;
1507
+ const sessionState = this.deps.getSessionState(sessionId);
1508
+ if (sessionState?.muted)
1509
+ return;
1510
+ const stream = rtc.mediaStream;
1511
+ const track = stream?.getAudioTracks?.()[0];
1512
+ const pc2 = session2?.connection;
1513
+ const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1514
+ if (!track && !sender)
1515
+ return;
1516
+ if (Date.now() - startedAt < warmupMs)
1517
+ return;
1518
+ if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
1519
+ return;
1520
+ }
1521
+ const trackLive = track?.readyState === "live";
1522
+ const senderLive = sender?.track?.readyState === "live";
1523
+ if (trackLive && senderLive)
1524
+ return;
1525
+ sipDebugLogger.logMicRecoveryDrop({
1526
+ sessionId,
1527
+ trackLive,
1528
+ senderLive
1529
+ });
1530
+ retries += 1;
1531
+ if (trackLive && !senderLive && track) {
1532
+ await rtc.replaceAudioTrack(track);
1533
+ return;
1534
+ }
1535
+ let nextStream;
1536
+ try {
1537
+ const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1538
+ nextStream = await this.deps.requestMicrophoneStream(deviceId);
1539
+ } catch (err) {
1540
+ console.warn("[sip] mic recovery failed to get stream", err);
1541
+ return;
1542
+ }
1543
+ const nextTrack = nextStream.getAudioTracks()[0];
1544
+ if (!nextTrack)
1545
+ return;
1546
+ await rtc.replaceAudioTrack(nextTrack);
1547
+ this.deps.setSessionMedia(sessionId, nextStream);
1548
+ };
1549
+ const timer = setInterval(() => {
1550
+ void tick();
1551
+ }, intervalMs);
1552
+ void tick();
1553
+ const session = this.deps.getSession(sessionId);
1554
+ const pc = session?.connection;
1555
+ const onIceChange = () => {
1556
+ const state = pc?.iceConnectionState;
1557
+ if (state === "failed" || state === "disconnected")
1558
+ void tick();
1559
+ };
1560
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1561
+ const stop = () => {
1562
+ stopped = true;
1563
+ clearInterval(timer);
1564
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1565
+ };
1566
+ this.active.set(sessionId, { stop });
1567
+ return stop;
1568
+ }
1569
+ disable(sessionId) {
1570
+ const entry = this.active.get(sessionId);
1571
+ if (!entry)
1572
+ return false;
1573
+ entry.stop();
1574
+ this.active.delete(sessionId);
1575
+ return true;
1576
+ }
1577
+ cleanupAll() {
1578
+ this.active.forEach((entry) => entry.stop());
1579
+ this.active.clear();
1580
+ }
893
1581
  };
894
1582
 
895
1583
  // src/jssip-lib/sip/client.ts
@@ -902,12 +1590,6 @@ var SipClient = class extends EventTargetEmitter {
902
1590
  this.sessionHandlers = /* @__PURE__ */ new Map();
903
1591
  this.maxSessionCount = Infinity;
904
1592
  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
1593
  this.errorHandler = options.errorHandler ?? new SipErrorHandler({
912
1594
  formatter: options.formatError,
913
1595
  messages: options.errorMessages
@@ -929,6 +1611,14 @@ var SipClient = class extends EventTargetEmitter {
929
1611
  attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
930
1612
  getMaxSessionCount: () => this.maxSessionCount
931
1613
  });
1614
+ this.micRecovery = new MicRecoveryManager({
1615
+ getRtc: (sessionId) => this.sessionManager.getRtc(sessionId),
1616
+ getSession: (sessionId) => this.sessionManager.getSession(sessionId),
1617
+ getSessionState: (sessionId) => this.stateStore.getState().sessions.find((s) => s.id === sessionId),
1618
+ setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream),
1619
+ emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1620
+ requestMicrophoneStream: (deviceId) => this.requestMicrophoneStreamInternal(deviceId)
1621
+ });
932
1622
  if (typeof window !== "undefined") {
933
1623
  window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
934
1624
  }
@@ -945,20 +1635,17 @@ var SipClient = class extends EventTargetEmitter {
945
1635
  micRecoveryIntervalMs,
946
1636
  micRecoveryMaxRetries,
947
1637
  maxSessionCount,
948
- pendingMediaTtlMs,
949
1638
  ...uaCfg
950
1639
  } = config;
951
1640
  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);
1641
+ this.micRecovery.configure({
1642
+ enabled: Boolean(enableMicRecovery),
1643
+ intervalMs: micRecoveryIntervalMs,
1644
+ maxRetries: micRecoveryMaxRetries
1645
+ });
960
1646
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
961
1647
  this.userAgent.start(uri, password, uaCfg, { debug });
1648
+ this.lifecycle.setDebugEnabled(Boolean(debug));
962
1649
  this.attachUAHandlers();
963
1650
  this.attachBeforeUnload();
964
1651
  this.syncDebugInspector(debug);
@@ -976,10 +1663,15 @@ var SipClient = class extends EventTargetEmitter {
976
1663
  call(target, callOptions = {}) {
977
1664
  try {
978
1665
  const opts = this.ensureMediaConstraints(callOptions);
979
- if (opts.mediaStream)
980
- this.sessionManager.enqueueOutgoingMedia(opts.mediaStream);
981
1666
  const ua = this.userAgent.getUA();
982
- ua?.call(target, opts);
1667
+ const session = ua?.call(target, opts);
1668
+ if (session && opts.mediaStream) {
1669
+ const sessionId = String(session?.id ?? "");
1670
+ if (sessionId) {
1671
+ this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
1672
+ this.sessionManager.setSession(sessionId, session);
1673
+ }
1674
+ }
983
1675
  } catch (e) {
984
1676
  const err = this.emitError(e, "CALL_FAILED", "call failed");
985
1677
  this.cleanupAllSessions();
@@ -1034,6 +1726,7 @@ var SipClient = class extends EventTargetEmitter {
1034
1726
  setDebug(debug) {
1035
1727
  this.debugPattern = debug;
1036
1728
  this.userAgent.setDebug(debug);
1729
+ this.lifecycle.setDebugEnabled(Boolean(debug));
1037
1730
  this.syncDebugInspector(debug);
1038
1731
  }
1039
1732
  attachSessionHandlers(sessionId, session) {
@@ -1069,14 +1762,13 @@ var SipClient = class extends EventTargetEmitter {
1069
1762
  cleanupSession(sessionId, session) {
1070
1763
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1071
1764
  this.detachSessionHandlers(sessionId, targetSession);
1072
- this.disableMicrophoneRecovery(sessionId);
1765
+ this.micRecovery.disable(sessionId);
1073
1766
  this.sessionManager.cleanupSession(sessionId);
1074
1767
  removeSessionState(this.stateStore, sessionId);
1075
1768
  }
1076
1769
  cleanupAllSessions() {
1077
1770
  this.sessionManager.cleanupAllSessions();
1078
- this.micRecovery.forEach((entry) => entry.stop());
1079
- this.micRecovery.clear();
1771
+ this.micRecovery.cleanupAll();
1080
1772
  this.sessionHandlers.clear();
1081
1773
  this.stateStore.setState({
1082
1774
  sessions: [],
@@ -1090,12 +1782,9 @@ var SipClient = class extends EventTargetEmitter {
1090
1782
  state: this.stateStore,
1091
1783
  rtc,
1092
1784
  detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1785
+ emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1093
1786
  onSessionFailed: (err, event) => this.onSessionFailed(err, event),
1094
- onSessionConfirmed: (confirmedSessionId) => {
1095
- if (this.micRecoveryEnabled) {
1096
- this.enableMicrophoneRecovery(confirmedSessionId);
1097
- }
1098
- },
1787
+ enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
1099
1788
  sessionId
1100
1789
  });
1101
1790
  }
@@ -1203,102 +1892,6 @@ var SipClient = class extends EventTargetEmitter {
1203
1892
  setSessionMedia(sessionId, stream) {
1204
1893
  this.sessionManager.setSessionMedia(sessionId, stream);
1205
1894
  }
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
1895
  switchCameraSession(sessionId, track) {
1303
1896
  if (!this.sessionExists(sessionId))
1304
1897
  return false;
@@ -1347,14 +1940,17 @@ var SipClient = class extends EventTargetEmitter {
1347
1940
  syncDebugInspector(debug) {
1348
1941
  if (typeof window === "undefined")
1349
1942
  return;
1350
- this.toggleStateLogger(Boolean(debug));
1943
+ const persisted = this.getPersistedDebug();
1944
+ const effectiveDebug = debug ?? persisted ?? this.debugPattern;
1945
+ this.lifecycle.setDebugEnabled(Boolean(effectiveDebug));
1946
+ this.toggleStateLogger(Boolean(effectiveDebug));
1351
1947
  const win = window;
1352
1948
  const disabledInspector = () => {
1353
1949
  console.warn("SIP debug inspector disabled; enable debug to inspect.");
1354
1950
  return null;
1355
1951
  };
1356
- win.sipState = () => debug ? this.stateStore.getState() : disabledInspector();
1357
- win.sipSessions = () => debug ? this.getSessions() : disabledInspector();
1952
+ win.sipState = () => effectiveDebug ? this.stateStore.getState() : disabledInspector();
1953
+ win.sipSessions = () => effectiveDebug ? this.getSessions() : disabledInspector();
1358
1954
  }
1359
1955
  toggleStateLogger(enabled) {
1360
1956
  if (!enabled) {
@@ -1367,22 +1963,10 @@ var SipClient = class extends EventTargetEmitter {
1367
1963
  let prev = this.stateStore.getState();
1368
1964
  console.info("[sip][state]", { initial: true }, prev);
1369
1965
  this.stateLogOff = this.stateStore.onChange((next) => {
1370
- const changes = this.diffState(prev, next);
1371
- if (changes) {
1372
- console.info("[sip][state]", changes, next);
1373
- }
1966
+ console.info("[sip][state]", next);
1374
1967
  prev = next;
1375
1968
  });
1376
1969
  }
1377
- diffState(prev, next) {
1378
- const changed = {};
1379
- for (const key of Object.keys(next)) {
1380
- if (prev[key] !== next[key]) {
1381
- changed[key] = { from: prev[key], to: next[key] };
1382
- }
1383
- }
1384
- return Object.keys(changed).length ? changed : null;
1385
- }
1386
1970
  getPersistedDebug() {
1387
1971
  if (typeof window === "undefined")
1388
1972
  return void 0;
@@ -1464,8 +2048,6 @@ function useSipActions() {
1464
2048
  getSessionIds: () => client.getSessionIds(),
1465
2049
  getSessions: () => client.getSessions(),
1466
2050
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1467
- enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1468
- disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1469
2051
  switchCamera: (...args) => client.switchCameraSession(...args),
1470
2052
  enableVideo: (...args) => client.enableVideoSession(...args),
1471
2053
  disableVideo: (...args) => client.disableVideoSession(...args)
@@ -1539,6 +2121,7 @@ function createCallPlayer(audioEl) {
1539
2121
  return () => session.off("peerconnection", onPeer);
1540
2122
  };
1541
2123
  function bindToSession(session) {
2124
+ clearAudioStream(audioEl.srcObject);
1542
2125
  if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
1543
2126
  cleanupTrackListener = dispose(cleanupTrackListener);
1544
2127
  cleanupTrackListener = attachTracks(session.connection);
@@ -1555,6 +2138,7 @@ function createCallPlayer(audioEl) {
1555
2138
  const e = payload?.data;
1556
2139
  cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
1557
2140
  cleanupTrackListener = dispose(cleanupTrackListener);
2141
+ clearAudioStream(audioEl.srcObject);
1558
2142
  if (!e?.session)
1559
2143
  return;
1560
2144
  cleanupSessionPeerListener = listenSessionPeerconnection(e.session);