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.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) =>
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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.
|
|
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 = () =>
|
|
1446
|
-
win.sipSessions = () =>
|
|
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
|
-
|
|
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;
|