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 +664 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +664 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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) =>
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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.
|
|
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 = () =>
|
|
1441
|
-
win.sipSessions = () =>
|
|
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
|
-
|
|
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;
|