react-jssip-kit 0.7.7 → 0.7.9

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.cjs CHANGED
@@ -473,6 +473,175 @@ function removeSessionState(state, sessionId) {
473
473
  });
474
474
  }
475
475
 
476
+ // src/jssip-lib/sip/debugLogging.ts
477
+ var describePc = (pc) => ({
478
+ connectionState: pc?.connectionState,
479
+ signalingState: pc?.signalingState,
480
+ iceConnectionState: pc?.iceConnectionState
481
+ });
482
+ var SipDebugLogger = class {
483
+ constructor() {
484
+ this.enabled = false;
485
+ this.statsStops = /* @__PURE__ */ new Map();
486
+ }
487
+ setEnabled(enabled) {
488
+ this.enabled = enabled;
489
+ if (!enabled) {
490
+ this.statsStops.forEach((stop) => stop());
491
+ this.statsStops.clear();
492
+ }
493
+ }
494
+ isEnabled() {
495
+ return this.enabled;
496
+ }
497
+ logLocalAudioError(sessionId, message, pc, extra) {
498
+ if (!this.enabled)
499
+ return;
500
+ console.error(message, {
501
+ sessionId,
502
+ pc: describePc(pc),
503
+ ...extra
504
+ });
505
+ void this.logOutboundStats(sessionId, pc, message);
506
+ }
507
+ logRemoteAudioError(sessionId, message, pc, extra) {
508
+ if (!this.enabled)
509
+ return;
510
+ console.error(message, {
511
+ sessionId,
512
+ pc: describePc(pc),
513
+ ...extra
514
+ });
515
+ void this.logInboundStats(sessionId, pc, message);
516
+ }
517
+ logMicRecoveryDrop(payload) {
518
+ if (!this.enabled)
519
+ return;
520
+ console.error("[sip] microphone dropped", payload);
521
+ }
522
+ logIceReady(sessionId, payload) {
523
+ if (!this.enabled)
524
+ return;
525
+ console.info("[sip] ice ready", { sessionId, ...payload });
526
+ }
527
+ logIceReadyConfig(sessionId, delayMs) {
528
+ if (!this.enabled)
529
+ return;
530
+ console.info("[sip] ice ready config", { sessionId, delayMs });
531
+ }
532
+ startCallStatsLogging(sessionId, session) {
533
+ if (!this.enabled || this.statsStops.has(sessionId))
534
+ return;
535
+ let pc = session?.connection ?? null;
536
+ const onPeer = (data) => {
537
+ pc = data.peerconnection;
538
+ };
539
+ session.on?.("peerconnection", onPeer);
540
+ const intervalMs = 3e3;
541
+ const logStats = async () => {
542
+ if (!this.enabled || !pc?.getStats)
543
+ return;
544
+ try {
545
+ const report = await pc.getStats();
546
+ const { outboundAudio, inboundAudio } = collectAudioStats(report);
547
+ console.info("[sip] call stats", {
548
+ sessionId,
549
+ pc: describePc(pc),
550
+ outboundAudio,
551
+ inboundAudio
552
+ });
553
+ } catch (err) {
554
+ console.error("[sip] call stats failed", { sessionId, error: err });
555
+ }
556
+ };
557
+ const timer = setInterval(() => {
558
+ void logStats();
559
+ }, intervalMs);
560
+ void logStats();
561
+ const stop = () => {
562
+ clearInterval(timer);
563
+ session.off?.("peerconnection", onPeer);
564
+ this.statsStops.delete(sessionId);
565
+ };
566
+ this.statsStops.set(sessionId, stop);
567
+ }
568
+ stopCallStatsLogging(sessionId) {
569
+ const stop = this.statsStops.get(sessionId);
570
+ if (stop)
571
+ stop();
572
+ }
573
+ async logOutboundStats(sessionId, pc, context) {
574
+ if (!pc?.getStats)
575
+ return;
576
+ try {
577
+ const report = await pc.getStats();
578
+ const { outboundAudio } = collectAudioStats(report);
579
+ if (outboundAudio.length) {
580
+ console.info("[sip] outgoing audio stats", {
581
+ sessionId,
582
+ context,
583
+ outboundAudio
584
+ });
585
+ }
586
+ } catch (err) {
587
+ console.error("[sip] outgoing audio stats failed", {
588
+ sessionId,
589
+ context,
590
+ error: err
591
+ });
592
+ }
593
+ }
594
+ async logInboundStats(sessionId, pc, context) {
595
+ if (!pc?.getStats)
596
+ return;
597
+ try {
598
+ const report = await pc.getStats();
599
+ const { inboundAudio } = collectAudioStats(report);
600
+ if (inboundAudio.length) {
601
+ console.error("[sip] incoming audio stats", {
602
+ sessionId,
603
+ context,
604
+ inboundAudio
605
+ });
606
+ }
607
+ } catch (err) {
608
+ console.error("[sip] incoming audio stats failed", {
609
+ sessionId,
610
+ context,
611
+ error: err
612
+ });
613
+ }
614
+ }
615
+ };
616
+ var sipDebugLogger = new SipDebugLogger();
617
+ function collectAudioStats(report) {
618
+ const outboundAudio = [];
619
+ const inboundAudio = [];
620
+ report.forEach((stat) => {
621
+ const kind = stat.kind ?? stat.mediaType;
622
+ if (stat.type === "outbound-rtp" && kind === "audio") {
623
+ outboundAudio.push({
624
+ id: stat.id,
625
+ packetsSent: stat.packetsSent,
626
+ bytesSent: stat.bytesSent,
627
+ jitter: stat.jitter,
628
+ roundTripTime: stat.roundTripTime
629
+ });
630
+ }
631
+ if (stat.type === "inbound-rtp" && kind === "audio") {
632
+ inboundAudio.push({
633
+ id: stat.id,
634
+ packetsReceived: stat.packetsReceived,
635
+ packetsLost: stat.packetsLost,
636
+ bytesReceived: stat.bytesReceived,
637
+ jitter: stat.jitter,
638
+ roundTripTime: stat.roundTripTime
639
+ });
640
+ }
641
+ });
642
+ return { outboundAudio, inboundAudio };
643
+ }
644
+
476
645
  // src/jssip-lib/sip/handlers/sessionHandlers.ts
