react-jssip-kit 0.7.7 → 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
@@ -832,6 +832,165 @@ var SessionManager = class {
832
832
  }
833
833
  };
834
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
+
835
994
  // src/jssip-lib/sip/sessionLifecycle.ts
836
995
  var SessionLifecycle = class {
837
996
  constructor(deps) {
@@ -842,6 +1001,9 @@ var SessionLifecycle = class {
842
1001
  this.attachSessionHandlers = deps.attachSessionHandlers;
843
1002
  this.getMaxSessionCount = deps.getMaxSessionCount;
844
1003
  }
1004
+ setDebugEnabled(enabled) {
1005
+ sipDebugLogger.setEnabled(enabled);
1006
+ }
845
1007
  handleNewRTCSession(e) {
846
1008
  const session = e.session;
847
1009
  const sessionId = String(
@@ -870,71 +1032,12 @@ var SessionLifecycle = class {
870
1032
  const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
871
1033
  this.sessionManager.setSession(sessionId, session);
872
1034
  this.attachSessionHandlers(sessionId, session);
1035
+ this.attachCallStatsLogging(sessionId, session);
873
1036
  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);
935
- }
936
- session.on?.("ended", stopRetry);
937
- session.on?.("failed", stopRetry);
1037
+ this.bindLocalOutgoingAudio(sessionId, session);
1038
+ }
1039
+ if (e.originator === "remote") {
1040
+ this.bindRemoteIncomingAudio(sessionId, session);
938
1041
  }
939
1042
  holdOtherSessions(this.state, sessionId, (id) => {
940
1043
  const otherRtc = this.sessionManager.getRtc(id);
@@ -951,6 +1054,414 @@ var SessionLifecycle = class {
951
1054
  });
952
1055
  this.emit("newRTCSession", e);
953
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
+ }
954
1465
  };
955
1466
 
956
1467
  // src/jssip-lib/sip/micRecovery.ts
@@ -1011,15 +1522,11 @@ var MicRecoveryManager = class {
1011
1522
  const senderLive = sender?.track?.readyState === "live";
1012
1523
  if (trackLive && senderLive)
1013
1524
  return;
1014
- this.deps.emitError(
1015
- {
1016
- cause: "microphone dropped",
1017
- trackLive,
1018
- senderLive
1019
- },
1020
- "MICROPHONE_DROPPED",
1021
- "microphone dropped"
1022
- );
1525
+ sipDebugLogger.logMicRecoveryDrop({
1526
+ sessionId,
1527
+ trackLive,
1528
+ senderLive
1529
+ });
1023
1530
  retries += 1;
1024
1531
  if (trackLive && !senderLive && track) {
1025
1532
  await rtc.replaceAudioTrack(track);
@@ -1138,6 +1645,7 @@ var SipClient = class extends EventTargetEmitter {
1138
1645
  });
1139
1646
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
1140
1647
  this.userAgent.start(uri, password, uaCfg, { debug });
1648
+ this.lifecycle.setDebugEnabled(Boolean(debug));
1141
1649
  this.attachUAHandlers();
1142
1650
  this.attachBeforeUnload();
1143
1651
  this.syncDebugInspector(debug);
@@ -1218,6 +1726,7 @@ var SipClient = class extends EventTargetEmitter {
1218
1726
  setDebug(debug) {
1219
1727
  this.debugPattern = debug;
1220
1728
  this.userAgent.setDebug(debug);
1729
+ this.lifecycle.setDebugEnabled(Boolean(debug));
1221
1730
  this.syncDebugInspector(debug);
1222
1731
  }
1223
1732
  attachSessionHandlers(sessionId, session) {
@@ -1431,14 +1940,17 @@ var SipClient = class extends EventTargetEmitter {
1431
1940
  syncDebugInspector(debug) {
1432
1941
  if (typeof window === "undefined")
1433
1942
  return;
1434
- 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));
1435
1947
  const win = window;
1436
1948
  const disabledInspector = () => {
1437
1949
  console.warn("SIP debug inspector disabled; enable debug to inspect.");
1438
1950
  return null;
1439
1951
  };
1440
- win.sipState = () => debug ? this.stateStore.getState() : disabledInspector();
1441
- win.sipSessions = () => debug ? this.getSessions() : disabledInspector();
1952
+ win.sipState = () => effectiveDebug ? this.stateStore.getState() : disabledInspector();
1953
+ win.sipSessions = () => effectiveDebug ? this.getSessions() : disabledInspector();
1442
1954
  }
1443
1955
  toggleStateLogger(enabled) {
1444
1956
  if (!enabled) {
@@ -1451,22 +1963,10 @@ var SipClient = class extends EventTargetEmitter {
1451
1963
  let prev = this.stateStore.getState();
1452
1964
  console.info("[sip][state]", { initial: true }, prev);
1453
1965
  this.stateLogOff = this.stateStore.onChange((next) => {
1454
- const changes = this.diffState(prev, next);
1455
- if (changes) {
1456
- console.info("[sip][state]", changes, next);
1457
- }
1966
+ console.info("[sip][state]", next);
1458
1967
  prev = next;
1459
1968
  });
1460
1969
  }
1461
- diffState(prev, next) {
1462
- const changed = {};
1463
- for (const key of Object.keys(next)) {
1464
- if (prev[key] !== next[key]) {
1465
- changed[key] = { from: prev[key], to: next[key] };
1466
- }
1467
- }
1468
- return Object.keys(changed).length ? changed : null;
1469
- }
1470
1970
  getPersistedDebug() {
1471
1971
  if (typeof window === "undefined")
1472
1972
  return void 0;