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