477
646
  function createSessionHandlers(deps) {
478
647
  const {
@@ -481,8 +650,20 @@ function createSessionHandlers(deps) {
481
650
  rtc,
482
651
  detachSessionHandlers,
483
652
  onSessionFailed,
484
- sessionId
653
+ sessionId,
654
+ iceCandidateReadyDelayMs
485
655
  } = deps;
656
+ let iceReadyCalled = false;
657
+ let iceReadyTimer = null;
658
+ const clearIceReadyTimer = () => {
659
+ if (!iceReadyTimer)
660
+ return;
661
+ clearTimeout(iceReadyTimer);
662
+ iceReadyTimer = null;
663
+ };
664
+ if (typeof iceCandidateReadyDelayMs === "number") {
665
+ sipDebugLogger.logIceReadyConfig(sessionId, iceCandidateReadyDelayMs);
666
+ }
486
667
  return {
487
668
  progress: (e) => {
488
669
  emitter.emit("progress", e);
@@ -505,6 +686,7 @@ function createSessionHandlers(deps) {
505
686
  },
506
687
  ended: (e) => {
507
688
  emitter.emit("ended", e);
689
+ clearIceReadyTimer();
508
690
  detachSessionHandlers();
509
691
  rtc.cleanup();
510
692
  const nextSessions = state.getState().sessions.filter((s) => s.id !== sessionId);
@@ -514,6 +696,7 @@ function createSessionHandlers(deps) {
514
696
  },
515
697
  failed: (e) => {
516
698
  emitter.emit("failed", e);
699
+ clearIceReadyTimer();
517
700
  detachSessionHandlers();
518
701
  rtc.cleanup();
519
702
  const cause = e?.cause || "call failed";
@@ -542,13 +725,55 @@ function createSessionHandlers(deps) {
542
725
  reinvite: (e) => emitter.emit("reinvite", e),
543
726
  update: (e) => emitter.emit("update", e),
544
727
  sdp: (e) => emitter.emit("sdp", e),
545
- icecandidate: (e) => emitter.emit("icecandidate", e),
728
+ icecandidate: (e) => {
729
+ const candidate = e?.candidate;
730
+ const ready = typeof e?.ready === "function" ? e.ready : null;
731
+ const delayMs = typeof iceCandidateReadyDelayMs === "number" ? iceCandidateReadyDelayMs : null;
732
+ if (!iceReadyCalled && ready && delayMs != null) {
733
+ if (candidate?.type === "srflx" && candidate?.relatedAddress != null && candidate?.relatedPort != null) {
734
+ iceReadyCalled = true;
735
+ if (iceReadyTimer) {
736
+ clearTimeout(iceReadyTimer);
737
+ iceReadyTimer = null;
738
+ }
739
+ sipDebugLogger.logIceReady(sessionId, {
740
+ source: "srflx",
741
+ delayMs,
742
+ candidateType: candidate?.type
743
+ });
744
+ ready();
745
+ } else if (!iceReadyTimer && delayMs > 0) {
746
+ iceReadyTimer = setTimeout(() => {
747
+ iceReadyTimer = null;
748
+ if (iceReadyCalled)
749
+ return;
750
+ iceReadyCalled = true;
751
+ sipDebugLogger.logIceReady(sessionId, {
752
+ source: "timer",
753
+ delayMs,
754
+ candidateType: candidate?.type
755
+ });
756
+ ready();
757
+ }, delayMs);
758
+ } else if (delayMs === 0) {
759
+ iceReadyCalled = true;
760
+ sipDebugLogger.logIceReady(sessionId, {
761
+ source: "immediate",
762
+ delayMs,
763
+ candidateType: candidate?.type
764
+ });
765
+ ready();
766
+ }
767
+ }
768
+ emitter.emit("icecandidate", e);
769
+ },
546
770
  refer: (e) => emitter.emit("refer", e),
547
771
  replaces: (e) => emitter.emit("replaces", e),
548
772
  newDTMF: (e) => emitter.emit("newDTMF", e),
549
773
  newInfo: (e) => emitter.emit("newInfo", e),
550
774
  getusermediafailed: (e) => {
551
775
  emitter.emit("getusermediafailed", e);
776
+ clearIceReadyTimer();
552
777
  detachSessionHandlers();
553
778
  rtc.cleanup();
554
779
  onSessionFailed("getUserMedia failed", e);
@@ -558,6 +783,7 @@ function createSessionHandlers(deps) {
558
783
  },
559
784
  "peerconnection:createofferfailed": (e) => {
560
785
  emitter.emit("peerconnection:createofferfailed", e);
786
+ clearIceReadyTimer();
561
787
  detachSessionHandlers();
562
788
  rtc.cleanup();
563
789
  onSessionFailed("peer connection createOffer failed", e);
@@ -567,6 +793,7 @@ function createSessionHandlers(deps) {
567
793
  },
568
794
  "peerconnection:createanswerfailed": (e) => {
569
795
  emitter.emit("peerconnection:createanswerfailed", e);
796
+ clearIceReadyTimer();
570
797
  detachSessionHandlers();
571
798
  rtc.cleanup();
572
799
  onSessionFailed("peer connection createAnswer failed", e);
@@ -576,6 +803,7 @@ function createSessionHandlers(deps) {
576
803
  },
577
804
  "peerconnection:setlocaldescriptionfailed": (e) => {
578
805
  emitter.emit("peerconnection:setlocaldescriptionfailed", e);
806
+ clearIceReadyTimer();
579
807
  detachSessionHandlers();
580
808
  rtc.cleanup();
581
809
  onSessionFailed("peer connection setLocalDescription failed", e);
@@ -585,6 +813,7 @@ function createSessionHandlers(deps) {
585
813
  },
586
814
  "peerconnection:setremotedescriptionfailed": (e) => {
587
815
  emitter.emit("peerconnection:setremotedescriptionfailed", e);
816
+ clearIceReadyTimer();
588
817
  detachSessionHandlers();
589
818
  rtc.cleanup();
590
819
  onSessionFailed("peer connection setRemoteDescription failed", e);
@@ -847,6 +1076,9 @@ var SessionLifecycle = class {
847
1076
  this.attachSessionHandlers = deps.attachSessionHandlers;
848
1077
  this.getMaxSessionCount = deps.getMaxSessionCount;
849
1078
  }
1079
+ setDebugEnabled(enabled) {
1080
+ sipDebugLogger.setEnabled(enabled);
1081
+ }
850
1082
  handleNewRTCSession(e) {
851
1083
  const session = e.session;
852
1084
  const sessionId = String(
@@ -875,71 +1107,12 @@ var SessionLifecycle = class {
875
1107
  const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
876
1108
  this.sessionManager.setSession(sessionId, session);
877
1109
  this.attachSessionHandlers(sessionId, session);
1110
+ this.attachCallStatsLogging(sessionId, session);
878
1111
  if (e.originator === "local" && !rtc.mediaStream) {
879
- const maxAttempts = 5;
880
- const retryDelayMs = 500;
881
- let attempts = 0;
882
- let retryScheduled = false;
883
- let retryTimer = null;
884
- let stopped = false;
885
- const tryBindFromPc = (pc) => {
886
- if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
887
- return false;
888
- }
889
- const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
890
- const audioTrack = audioSender?.track;
891
- if (!audioTrack)
892
- return false;
893
- const outgoingStream = new MediaStream([audioTrack]);
894
- this.sessionManager.setSessionMedia(sessionId, outgoingStream);
895
- return true;
896
- };
897
- const scheduleRetry = (pc) => {
898
- if (stopped || retryScheduled || attempts >= maxAttempts) {
899
- session.off?.("peerconnection", onPeer);
900
- return;
901
- }
902
- retryScheduled = true;
903
- attempts += 1;
904
- retryTimer = setTimeout(() => {
905
- retryScheduled = false;
906
- retryTimer = null;
907
- if (tryBindFromPc(pc)) {
908
- session.off?.("peerconnection", onPeer);
909
- return;
910
- }
911
- scheduleRetry(pc);
912
- }, retryDelayMs);
913
- };
914
- const onPeer = (data) => {
915
- if (stopped)
916
- return;
917
- if (tryBindFromPc(data.peerconnection)) {
918
- session.off?.("peerconnection", onPeer);
919
- return;
920
- }
921
- scheduleRetry(data.peerconnection);
922
- };
923
- const stopRetry = () => {
924
- if (stopped)
925
- return;
926
- stopped = true;
927
- if (retryTimer) {
928
- clearTimeout(retryTimer);
929
- retryTimer = null;
930
- }
931
- session.off?.("peerconnection", onPeer);
932
- session.off?.("ended", stopRetry);
933
- session.off?.("failed", stopRetry);
934
- };
935
- const existingPc = session?.connection;
936
- if (!tryBindFromPc(existingPc)) {
937
- if (existingPc)
938
- scheduleRetry(existingPc);
939
- session.on?.("peerconnection", onPeer);
940
- }
941
- session.on?.("ended", stopRetry);
942
- session.on?.("failed", stopRetry);
1112
+ this.bindLocalOutgoingAudio(sessionId, session);
1113
+ }
1114
+ if (e.originator === "remote") {
1115
+ this.bindRemoteIncomingAudio(sessionId, session);
943
1116
  }
944
1117
  holdOtherSessions(this.state, sessionId, (id) => {
945
1118
  const otherRtc = this.sessionManager.getRtc(id);
@@ -956,6 +1129,414 @@ var SessionLifecycle = class {
956
1129
  });
957
1130
  this.emit("newRTCSession", e);
958
1131
  }
1132
+ bindLocalOutgoingAudio(sessionId, session) {
1133
+ const maxAttempts = 50;
1134
+ const retryDelayMs = 500;
1135
+ let attempts = 0;
1136
+ let retryScheduled = false;
1137
+ let retryTimer = null;
1138
+ let stopped = false;
1139
+ let exhausted = false;
1140
+ let exhaustedCheckUsed = false;
1141
+ let attachedPc = null;
1142
+ const logLocalAudioError = (message, pc, extra) => {
1143
+ sipDebugLogger.logLocalAudioError(sessionId, message, pc, extra);
1144
+ };
1145
+ const tryBindFromPc = (pc) => {
1146
+ if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
1147
+ return false;
1148
+ }
1149
+ const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1150
+ const audioTrack = audioSender?.track;
1151
+ if (!audioTrack) {
1152
+ logLocalAudioError(
1153
+ "[sip] outgoing audio bind failed: no audio track",
1154
+ pc
1155
+ );
1156
+ return false;
1157
+ }
1158
+ const outgoingStream = new MediaStream([audioTrack]);
1159
+ this.sessionManager.setSessionMedia(sessionId, outgoingStream);
1160
+ return true;
1161
+ };
1162
+ const onPcStateChange = () => {
1163
+ if (stopped)
1164
+ return;
1165
+ if (exhausted) {
1166
+ if (exhaustedCheckUsed)
1167
+ return;
1168
+ exhaustedCheckUsed = true;
1169
+ if (tryBindFromPc(attachedPc))
1170
+ stopRetry();
1171
+ return;
1172
+ }
1173
+ if (tryBindFromPc(attachedPc))
1174
+ stopRetry();
1175
+ };
1176
+ const attachPcListeners = (pc) => {
1177
+ if (!pc || pc === attachedPc)
1178
+ return;
1179
+ if (attachedPc) {
1180
+ attachedPc.removeEventListener?.(
1181
+ "signalingstatechange",
1182
+ onPcStateChange
1183
+ );
1184
+ attachedPc.removeEventListener?.(
1185
+ "connectionstatechange",
1186
+ onPcStateChange
1187
+ );
1188
+ attachedPc.removeEventListener?.(
1189
+ "iceconnectionstatechange",
1190
+ onPcStateChange
1191
+ );
1192
+ }
1193
+ attachedPc = pc;
1194
+ attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1195
+ attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1196
+ attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1197
+ };
1198
+ const clearRetryTimer = () => {
1199
+ if (!retryTimer)
1200
+ return;
1201
+ clearTimeout(retryTimer);
1202
+ retryTimer = null;
1203
+ };
1204
+ const stopRetry = () => {
1205
+ if (stopped)
1206
+ return;
1207
+ stopped = true;
1208
+ clearRetryTimer();
1209
+ if (attachedPc) {
1210
+ attachedPc.removeEventListener?.(
1211
+ "signalingstatechange",
1212
+ onPcStateChange
1213
+ );
1214
+ attachedPc.removeEventListener?.(
1215
+ "connectionstatechange",
1216
+ onPcStateChange
1217
+ );
1218
+ attachedPc.removeEventListener?.(
1219
+ "iceconnectionstatechange",
1220
+ onPcStateChange
1221
+ );
1222
+ attachedPc = null;
1223
+ }
1224
+ session.off?.("peerconnection", onPeer);
1225
+ session.off?.("confirmed", onConfirmed);
1226
+ session.off?.("ended", stopRetry);
1227
+ session.off?.("failed", stopRetry);
1228
+ };
1229
+ const scheduleRetry = (pc) => {
1230
+ if (stopped || retryScheduled || exhausted)
1231
+ return;
1232
+ if (attempts >= maxAttempts) {
1233
+ logLocalAudioError(
1234
+ "[sip] outgoing audio bind failed: max retries reached",
1235
+ pc,
1236
+ { attempts }
1237
+ );
1238
+ exhausted = true;
1239
+ clearRetryTimer();
1240
+ return;
1241
+ }
1242
+ if (!pc) {
1243
+ logLocalAudioError(
1244
+ "[sip] outgoing audio bind failed: missing peerconnection",
1245
+ pc
1246
+ );
1247
+ }
1248
+ retryScheduled = true;
1249
+ attempts += 1;
1250
+ retryTimer = setTimeout(() => {
1251
+ retryScheduled = false;
1252
+ retryTimer = null;
1253
+ if (tryBindFromPc(pc)) {
1254
+ stopRetry();
1255
+ return;
1256
+ }
1257
+ scheduleRetry(pc);
1258
+ }, retryDelayMs);
1259
+ };
1260
+ const onPeer = (data) => {
1261
+ if (stopped)
1262
+ return;
1263
+ attachPcListeners(data.peerconnection);
1264
+ if (exhausted) {
1265
+ if (exhaustedCheckUsed)
1266
+ return;
1267
+ exhaustedCheckUsed = true;
1268
+ if (tryBindFromPc(data.peerconnection))
1269
+ stopRetry();
1270
+ return;
1271
+ }
1272
+ if (tryBindFromPc(data.peerconnection)) {
1273
+ stopRetry();
1274
+ return;
1275
+ }
1276
+ scheduleRetry(data.peerconnection);
1277
+ };
1278
+ const onConfirmed = () => {
1279
+ if (stopped)
1280
+ return;
1281
+ const currentPc = session?.connection ?? attachedPc;
1282
+ if (exhausted) {
1283
+ if (exhaustedCheckUsed)
1284
+ return;
1285
+ exhaustedCheckUsed = true;
1286
+ if (tryBindFromPc(currentPc))
1287
+ stopRetry();
1288
+ return;
1289
+ }
1290
+ if (tryBindFromPc(currentPc)) {
1291
+ stopRetry();
1292
+ return;
1293
+ }
1294
+ scheduleRetry(currentPc);
1295
+ };
1296
+ const existingPc = session?.connection;
1297
+ if (!tryBindFromPc(existingPc)) {
1298
+ if (existingPc) {
1299
+ attachPcListeners(existingPc);
1300
+ scheduleRetry(existingPc);
1301
+ }
1302
+ session.on?.("peerconnection", onPeer);
1303
+ }
1304
+ session.on?.("confirmed", onConfirmed);
1305
+ session.on?.("ended", stopRetry);
1306
+ session.on?.("failed", stopRetry);
1307
+ }
1308
+ bindRemoteIncomingAudio(sessionId, session) {
1309
+ const maxAttempts = 50;
1310
+ const retryDelayMs = 500;
1311
+ let attempts = 0;
1312
+ let retryScheduled = false;
1313
+ let retryTimer = null;
1314
+ let stopped = false;
1315
+ let exhausted = false;
1316
+ let exhaustedCheckUsed = false;
1317
+ let attachedPc = null;
1318
+ let attachedTrack = null;
1319
+ const logRemoteAudioError = (message, pc, extra) => {
1320
+ sipDebugLogger.logRemoteAudioError(sessionId, message, pc, extra);
1321
+ };
1322
+ const logMissingReceiver = (pc, note) => {
1323
+ logRemoteAudioError(
1324
+ "[sip] incoming audio bind failed: no remote track",
1325
+ pc,
1326
+ { note }
1327
+ );
1328
+ };
1329
+ const getRemoteAudioTrack = (pc) => {
1330
+ const receiver = pc?.getReceivers?.()?.find((r) => r.track?.kind === "audio");
1331
+ return receiver?.track ?? null;
1332
+ };
1333
+ const attachTrackListeners = (track) => {
1334
+ if (!track || track === attachedTrack)
1335
+ return;
1336
+ if (attachedTrack) {
1337
+ attachedTrack.removeEventListener?.("ended", onRemoteEnded);
1338
+ attachedTrack.removeEventListener?.("mute", onRemoteMuted);
1339
+ }
1340
+ attachedTrack = track;
1341
+ attachedTrack.addEventListener?.("ended", onRemoteEnded);
1342
+ attachedTrack.addEventListener?.("mute", onRemoteMuted);
1343
+ };
1344
+ const checkRemoteTrack = (pc) => {
1345
+ if (stopped || !pc)
1346
+ return false;
1347
+ const track = getRemoteAudioTrack(pc);
1348
+ if (!track)
1349
+ return false;
1350
+ attachTrackListeners(track);
1351
+ if (track.readyState !== "live") {
1352
+ logRemoteAudioError("[sip] incoming audio track not live", pc, {
1353
+ trackState: track.readyState
1354
+ });
1355
+ }
1356
+ return true;
1357
+ };
1358
+ const onRemoteEnded = () => {
1359
+ logRemoteAudioError("[sip] incoming audio track ended", attachedPc);
1360
+ };
1361
+ const onRemoteMuted = () => {
1362
+ logRemoteAudioError("[sip] incoming audio track muted", attachedPc);
1363
+ };
1364
+ const onPcStateChange = () => {
1365
+ if (stopped)
1366
+ return;
1367
+ if (exhausted) {
1368
+ if (exhaustedCheckUsed)
1369
+ return;
1370
+ exhaustedCheckUsed = true;
1371
+ if (checkRemoteTrack(attachedPc))
1372
+ stopRetry();
1373
+ return;
1374
+ }
1375
+ if (checkRemoteTrack(attachedPc))
1376
+ stopRetry();
1377
+ };
1378
+ const attachPcListeners = (pc) => {
1379
+ if (!pc || pc === attachedPc)
1380
+ return;
1381
+ if (attachedPc) {
1382
+ attachedPc.removeEventListener?.(
1383
+ "signalingstatechange",
1384
+ onPcStateChange
1385
+ );
1386
+ attachedPc.removeEventListener?.(
1387
+ "connectionstatechange",
1388
+ onPcStateChange
1389
+ );
1390
+ attachedPc.removeEventListener?.(
1391
+ "iceconnectionstatechange",
1392
+ onPcStateChange
1393
+ );
1394
+ attachedPc.removeEventListener?.("track", onTrack);
1395
+ }
1396
+ attachedPc = pc;
1397
+ attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1398
+ attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1399
+ attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1400
+ attachedPc.addEventListener?.("track", onTrack);
1401
+ };
1402
+ const clearRetryTimer = () => {
1403
+ if (!retryTimer)
1404
+ return;
1405
+ clearTimeout(retryTimer);
1406
+ retryTimer = null;
1407
+ };
1408
+ const stopRetry = () => {
1409
+ if (stopped)
1410
+ return;
1411
+ stopped = true;
1412
+ clearRetryTimer();
1413
+ if (attachedPc) {
1414
+ attachedPc.removeEventListener?.(
1415
+ "signalingstatechange",
1416
+ onPcStateChange
1417
+ );
1418
+ attachedPc.removeEventListener?.(
1419
+ "connectionstatechange",
1420
+ onPcStateChange
1421
+ );
1422
+ attachedPc.removeEventListener?.(
1423
+ "iceconnectionstatechange",
1424
+ onPcStateChange
1425
+ );
1426
+ attachedPc.removeEventListener?.("track", onTrack);
1427
+ attachedPc = null;
1428
+ }
1429
+ if (attachedTrack) {
1430
+ attachedTrack.removeEventListener?.("ended", onRemoteEnded);
1431
+ attachedTrack.removeEventListener?.("mute", onRemoteMuted);
1432
+ attachedTrack = null;
1433
+ }
1434
+ session.off?.("peerconnection", onPeer);
1435
+ session.off?.("confirmed", onConfirmed);
1436
+ session.off?.("ended", stopRetry);
1437
+ session.off?.("failed", stopRetry);
1438
+ };
1439
+ const scheduleRetry = (pc) => {
1440
+ if (stopped || retryScheduled || exhausted)
1441
+ return;
1442
+ if (attempts >= maxAttempts) {
1443
+ logRemoteAudioError(
1444
+ "[sip] incoming audio bind failed: max retries reached",
1445
+ pc,
1446
+ { attempts }
1447
+ );
1448
+ exhausted = true;
1449
+ clearRetryTimer();
1450
+ return;
1451
+ }
1452
+ retryScheduled = true;
1453
+ attempts += 1;
1454
+ retryTimer = setTimeout(() => {
1455
+ retryScheduled = false;
1456
+ retryTimer = null;
1457
+ if (checkRemoteTrack(pc)) {
1458
+ stopRetry();
1459
+ return;
1460
+ }
1461
+ if (!pc)
1462
+ logMissingReceiver(pc, "missing peerconnection");
1463
+ scheduleRetry(pc);
1464
+ }, retryDelayMs);
1465
+ };
1466
+ const onTrack = () => {
1467
+ if (stopped)
1468
+ return;
1469
+ if (exhausted) {
1470
+ if (exhaustedCheckUsed)
1471
+ return;
1472
+ exhaustedCheckUsed = true;
1473
+ if (checkRemoteTrack(attachedPc))
1474
+ stopRetry();
1475
+ return;
1476
+ }
1477
+ if (checkRemoteTrack(attachedPc))
1478
+ stopRetry();
1479
+ };
1480
+ const onPeer = (data) => {
1481
+ if (stopped)
1482
+ return;
1483
+ attachPcListeners(data.peerconnection);
1484
+ if (exhausted) {
1485
+ if (exhaustedCheckUsed)
1486
+ return;
1487
+ exhaustedCheckUsed = true;
1488
+ if (checkRemoteTrack(data.peerconnection))
1489
+ stopRetry();
1490
+ return;
1491
+ }
1492
+ if (checkRemoteTrack(data.peerconnection)) {
1493
+ stopRetry();
1494
+ return;
1495
+ }
1496
+ scheduleRetry(data.peerconnection);
1497
+ };
1498
+ const onConfirmed = () => {
1499
+ if (stopped)
1500
+ return;
1501
+ const currentPc = session?.connection ?? attachedPc;
1502
+ if (exhausted) {
1503
+ if (exhaustedCheckUsed)
1504
+ return;
1505
+ exhaustedCheckUsed = true;
1506
+ if (checkRemoteTrack(currentPc))
1507
+ stopRetry();
1508
+ return;
1509
+ }
1510
+ if (checkRemoteTrack(currentPc)) {
1511
+ stopRetry();
1512
+ return;
1513
+ }
1514
+ logMissingReceiver(currentPc, "confirmed without remote track");
1515
+ scheduleRetry(currentPc);
1516
+ };
1517
+ const existingPc = session?.connection;
1518
+ if (!checkRemoteTrack(existingPc)) {
1519
+ if (existingPc) {
1520
+ attachPcListeners(existingPc);
1521
+ scheduleRetry(existingPc);
1522
+ }
1523
+ session.on?.("peerconnection", onPeer);
1524
+ }
1525
+ session.on?.("confirmed", onConfirmed);
1526
+ session.on?.("ended", stopRetry);
1527
+ session.on?.("failed", stopRetry);
1528
+ }
1529
+ attachCallStatsLogging(sessionId, session) {
1530
+ const onConfirmed = () => {
1531
+ sipDebugLogger.startCallStatsLogging(sessionId, session);
1532
+ };
1533
+ const onEnd = () => {
1534
+ sipDebugLogger.stopCallStatsLogging(sessionId);
1535
+ };
1536
+ session.on?.("confirmed", onConfirmed);
1537
+ session.on?.("ended", onEnd);
1538
+ session.on?.("failed", onEnd);
1539
+ }
959
1540
  };
960
1541
 
961
1542
  // src/jssip-lib/sip/micRecovery.ts
@@ -1016,15 +1597,11 @@ var MicRecoveryManager = class {
1016
1597
  const senderLive = sender?.track?.readyState === "live";
1017
1598
  if (trackLive && senderLive)
1018
1599
  return;
1019
- this.deps.emitError(
1020
- {
1021
- cause: "microphone dropped",
1022
- trackLive,
1023
- senderLive
1024
- },
1025
- "MICROPHONE_DROPPED",
1026
- "microphone dropped"
1027
- );
1600
+ sipDebugLogger.logMicRecoveryDrop({
1601
+ sessionId,
1602
+ trackLive,
1603
+ senderLive
1604
+ });
1028
1605
  retries += 1;
1029
1606
  if (trackLive && !senderLive && track) {
1030
1607
  await rtc.replaceAudioTrack(track);
@@ -1133,9 +1710,11 @@ var SipClient = class extends EventTargetEmitter {
1133
1710
  micRecoveryIntervalMs,
1134
1711
  micRecoveryMaxRetries,
1135
1712
  maxSessionCount,
1713
+ iceCandidateReadyDelayMs,
1136
1714
  ...uaCfg
1137
1715
  } = config;
1138
1716
  this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
1717
+ this.iceCandidateReadyDelayMs = typeof iceCandidateReadyDelayMs === "number" ? iceCandidateReadyDelayMs : void 0;
1139
1718
  this.micRecovery.configure({
1140
1719
  enabled: Boolean(enableMicRecovery),
1141
1720
  intervalMs: micRecoveryIntervalMs,
@@ -1143,6 +1722,7 @@ var SipClient = class extends EventTargetEmitter {
1143
1722
  });
1144
1723
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
1145
1724
  this.userAgent.start(uri, password, uaCfg, { debug });
1725
+ this.lifecycle.setDebugEnabled(Boolean(debug));
1146
1726
  this.attachUAHandlers();
1147
1727
  this.attachBeforeUnload();
1148
1728
  this.syncDebugInspector(debug);
@@ -1223,6 +1803,7 @@ var SipClient = class extends EventTargetEmitter {
1223
1803
  setDebug(debug) {
1224
1804
  this.debugPattern = debug;
1225
1805
  this.userAgent.setDebug(debug);
1806
+ this.lifecycle.setDebugEnabled(Boolean(debug));
1226
1807
  this.syncDebugInspector(debug);
1227
1808
  }
1228
1809
  attachSessionHandlers(sessionId, session) {
@@ -1281,6 +1862,7 @@ var SipClient = class extends EventTargetEmitter {
1281
1862
  emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1282
1863
  onSessionFailed: (err, event) => this.onSessionFailed(err, event),
1283
1864
  enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
1865
+ iceCandidateReadyDelayMs: this.iceCandidateReadyDelayMs,
1284
1866
  sessionId
1285
1867
  });
1286
1868
  }
@@ -1436,14 +2018,17 @@ var SipClient = class extends EventTargetEmitter {
1436
2018
  syncDebugInspector(debug) {
1437
2019
  if (typeof window === "undefined")
1438
2020
  return;
1439
- this.toggleStateLogger(Boolean(debug));
2021
+ const persisted = this.getPersistedDebug();
2022
+ const effectiveDebug = debug ?? persisted ?? this.debugPattern;
2023
+ this.lifecycle.setDebugEnabled(Boolean(effectiveDebug));
2024
+ this.toggleStateLogger(Boolean(effectiveDebug));
1440
2025
  const win = window;
1441
2026
  const disabledInspector = () => {
1442
2027
  console.warn("SIP debug inspector disabled; enable debug to inspect.");
1443
2028
  return null;
1444
2029
  };
1445
- win.sipState = () => debug ? this.stateStore.getState() : disabledInspector();
1446
- win.sipSessions = () => debug ? this.getSessions() : disabledInspector();
2030
+ win.sipState = () => effectiveDebug ? this.stateStore.getState() : disabledInspector();
2031
+ win.sipSessions = () => effectiveDebug ? this.getSessions() : disabledInspector();
1447
2032
  }
1448
2033
  toggleStateLogger(enabled) {
1449
2034
  if (!enabled) {
@@ -1456,22 +2041,10 @@ var SipClient = class extends EventTargetEmitter {
1456
2041
  let prev = this.stateStore.getState();
1457
2042
  console.info("[sip][state]", { initial: true }, prev);
1458
2043
  this.stateLogOff = this.stateStore.onChange((next) => {
1459
- const changes = this.diffState(prev, next);
1460
- if (changes) {
1461
- console.info("[sip][state]", changes, next);
1462
- }
2044
+ console.info("[sip][state]", next);
1463
2045
  prev = next;
1464
2046
  });
1465
2047
  }
1466
- diffState(prev, next) {
1467
- const changed = {};
1468
- for (const key of Object.keys(next)) {
1469
- if (prev[key] !== next[key]) {
1470
- changed[key] = { from: prev[key], to: next[key] };
1471
- }
1472
- }
1473
- return Object.keys(changed).length ? changed : null;
1474
- }
1475
2048
  getPersistedDebug() {
1476
2049
  if (typeof window === "undefined")
1477
2050
  return void 0;