react-jssip-kit 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +767 -183
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -15
- package/dist/index.d.ts +0 -15
- package/dist/index.js +767 -183
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -481,7 +481,6 @@ function createSessionHandlers(deps) {
|
|
|
481
481
|
rtc,
|
|
482
482
|
detachSessionHandlers,
|
|
483
483
|
onSessionFailed,
|
|
484
|
-
onSessionConfirmed,
|
|
485
484
|
sessionId
|
|
486
485
|
} = deps;
|
|
487
486
|
return {
|
|
@@ -502,7 +501,7 @@ function createSessionHandlers(deps) {
|
|
|
502
501
|
},
|
|
503
502
|
confirmed: (e) => {
|
|
504
503
|
emitter.emit("confirmed", e);
|
|
505
|
-
|
|
504
|
+
deps.enableMicrophoneRecovery?.(sessionId);
|
|
506
505
|
},
|
|
507
506
|
ended: (e) => {
|
|
508
507
|
emitter.emit("ended", e);
|
|
@@ -614,8 +613,8 @@ var WebRTCSessionController = class {
|
|
|
614
613
|
}
|
|
615
614
|
cleanup(stopTracks = true) {
|
|
616
615
|
const pc = this.getPC();
|
|
616
|
+
const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
|
|
617
617
|
if (pc && typeof pc.getSenders === "function") {
|
|
618
|
-
const isClosed = pc.connectionState === "closed" || pc.signalingState === "closed";
|
|
619
618
|
if (!isClosed) {
|
|
620
619
|
for (const s of pc.getSenders()) {
|
|
621
620
|
try {
|
|
@@ -626,8 +625,14 @@ var WebRTCSessionController = class {
|
|
|
626
625
|
}
|
|
627
626
|
}
|
|
628
627
|
if (stopTracks && this.mediaStream) {
|
|
629
|
-
|
|
628
|
+
const senderTracks = pc && !isClosed ? new Set(
|
|
629
|
+
pc.getSenders().map((s) => s.track).filter((t) => Boolean(t))
|
|
630
|
+
) : null;
|
|
631
|
+
for (const t of this.mediaStream.getTracks()) {
|
|
632
|
+
if (senderTracks?.has(t))
|
|
633
|
+
continue;
|
|
630
634
|
t.stop();
|
|
635
|
+
}
|
|
631
636
|
}
|
|
632
637
|
this.mediaStream = null;
|
|
633
638
|
this.currentSession = null;
|
|
@@ -706,29 +711,14 @@ var WebRTCSessionController = class {
|
|
|
706
711
|
var SessionManager = class {
|
|
707
712
|
constructor() {
|
|
708
713
|
this.entries = /* @__PURE__ */ new Map();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
enqueueOutgoingMedia(stream) {
|
|
717
|
-
this.pendingMediaQueue.push({ stream, addedAt: Date.now() });
|
|
718
|
-
}
|
|
719
|
-
dequeueOutgoingMedia() {
|
|
720
|
-
const now = Date.now();
|
|
721
|
-
while (this.pendingMediaQueue.length) {
|
|
722
|
-
const next = this.pendingMediaQueue.shift();
|
|
723
|
-
if (!next)
|
|
724
|
-
break;
|
|
725
|
-
if (now - next.addedAt <= this.pendingMediaTtlMs) {
|
|
726
|
-
return next.stream;
|
|
727
|
-
} else {
|
|
728
|
-
next.stream.getTracks().forEach((t) => t.stop());
|
|
729
|
-
}
|
|
714
|
+
}
|
|
715
|
+
stopMediaStream(stream) {
|
|
716
|
+
if (!stream)
|
|
717
|
+
return;
|
|
718
|
+
for (const t of stream.getTracks()) {
|
|
719
|
+
if (t.readyState !== "ended")
|
|
720
|
+
t.stop();
|
|
730
721
|
}
|
|
731
|
-
return null;
|
|
732
722
|
}
|
|
733
723
|
getOrCreateRtc(sessionId, session) {
|
|
734
724
|
let entry = this.entries.get(sessionId);
|
|
@@ -770,6 +760,9 @@ var SessionManager = class {
|
|
|
770
760
|
session: null,
|
|
771
761
|
media: null
|
|
772
762
|
};
|
|
763
|
+
if (entry.media && entry.media !== stream) {
|
|
764
|
+
this.stopMediaStream(entry.media);
|
|
765
|
+
}
|
|
773
766
|
entry.media = stream;
|
|
774
767
|
entry.rtc.setMediaStream(stream);
|
|
775
768
|
this.entries.set(sessionId, entry);
|
|
@@ -799,15 +792,16 @@ var SessionManager = class {
|
|
|
799
792
|
const entry = this.entries.get(sessionId);
|
|
800
793
|
if (entry) {
|
|
801
794
|
entry.rtc.cleanup();
|
|
795
|
+
this.stopMediaStream(entry.media);
|
|
802
796
|
this.entries.delete(sessionId);
|
|
803
797
|
}
|
|
804
798
|
}
|
|
805
799
|
cleanupAllSessions() {
|
|
806
800
|
for (const [, entry] of this.entries.entries()) {
|
|
807
801
|
entry.rtc.cleanup();
|
|
802
|
+
this.stopMediaStream(entry.media);
|
|
808
803
|
}
|
|
809
804
|
this.entries.clear();
|
|
810
|
-
this.pendingMediaQueue = [];
|
|
811
805
|
}
|
|
812
806
|
answer(sessionId, options) {
|
|
813
807
|
const rtc = this.getRtc(sessionId);
|
|
@@ -843,6 +837,165 @@ var SessionManager = class {
|
|
|
843
837
|
}
|
|
844
838
|
};
|
|
845
839
|
|
|
840
|
+
// src/jssip-lib/sip/debugLogging.ts
|
|
841
|
+
var describePc = (pc) => ({
|
|
842
|
+
connectionState: pc?.connectionState,
|
|
843
|
+
signalingState: pc?.signalingState,
|
|
844
|
+
iceConnectionState: pc?.iceConnectionState
|
|
845
|
+
});
|
|
846
|
+
var SipDebugLogger = class {
|
|
847
|
+
constructor() {
|
|
848
|
+
this.enabled = false;
|
|
849
|
+
this.statsStops = /* @__PURE__ */ new Map();
|
|
850
|
+
}
|
|
851
|
+
setEnabled(enabled) {
|
|
852
|
+
this.enabled = enabled;
|
|
853
|
+
if (!enabled) {
|
|
854
|
+
this.statsStops.forEach((stop) => stop());
|
|
855
|
+
this.statsStops.clear();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
isEnabled() {
|
|
859
|
+
return this.enabled;
|
|
860
|
+
}
|
|
861
|
+
logLocalAudioError(sessionId, message, pc, extra) {
|
|
862
|
+
if (!this.enabled)
|
|
863
|
+
return;
|
|
864
|
+
console.error(message, {
|
|
865
|
+
sessionId,
|
|
866
|
+
pc: describePc(pc),
|
|
867
|
+
...extra
|
|
868
|
+
});
|
|
869
|
+
void this.logOutboundStats(sessionId, pc, message);
|
|
870
|
+
}
|
|
871
|
+
logRemoteAudioError(sessionId, message, pc, extra) {
|
|
872
|
+
if (!this.enabled)
|
|
873
|
+
return;
|
|
874
|
+
console.error(message, {
|
|
875
|
+
sessionId,
|
|
876
|
+
pc: describePc(pc),
|
|
877
|
+
...extra
|
|
878
|
+
});
|
|
879
|
+
void this.logInboundStats(sessionId, pc, message);
|
|
880
|
+
}
|
|
881
|
+
logMicRecoveryDrop(payload) {
|
|
882
|
+
if (!this.enabled)
|
|
883
|
+
return;
|
|
884
|
+
console.error("[sip] microphone dropped", payload);
|
|
885
|
+
}
|
|
886
|
+
startCallStatsLogging(sessionId, session) {
|
|
887
|
+
if (!this.enabled || this.statsStops.has(sessionId))
|
|
888
|
+
return;
|
|
889
|
+
let pc = session?.connection ?? null;
|
|
890
|
+
const onPeer = (data) => {
|
|
891
|
+
pc = data.peerconnection;
|
|
892
|
+
};
|
|
893
|
+
session.on?.("peerconnection", onPeer);
|
|
894
|
+
const intervalMs = 3e3;
|
|
895
|
+
const logStats = async () => {
|
|
896
|
+
if (!this.enabled || !pc?.getStats)
|
|
897
|
+
return;
|
|
898
|
+
try {
|
|
899
|
+
const report = await pc.getStats();
|
|
900
|
+
const { outboundAudio, inboundAudio } = collectAudioStats(report);
|
|
901
|
+
console.info("[sip] call stats", {
|
|
902
|
+
sessionId,
|
|
903
|
+
pc: describePc(pc),
|
|
904
|
+
outboundAudio,
|
|
905
|
+
inboundAudio
|
|
906
|
+
});
|
|
907
|
+
} catch (err) {
|
|
908
|
+
console.error("[sip] call stats failed", { sessionId, error: err });
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const timer = setInterval(() => {
|
|
912
|
+
void logStats();
|
|
913
|
+
}, intervalMs);
|
|
914
|
+
void logStats();
|
|
915
|
+
const stop = () => {
|
|
916
|
+
clearInterval(timer);
|
|
917
|
+
session.off?.("peerconnection", onPeer);
|
|
918
|
+
this.statsStops.delete(sessionId);
|
|
919
|
+
};
|
|
920
|
+
this.statsStops.set(sessionId, stop);
|
|
921
|
+
}
|
|
922
|
+
stopCallStatsLogging(sessionId) {
|
|
923
|
+
const stop = this.statsStops.get(sessionId);
|
|
924
|
+
if (stop)
|
|
925
|
+
stop();
|
|
926
|
+
}
|
|
927
|
+
async logOutboundStats(sessionId, pc, context) {
|
|
928
|
+
if (!pc?.getStats)
|
|
929
|
+
return;
|
|
930
|
+
try {
|
|
931
|
+
const report = await pc.getStats();
|
|
932
|
+
const { outboundAudio } = collectAudioStats(report);
|
|
933
|
+
if (outboundAudio.length) {
|
|
934
|
+
console.info("[sip] outgoing audio stats", {
|
|
935
|
+
sessionId,
|
|
936
|
+
context,
|
|
937
|
+
outboundAudio
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.error("[sip] outgoing audio stats failed", {
|
|
942
|
+
sessionId,
|
|
943
|
+
context,
|
|
944
|
+
error: err
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async logInboundStats(sessionId, pc, context) {
|
|
949
|
+
if (!pc?.getStats)
|
|
950
|
+
return;
|
|
951
|
+
try {
|
|
952
|
+
const report = await pc.getStats();
|
|
953
|
+
const { inboundAudio } = collectAudioStats(report);
|
|
954
|
+
if (inboundAudio.length) {
|
|
955
|
+
console.error("[sip] incoming audio stats", {
|
|
956
|
+
sessionId,
|
|
957
|
+
context,
|
|
958
|
+
inboundAudio
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
} catch (err) {
|
|
962
|
+
console.error("[sip] incoming audio stats failed", {
|
|
963
|
+
sessionId,
|
|
964
|
+
context,
|
|
965
|
+
error: err
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
var sipDebugLogger = new SipDebugLogger();
|
|
971
|
+
function collectAudioStats(report) {
|
|
972
|
+
const outboundAudio = [];
|
|
973
|
+
const inboundAudio = [];
|
|
974
|
+
report.forEach((stat) => {
|
|
975
|
+
const kind = stat.kind ?? stat.mediaType;
|
|
976
|
+
if (stat.type === "outbound-rtp" && kind === "audio") {
|
|
977
|
+
outboundAudio.push({
|
|
978
|
+
id: stat.id,
|
|
979
|
+
packetsSent: stat.packetsSent,
|
|
980
|
+
bytesSent: stat.bytesSent,
|
|
981
|
+
jitter: stat.jitter,
|
|
982
|
+
roundTripTime: stat.roundTripTime
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
if (stat.type === "inbound-rtp" && kind === "audio") {
|
|
986
|
+
inboundAudio.push({
|
|
987
|
+
id: stat.id,
|
|
988
|
+
packetsReceived: stat.packetsReceived,
|
|
989
|
+
packetsLost: stat.packetsLost,
|
|
990
|
+
bytesReceived: stat.bytesReceived,
|
|
991
|
+
jitter: stat.jitter,
|
|
992
|
+
roundTripTime: stat.roundTripTime
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
return { outboundAudio, inboundAudio };
|
|
997
|
+
}
|
|
998
|
+
|
|
846
999
|
// src/jssip-lib/sip/sessionLifecycle.ts
|
|
847
1000
|
var SessionLifecycle = class {
|
|
848
1001
|
constructor(deps) {
|
|
@@ -853,37 +1006,48 @@ var SessionLifecycle = class {
|
|
|
853
1006
|
this.attachSessionHandlers = deps.attachSessionHandlers;
|
|
854
1007
|
this.getMaxSessionCount = deps.getMaxSessionCount;
|
|
855
1008
|
}
|
|
1009
|
+
setDebugEnabled(enabled) {
|
|
1010
|
+
sipDebugLogger.setEnabled(enabled);
|
|
1011
|
+
}
|
|
856
1012
|
handleNewRTCSession(e) {
|
|
857
1013
|
const session = e.session;
|
|
858
|
-
const sessionId = String(
|
|
1014
|
+
const sessionId = String(
|
|
1015
|
+
session?.id ?? crypto.randomUUID?.() ?? Date.now()
|
|
1016
|
+
);
|
|
859
1017
|
const currentSessions = this.state.getState().sessions;
|
|
860
1018
|
if (currentSessions.length >= this.getMaxSessionCount()) {
|
|
861
1019
|
try {
|
|
862
|
-
session.terminate?.({
|
|
1020
|
+
session.terminate?.({
|
|
1021
|
+
status_code: 486,
|
|
1022
|
+
reason_phrase: "Busy Here"
|
|
1023
|
+
});
|
|
863
1024
|
} catch {
|
|
864
1025
|
}
|
|
865
1026
|
if (e.originator === "remote") {
|
|
866
1027
|
this.emit("missed", e);
|
|
1028
|
+
} else {
|
|
1029
|
+
this.emitError(
|
|
1030
|
+
"max session count reached",
|
|
1031
|
+
"MAX_SESSIONS_REACHED",
|
|
1032
|
+
"max session count reached"
|
|
1033
|
+
);
|
|
867
1034
|
}
|
|
868
|
-
this.emitError("max session count reached", "MAX_SESSIONS_REACHED", "max session count reached");
|
|
869
1035
|
return;
|
|
870
1036
|
}
|
|
871
|
-
const outgoingMedia = e.originator === "local" ? this.sessionManager.dequeueOutgoingMedia() : null;
|
|
872
|
-
if (outgoingMedia)
|
|
873
|
-
this.sessionManager.setSessionMedia(sessionId, outgoingMedia);
|
|
874
1037
|
const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
|
|
875
|
-
if (outgoingMedia)
|
|
876
|
-
rtc.setMediaStream(outgoingMedia);
|
|
877
1038
|
this.sessionManager.setSession(sessionId, session);
|
|
878
1039
|
this.attachSessionHandlers(sessionId, session);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
sessionId,
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
)
|
|
1040
|
+
this.attachCallStatsLogging(sessionId, session);
|
|
1041
|
+
if (e.originator === "local" && !rtc.mediaStream) {
|
|
1042
|
+
this.bindLocalOutgoingAudio(sessionId, session);
|
|
1043
|
+
}
|
|
1044
|
+
if (e.originator === "remote") {
|
|
1045
|
+
this.bindRemoteIncomingAudio(sessionId, session);
|
|
1046
|
+
}
|
|
1047
|
+
holdOtherSessions(this.state, sessionId, (id) => {
|
|
1048
|
+
const otherRtc = this.sessionManager.getRtc(id);
|
|
1049
|
+
otherRtc?.hold();
|
|
1050
|
+
});
|
|
887
1051
|
const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
|
|
888
1052
|
upsertSessionState(this.state, sessionId, {
|
|
889
1053
|
direction: e.originator,
|
|
@@ -895,6 +1059,530 @@ var SessionLifecycle = class {
|
|
|
895
1059
|
});
|
|
896
1060
|
this.emit("newRTCSession", e);
|
|
897
1061
|
}
|
|
1062
|
+
bindLocalOutgoingAudio(sessionId, session) {
|
|
1063
|
+
const maxAttempts = 50;
|
|
1064
|
+
const retryDelayMs = 500;
|
|
1065
|
+
let attempts = 0;
|
|
1066
|
+
let retryScheduled = false;
|
|
1067
|
+
let retryTimer = null;
|
|
1068
|
+
let stopped = false;
|
|
1069
|
+
let exhausted = false;
|
|
1070
|
+
let exhaustedCheckUsed = false;
|
|
1071
|
+
let attachedPc = null;
|
|
1072
|
+
const logLocalAudioError = (message, pc, extra) => {
|
|
1073
|
+
sipDebugLogger.logLocalAudioError(sessionId, message, pc, extra);
|
|
1074
|
+
};
|
|
1075
|
+
const tryBindFromPc = (pc) => {
|
|
1076
|
+
if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
|
|
1080
|
+
const audioTrack = audioSender?.track;
|
|
1081
|
+
if (!audioTrack) {
|
|
1082
|
+
logLocalAudioError(
|
|
1083
|
+
"[sip] outgoing audio bind failed: no audio track",
|
|
1084
|
+
pc
|
|
1085
|
+
);
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
const outgoingStream = new MediaStream([audioTrack]);
|
|
1089
|
+
this.sessionManager.setSessionMedia(sessionId, outgoingStream);
|
|
1090
|
+
return true;
|
|
1091
|
+
};
|
|
1092
|
+
const onPcStateChange = () => {
|
|
1093
|
+
if (stopped)
|
|
1094
|
+
return;
|
|
1095
|
+
if (exhausted) {
|
|
1096
|
+
if (exhaustedCheckUsed)
|
|
1097
|
+
return;
|
|
1098
|
+
exhaustedCheckUsed = true;
|
|
1099
|
+
if (tryBindFromPc(attachedPc))
|
|
1100
|
+
stopRetry();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (tryBindFromPc(attachedPc))
|
|
1104
|
+
stopRetry();
|
|
1105
|
+
};
|
|
1106
|
+
const attachPcListeners = (pc) => {
|
|
1107
|
+
if (!pc || pc === attachedPc)
|
|
1108
|
+
return;
|
|
1109
|
+
if (attachedPc) {
|
|
1110
|
+
attachedPc.removeEventListener?.(
|
|
1111
|
+
"signalingstatechange",
|
|
1112
|
+
onPcStateChange
|
|
1113
|
+
);
|
|
1114
|
+
attachedPc.removeEventListener?.(
|
|
1115
|
+
"connectionstatechange",
|
|
1116
|
+
onPcStateChange
|
|
1117
|
+
);
|
|
1118
|
+
attachedPc.removeEventListener?.(
|
|
1119
|
+
"iceconnectionstatechange",
|
|
1120
|
+
onPcStateChange
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
attachedPc = pc;
|
|
1124
|
+
attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
|
|
1125
|
+
attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
|
|
1126
|
+
attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
|
|
1127
|
+
};
|
|
1128
|
+
const clearRetryTimer = () => {
|
|
1129
|
+
if (!retryTimer)
|
|
1130
|
+
return;
|
|
1131
|
+
clearTimeout(retryTimer);
|
|
1132
|
+
retryTimer = null;
|
|
1133
|
+
};
|
|
1134
|
+
const stopRetry = () => {
|
|
1135
|
+
if (stopped)
|
|
1136
|
+
return;
|
|
1137
|
+
stopped = true;
|
|
1138
|
+
clearRetryTimer();
|
|
1139
|
+
if (attachedPc) {
|
|
1140
|
+
attachedPc.removeEventListener?.(
|
|
1141
|
+
"signalingstatechange",
|
|
1142
|
+
onPcStateChange
|
|
1143
|
+
);
|
|
1144
|
+
attachedPc.removeEventListener?.(
|
|
1145
|
+
"connectionstatechange",
|
|
1146
|
+
onPcStateChange
|
|
1147
|
+
);
|
|
1148
|
+
attachedPc.removeEventListener?.(
|
|
1149
|
+
"iceconnectionstatechange",
|
|
1150
|
+
onPcStateChange
|
|
1151
|
+
);
|
|
1152
|
+
attachedPc = null;
|
|
1153
|
+
}
|
|
1154
|
+
session.off?.("peerconnection", onPeer);
|
|
1155
|
+
session.off?.("confirmed", onConfirmed);
|
|
1156
|
+
session.off?.("ended", stopRetry);
|
|
1157
|
+
session.off?.("failed", stopRetry);
|
|
1158
|
+
};
|
|
1159
|
+
const scheduleRetry = (pc) => {
|
|
1160
|
+
if (stopped || retryScheduled || exhausted)
|
|
1161
|
+
return;
|
|
1162
|
+
if (attempts >= maxAttempts) {
|
|
1163
|
+
logLocalAudioError(
|
|
1164
|
+
"[sip] outgoing audio bind failed: max retries reached",
|
|
1165
|
+
pc,
|
|
1166
|
+
{ attempts }
|
|
1167
|
+
);
|
|
1168
|
+
exhausted = true;
|
|
1169
|
+
clearRetryTimer();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (!pc) {
|
|
1173
|
+
logLocalAudioError(
|
|
1174
|
+
"[sip] outgoing audio bind failed: missing peerconnection",
|
|
1175
|
+
pc
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
retryScheduled = true;
|
|
1179
|
+
attempts += 1;
|
|
1180
|
+
retryTimer = setTimeout(() => {
|
|
1181
|
+
retryScheduled = false;
|
|
1182
|
+
retryTimer = null;
|
|
1183
|
+
if (tryBindFromPc(pc)) {
|
|
1184
|
+
stopRetry();
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
scheduleRetry(pc);
|
|
1188
|
+
}, retryDelayMs);
|
|
1189
|
+
};
|
|
1190
|
+
const onPeer = (data) => {
|
|
1191
|
+
if (stopped)
|
|
1192
|
+
return;
|
|
1193
|
+
attachPcListeners(data.peerconnection);
|
|
1194
|
+
if (exhausted) {
|
|
1195
|
+
if (exhaustedCheckUsed)
|
|
1196
|
+
return;
|
|
1197
|
+
exhaustedCheckUsed = true;
|
|
1198
|
+
if (tryBindFromPc(data.peerconnection))
|
|
1199
|
+
stopRetry();
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (tryBindFromPc(data.peerconnection)) {
|
|
1203
|
+
stopRetry();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
scheduleRetry(data.peerconnection);
|
|
1207
|
+
};
|
|
1208
|
+
const onConfirmed = () => {
|
|
1209
|
+
if (stopped)
|
|
1210
|
+
return;
|
|
1211
|
+
const currentPc = session?.connection ?? attachedPc;
|
|
1212
|
+
if (exhausted) {
|
|
1213
|
+
if (exhaustedCheckUsed)
|
|
1214
|
+
return;
|
|
1215
|
+
exhaustedCheckUsed = true;
|
|
1216
|
+
if (tryBindFromPc(currentPc))
|
|
1217
|
+
stopRetry();
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (tryBindFromPc(currentPc)) {
|
|
1221
|
+
stopRetry();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
scheduleRetry(currentPc);
|
|
1225
|
+
};
|
|
1226
|
+
const existingPc = session?.connection;
|
|
1227
|
+
if (!tryBindFromPc(existingPc)) {
|
|
1228
|
+
if (existingPc) {
|
|
1229
|
+
attachPcListeners(existingPc);
|
|
1230
|
+
scheduleRetry(existingPc);
|
|
1231
|
+
}
|
|
1232
|
+
session.on?.("peerconnection", onPeer);
|
|
1233
|
+
}
|
|
1234
|
+
session.on?.("confirmed", onConfirmed);
|
|
1235
|
+
session.on?.("ended", stopRetry);
|
|
1236
|
+
session.on?.("failed", stopRetry);
|
|
1237
|
+
}
|
|
1238
|
+
bindRemoteIncomingAudio(sessionId, session) {
|
|
1239
|
+
const maxAttempts = 50;
|
|
1240
|
+
const retryDelayMs = 500;
|
|
1241
|
+
let attempts = 0;
|
|
1242
|
+
let retryScheduled = false;
|
|
1243
|
+
let retryTimer = null;
|
|
1244
|
+
let stopped = false;
|
|
1245
|
+
let exhausted = false;
|
|
1246
|
+
let exhaustedCheckUsed = false;
|
|
1247
|
+
let attachedPc = null;
|
|
1248
|
+
let attachedTrack = null;
|
|
1249
|
+
const logRemoteAudioError = (message, pc, extra) => {
|
|
1250
|
+
sipDebugLogger.logRemoteAudioError(sessionId, message, pc, extra);
|
|
1251
|
+
};
|
|
1252
|
+
const logMissingReceiver = (pc, note) => {
|
|
1253
|
+
logRemoteAudioError(
|
|
1254
|
+
"[sip] incoming audio bind failed: no remote track",
|
|
1255
|
+
pc,
|
|
1256
|
+
{ note }
|
|
1257
|
+
);
|
|
1258
|
+
};
|
|
1259
|
+
const getRemoteAudioTrack = (pc) => {
|
|
1260
|
+
const receiver = pc?.getReceivers?.()?.find((r) => r.track?.kind === "audio");
|
|
1261
|
+
return receiver?.track ?? null;
|
|
1262
|
+
};
|
|
1263
|
+
const attachTrackListeners = (track) => {
|
|
1264
|
+
if (!track || track === attachedTrack)
|
|
1265
|
+
return;
|
|
1266
|
+
if (attachedTrack) {
|
|
1267
|
+
attachedTrack.removeEventListener?.("ended", onRemoteEnded);
|
|
1268
|
+
attachedTrack.removeEventListener?.("mute", onRemoteMuted);
|
|
1269
|
+
}
|
|
1270
|
+
attachedTrack = track;
|
|
1271
|
+
attachedTrack.addEventListener?.("ended", onRemoteEnded);
|
|
1272
|
+
attachedTrack.addEventListener?.("mute", onRemoteMuted);
|
|
1273
|
+
};
|
|
1274
|
+
const checkRemoteTrack = (pc) => {
|
|
1275
|
+
if (stopped || !pc)
|
|
1276
|
+
return false;
|
|
1277
|
+
const track = getRemoteAudioTrack(pc);
|
|
1278
|
+
if (!track)
|
|
1279
|
+
return false;
|
|
1280
|
+
attachTrackListeners(track);
|
|
1281
|
+
if (track.readyState !== "live") {
|
|
1282
|
+
logRemoteAudioError("[sip] incoming audio track not live", pc, {
|
|
1283
|
+
trackState: track.readyState
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
return true;
|
|
1287
|
+
};
|
|
1288
|
+
const onRemoteEnded = () => {
|
|
1289
|
+
logRemoteAudioError("[sip] incoming audio track ended", attachedPc);
|
|
1290
|
+
};
|
|
1291
|
+
const onRemoteMuted = () => {
|
|
1292
|
+
logRemoteAudioError("[sip] incoming audio track muted", attachedPc);
|
|
1293
|
+
};
|
|
1294
|
+
const onPcStateChange = () => {
|
|
1295
|
+
if (stopped)
|
|
1296
|
+
return;
|
|
1297
|
+
if (exhausted) {
|
|
1298
|
+
if (exhaustedCheckUsed)
|
|
1299
|
+
return;
|
|
1300
|
+
exhaustedCheckUsed = true;
|
|
1301
|
+
if (checkRemoteTrack(attachedPc))
|
|
1302
|
+
stopRetry();
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (checkRemoteTrack(attachedPc))
|
|
1306
|
+
stopRetry();
|
|
1307
|
+
};
|
|
1308
|
+
const attachPcListeners = (pc) => {
|
|
1309
|
+
if (!pc || pc === attachedPc)
|
|
1310
|
+
return;
|
|
1311
|
+
if (attachedPc) {
|
|
1312
|
+
attachedPc.removeEventListener?.(
|
|
1313
|
+
"signalingstatechange",
|
|
1314
|
+
onPcStateChange
|
|
1315
|
+
);
|
|
1316
|
+
attachedPc.removeEventListener?.(
|
|
1317
|
+
"connectionstatechange",
|
|
1318
|
+
onPcStateChange
|
|
1319
|
+
);
|
|
1320
|
+
attachedPc.removeEventListener?.(
|
|
1321
|
+
"iceconnectionstatechange",
|
|
1322
|
+
onPcStateChange
|
|
1323
|
+
);
|
|
1324
|
+
attachedPc.removeEventListener?.("track", onTrack);
|
|
1325
|
+
}
|
|
1326
|
+
attachedPc = pc;
|
|
1327
|
+
attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
|
|
1328
|
+
attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
|
|
1329
|
+
attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
|
|
1330
|
+
attachedPc.addEventListener?.("track", onTrack);
|
|
1331
|
+
};
|
|
1332
|
+
const clearRetryTimer = () => {
|
|
1333
|
+
if (!retryTimer)
|
|
1334
|
+
return;
|
|
1335
|
+
clearTimeout(retryTimer);
|
|
1336
|
+
retryTimer = null;
|
|
1337
|
+
};
|
|
1338
|
+
const stopRetry = () => {
|
|
1339
|
+
if (stopped)
|
|
1340
|
+
return;
|
|
1341
|
+
stopped = true;
|
|
1342
|
+
clearRetryTimer();
|
|
1343
|
+
if (attachedPc) {
|
|
1344
|
+
attachedPc.removeEventListener?.(
|
|
1345
|
+
"signalingstatechange",
|
|
1346
|
+
onPcStateChange
|
|
1347
|
+
);
|
|
1348
|
+
attachedPc.removeEventListener?.(
|
|
1349
|
+
"connectionstatechange",
|
|
1350
|
+
onPcStateChange
|
|
1351
|
+
);
|
|
1352
|
+
attachedPc.removeEventListener?.(
|
|
1353
|
+
"iceconnectionstatechange",
|
|
1354
|
+
onPcStateChange
|
|
1355
|
+
);
|
|
1356
|
+
attachedPc.removeEventListener?.("track", onTrack);
|
|
1357
|
+
attachedPc = null;
|
|
1358
|
+
}
|
|
1359
|
+
if (attachedTrack) {
|
|
1360
|
+
attachedTrack.removeEventListener?.("ended", onRemoteEnded);
|
|
1361
|
+
attachedTrack.removeEventListener?.("mute", onRemoteMuted);
|
|
1362
|
+
attachedTrack = null;
|
|
1363
|
+
}
|
|
1364
|
+
session.off?.("peerconnection", onPeer);
|
|
1365
|
+
session.off?.("confirmed", onConfirmed);
|
|
1366
|
+
session.off?.("ended", stopRetry);
|
|
1367
|
+
session.off?.("failed", stopRetry);
|
|
1368
|
+
};
|
|
1369
|
+
const scheduleRetry = (pc) => {
|
|
1370
|
+
if (stopped || retryScheduled || exhausted)
|
|
1371
|
+
return;
|
|
1372
|
+
if (attempts >= maxAttempts) {
|
|
1373
|
+
logRemoteAudioError(
|
|
1374
|
+
"[sip] incoming audio bind failed: max retries reached",
|
|
1375
|
+
pc,
|
|
1376
|
+
{ attempts }
|
|
1377
|
+
);
|
|
1378
|
+
exhausted = true;
|
|
1379
|
+
clearRetryTimer();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
retryScheduled = true;
|
|
1383
|
+
attempts += 1;
|
|
1384
|
+
retryTimer = setTimeout(() => {
|
|
1385
|
+
retryScheduled = false;
|
|
1386
|
+
retryTimer = null;
|
|
1387
|
+
if (checkRemoteTrack(pc)) {
|
|
1388
|
+
stopRetry();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (!pc)
|
|
1392
|
+
logMissingReceiver(pc, "missing peerconnection");
|
|
1393
|
+
scheduleRetry(pc);
|
|
1394
|
+
}, retryDelayMs);
|
|
1395
|
+
};
|
|
1396
|
+
const onTrack = () => {
|
|
1397
|
+
if (stopped)
|
|
1398
|
+
return;
|
|
1399
|
+
if (exhausted) {
|
|
1400
|
+
if (exhaustedCheckUsed)
|
|
1401
|
+
return;
|
|
1402
|
+
exhaustedCheckUsed = true;
|
|
1403
|
+
if (checkRemoteTrack(attachedPc))
|
|
1404
|
+
stopRetry();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (checkRemoteTrack(attachedPc))
|
|
1408
|
+
stopRetry();
|
|
1409
|
+
};
|
|
1410
|
+
const onPeer = (data) => {
|
|
1411
|
+
if (stopped)
|
|
1412
|
+
return;
|
|
1413
|
+
attachPcListeners(data.peerconnection);
|
|
1414
|
+
if (exhausted) {
|
|
1415
|
+
if (exhaustedCheckUsed)
|
|
1416
|
+
return;
|
|
1417
|
+
exhaustedCheckUsed = true;
|
|
1418
|
+
if (checkRemoteTrack(data.peerconnection))
|
|
1419
|
+
stopRetry();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (checkRemoteTrack(data.peerconnection)) {
|
|
1423
|
+
stopRetry();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
scheduleRetry(data.peerconnection);
|
|
1427
|
+
};
|
|
1428
|
+
const onConfirmed = () => {
|
|
1429
|
+
if (stopped)
|
|
1430
|
+
return;
|
|
1431
|
+
const currentPc = session?.connection ?? attachedPc;
|
|
1432
|
+
if (exhausted) {
|
|
1433
|
+
if (exhaustedCheckUsed)
|
|
1434
|
+
return;
|
|
1435
|
+
exhaustedCheckUsed = true;
|
|
1436
|
+
if (checkRemoteTrack(currentPc))
|
|
1437
|
+
stopRetry();
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
if (checkRemoteTrack(currentPc)) {
|
|
1441
|
+
stopRetry();
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
logMissingReceiver(currentPc, "confirmed without remote track");
|
|
1445
|
+
scheduleRetry(currentPc);
|
|
1446
|
+
};
|
|
1447
|
+
const existingPc = session?.connection;
|
|
1448
|
+
if (!checkRemoteTrack(existingPc)) {
|
|
1449
|
+
if (existingPc) {
|
|
1450
|
+
attachPcListeners(existingPc);
|
|
1451
|
+
scheduleRetry(existingPc);
|
|
1452
|
+
}
|
|
1453
|
+
session.on?.("peerconnection", onPeer);
|
|
1454
|
+
}
|
|
1455
|
+
session.on?.("confirmed", onConfirmed);
|
|
1456
|
+
session.on?.("ended", stopRetry);
|
|
1457
|
+
session.on?.("failed", stopRetry);
|
|
1458
|
+
}
|
|
1459
|
+
attachCallStatsLogging(sessionId, session) {
|
|
1460
|
+
const onConfirmed = () => {
|
|
1461
|
+
sipDebugLogger.startCallStatsLogging(sessionId, session);
|
|
1462
|
+
};
|
|
1463
|
+
const onEnd = () => {
|
|
1464
|
+
sipDebugLogger.stopCallStatsLogging(sessionId);
|
|
1465
|
+
};
|
|
1466
|
+
session.on?.("confirmed", onConfirmed);
|
|
1467
|
+
session.on?.("ended", onEnd);
|
|
1468
|
+
session.on?.("failed", onEnd);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// src/jssip-lib/sip/micRecovery.ts
|
|
1473
|
+
var MicRecoveryManager = class {
|
|
1474
|
+
constructor(deps) {
|
|
1475
|
+
this.enabled = false;
|
|
1476
|
+
this.defaults = {
|
|
1477
|
+
intervalMs: 2e3,
|
|
1478
|
+
maxRetries: Infinity
|
|
1479
|
+
};
|
|
1480
|
+
this.active = /* @__PURE__ */ new Map();
|
|
1481
|
+
this.deps = deps;
|
|
1482
|
+
}
|
|
1483
|
+
configure(config) {
|
|
1484
|
+
if (typeof config.enabled === "boolean") {
|
|
1485
|
+
this.enabled = config.enabled;
|
|
1486
|
+
}
|
|
1487
|
+
if (typeof config.intervalMs === "number") {
|
|
1488
|
+
this.defaults.intervalMs = config.intervalMs;
|
|
1489
|
+
}
|
|
1490
|
+
if (typeof config.maxRetries === "number") {
|
|
1491
|
+
this.defaults.maxRetries = config.maxRetries;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
enable(sessionId, options = {}) {
|
|
1495
|
+
if (!this.enabled)
|
|
1496
|
+
return () => {
|
|
1497
|
+
};
|
|
1498
|
+
this.disable(sessionId);
|
|
1499
|
+
const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
|
|
1500
|
+
const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
|
|
1501
|
+
let retries = 0;
|
|
1502
|
+
let stopped = false;
|
|
1503
|
+
const startedAt = Date.now();
|
|
1504
|
+
const warmupMs = Math.max(intervalMs * 2, 2e3);
|
|
1505
|
+
const tick = async () => {
|
|
1506
|
+
if (stopped || retries >= maxRetries)
|
|
1507
|
+
return;
|
|
1508
|
+
const rtc = this.deps.getRtc(sessionId);
|
|
1509
|
+
const session2 = this.deps.getSession(sessionId);
|
|
1510
|
+
if (!rtc || !session2)
|
|
1511
|
+
return;
|
|
1512
|
+
const sessionState = this.deps.getSessionState(sessionId);
|
|
1513
|
+
if (sessionState?.muted)
|
|
1514
|
+
return;
|
|
1515
|
+
const stream = rtc.mediaStream;
|
|
1516
|
+
const track = stream?.getAudioTracks?.()[0];
|
|
1517
|
+
const pc2 = session2?.connection;
|
|
1518
|
+
const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
|
|
1519
|
+
if (!track && !sender)
|
|
1520
|
+
return;
|
|
1521
|
+
if (Date.now() - startedAt < warmupMs)
|
|
1522
|
+
return;
|
|
1523
|
+
if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const trackLive = track?.readyState === "live";
|
|
1527
|
+
const senderLive = sender?.track?.readyState === "live";
|
|
1528
|
+
if (trackLive && senderLive)
|
|
1529
|
+
return;
|
|
1530
|
+
sipDebugLogger.logMicRecoveryDrop({
|
|
1531
|
+
sessionId,
|
|
1532
|
+
trackLive,
|
|
1533
|
+
senderLive
|
|
1534
|
+
});
|
|
1535
|
+
retries += 1;
|
|
1536
|
+
if (trackLive && !senderLive && track) {
|
|
1537
|
+
await rtc.replaceAudioTrack(track);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
let nextStream;
|
|
1541
|
+
try {
|
|
1542
|
+
const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
|
|
1543
|
+
nextStream = await this.deps.requestMicrophoneStream(deviceId);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
console.warn("[sip] mic recovery failed to get stream", err);
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const nextTrack = nextStream.getAudioTracks()[0];
|
|
1549
|
+
if (!nextTrack)
|
|
1550
|
+
return;
|
|
1551
|
+
await rtc.replaceAudioTrack(nextTrack);
|
|
1552
|
+
this.deps.setSessionMedia(sessionId, nextStream);
|
|
1553
|
+
};
|
|
1554
|
+
const timer = setInterval(() => {
|
|
1555
|
+
void tick();
|
|
1556
|
+
}, intervalMs);
|
|
1557
|
+
void tick();
|
|
1558
|
+
const session = this.deps.getSession(sessionId);
|
|
1559
|
+
const pc = session?.connection;
|
|
1560
|
+
const onIceChange = () => {
|
|
1561
|
+
const state = pc?.iceConnectionState;
|
|
1562
|
+
if (state === "failed" || state === "disconnected")
|
|
1563
|
+
void tick();
|
|
1564
|
+
};
|
|
1565
|
+
pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1566
|
+
const stop = () => {
|
|
1567
|
+
stopped = true;
|
|
1568
|
+
clearInterval(timer);
|
|
1569
|
+
pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1570
|
+
};
|
|
1571
|
+
this.active.set(sessionId, { stop });
|
|
1572
|
+
return stop;
|
|
1573
|
+
}
|
|
1574
|
+
disable(sessionId) {
|
|
1575
|
+
const entry = this.active.get(sessionId);
|
|
1576
|
+
if (!entry)
|
|
1577
|
+
return false;
|
|
1578
|
+
entry.stop();
|
|
1579
|
+
this.active.delete(sessionId);
|
|
1580
|
+
return true;
|
|
1581
|
+
}
|
|
1582
|
+
cleanupAll() {
|
|
1583
|
+
this.active.forEach((entry) => entry.stop());
|
|
1584
|
+
this.active.clear();
|
|
1585
|
+
}
|
|
898
1586
|
};
|
|
899
1587
|
|
|
900
1588
|
// src/jssip-lib/sip/client.ts
|
|
@@ -907,12 +1595,6 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
907
1595
|
this.sessionHandlers = /* @__PURE__ */ new Map();
|
|
908
1596
|
this.maxSessionCount = Infinity;
|
|
909
1597
|
this.sessionManager = new SessionManager();
|
|
910
|
-
this.micRecovery = /* @__PURE__ */ new Map();
|
|
911
|
-
this.micRecoveryEnabled = false;
|
|
912
|
-
this.micRecoveryDefaults = {
|
|
913
|
-
intervalMs: 2e3,
|
|
914
|
-
maxRetries: Infinity
|
|
915
|
-
};
|
|
916
1598
|
this.errorHandler = options.errorHandler ?? new SipErrorHandler({
|
|
917
1599
|
formatter: options.formatError,
|
|
918
1600
|
messages: options.errorMessages
|
|
@@ -934,6 +1616,14 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
934
1616
|
attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
|
|
935
1617
|
getMaxSessionCount: () => this.maxSessionCount
|
|
936
1618
|
});
|
|
1619
|
+
this.micRecovery = new MicRecoveryManager({
|
|
1620
|
+
getRtc: (sessionId) => this.sessionManager.getRtc(sessionId),
|
|
1621
|
+
getSession: (sessionId) => this.sessionManager.getSession(sessionId),
|
|
1622
|
+
getSessionState: (sessionId) => this.stateStore.getState().sessions.find((s) => s.id === sessionId),
|
|
1623
|
+
setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream),
|
|
1624
|
+
emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
|
|
1625
|
+
requestMicrophoneStream: (deviceId) => this.requestMicrophoneStreamInternal(deviceId)
|
|
1626
|
+
});
|
|
937
1627
|
if (typeof window !== "undefined") {
|
|
938
1628
|
window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
|
|
939
1629
|
}
|
|
@@ -950,20 +1640,17 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
950
1640
|
micRecoveryIntervalMs,
|
|
951
1641
|
micRecoveryMaxRetries,
|
|
952
1642
|
maxSessionCount,
|
|
953
|
-
pendingMediaTtlMs,
|
|
954
1643
|
...uaCfg
|
|
955
1644
|
} = config;
|
|
956
1645
|
this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
|
|
957
|
-
this.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
|
|
963
|
-
}
|
|
964
|
-
this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
|
|
1646
|
+
this.micRecovery.configure({
|
|
1647
|
+
enabled: Boolean(enableMicRecovery),
|
|
1648
|
+
intervalMs: micRecoveryIntervalMs,
|
|
1649
|
+
maxRetries: micRecoveryMaxRetries
|
|
1650
|
+
});
|
|
965
1651
|
const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
|
|
966
1652
|
this.userAgent.start(uri, password, uaCfg, { debug });
|
|
1653
|
+
this.lifecycle.setDebugEnabled(Boolean(debug));
|
|
967
1654
|
this.attachUAHandlers();
|
|
968
1655
|
this.attachBeforeUnload();
|
|
969
1656
|
this.syncDebugInspector(debug);
|
|
@@ -981,10 +1668,15 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
981
1668
|
call(target, callOptions = {}) {
|
|
982
1669
|
try {
|
|
983
1670
|
const opts = this.ensureMediaConstraints(callOptions);
|
|
984
|
-
if (opts.mediaStream)
|
|
985
|
-
this.sessionManager.enqueueOutgoingMedia(opts.mediaStream);
|
|
986
1671
|
const ua = this.userAgent.getUA();
|
|
987
|
-
ua?.call(target, opts);
|
|
1672
|
+
const session = ua?.call(target, opts);
|
|
1673
|
+
if (session && opts.mediaStream) {
|
|
1674
|
+
const sessionId = String(session?.id ?? "");
|
|
1675
|
+
if (sessionId) {
|
|
1676
|
+
this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
|
|
1677
|
+
this.sessionManager.setSession(sessionId, session);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
988
1680
|
} catch (e) {
|
|
989
1681
|
const err = this.emitError(e, "CALL_FAILED", "call failed");
|
|
990
1682
|
this.cleanupAllSessions();
|
|
@@ -1039,6 +1731,7 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1039
1731
|
setDebug(debug) {
|
|
1040
1732
|
this.debugPattern = debug;
|
|
1041
1733
|
this.userAgent.setDebug(debug);
|
|
1734
|
+
this.lifecycle.setDebugEnabled(Boolean(debug));
|
|
1042
1735
|
this.syncDebugInspector(debug);
|
|
1043
1736
|
}
|
|
1044
1737
|
attachSessionHandlers(sessionId, session) {
|
|
@@ -1074,14 +1767,13 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1074
1767
|
cleanupSession(sessionId, session) {
|
|
1075
1768
|
const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
|
|
1076
1769
|
this.detachSessionHandlers(sessionId, targetSession);
|
|
1077
|
-
this.
|
|
1770
|
+
this.micRecovery.disable(sessionId);
|
|
1078
1771
|
this.sessionManager.cleanupSession(sessionId);
|
|
1079
1772
|
removeSessionState(this.stateStore, sessionId);
|
|
1080
1773
|
}
|
|
1081
1774
|
cleanupAllSessions() {
|
|
1082
1775
|
this.sessionManager.cleanupAllSessions();
|
|
1083
|
-
this.micRecovery.
|
|
1084
|
-
this.micRecovery.clear();
|
|
1776
|
+
this.micRecovery.cleanupAll();
|
|
1085
1777
|
this.sessionHandlers.clear();
|
|
1086
1778
|
this.stateStore.setState({
|
|
1087
1779
|
sessions: [],
|
|
@@ -1095,12 +1787,9 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1095
1787
|
state: this.stateStore,
|
|
1096
1788
|
rtc,
|
|
1097
1789
|
detachSessionHandlers: () => this.cleanupSession(sessionId, session),
|
|
1790
|
+
emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
|
|
1098
1791
|
onSessionFailed: (err, event) => this.onSessionFailed(err, event),
|
|
1099
|
-
|
|
1100
|
-
if (this.micRecoveryEnabled) {
|
|
1101
|
-
this.enableMicrophoneRecovery(confirmedSessionId);
|
|
1102
|
-
}
|
|
1103
|
-
},
|
|
1792
|
+
enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
|
|
1104
1793
|
sessionId
|
|
1105
1794
|
});
|
|
1106
1795
|
}
|
|
@@ -1208,102 +1897,6 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1208
1897
|
setSessionMedia(sessionId, stream) {
|
|
1209
1898
|
this.sessionManager.setSessionMedia(sessionId, stream);
|
|
1210
1899
|
}
|
|
1211
|
-
enableMicrophoneRecovery(sessionId, options = {}) {
|
|
1212
|
-
const resolved = this.resolveExistingSessionId(sessionId);
|
|
1213
|
-
if (!resolved)
|
|
1214
|
-
return () => {
|
|
1215
|
-
};
|
|
1216
|
-
this.disableMicrophoneRecovery(resolved);
|
|
1217
|
-
const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
|
|
1218
|
-
const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
|
|
1219
|
-
let retries = 0;
|
|
1220
|
-
let stopped = false;
|
|
1221
|
-
const startedAt = Date.now();
|
|
1222
|
-
const warmupMs = Math.max(intervalMs * 2, 2e3);
|
|
1223
|
-
const tick = async () => {
|
|
1224
|
-
if (stopped || retries >= maxRetries)
|
|
1225
|
-
return;
|
|
1226
|
-
const rtc = this.sessionManager.getRtc(resolved);
|
|
1227
|
-
const session2 = this.sessionManager.getSession(resolved);
|
|
1228
|
-
if (!rtc || !session2)
|
|
1229
|
-
return;
|
|
1230
|
-
const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
|
|
1231
|
-
if (sessionState?.muted)
|
|
1232
|
-
return;
|
|
1233
|
-
const stream = rtc.mediaStream;
|
|
1234
|
-
const track = stream?.getAudioTracks?.()[0];
|
|
1235
|
-
const pc2 = session2?.connection;
|
|
1236
|
-
const sender = pc2?.getSenders?.().find((s) => s.track?.kind === "audio");
|
|
1237
|
-
if (!track && !sender)
|
|
1238
|
-
return;
|
|
1239
|
-
if (Date.now() - startedAt < warmupMs)
|
|
1240
|
-
return;
|
|
1241
|
-
if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
const trackLive = track?.readyState === "live";
|
|
1245
|
-
const senderLive = sender?.track?.readyState === "live";
|
|
1246
|
-
if (trackLive && senderLive)
|
|
1247
|
-
return;
|
|
1248
|
-
this.emitError(
|
|
1249
|
-
{
|
|
1250
|
-
cause: "microphone dropped",
|
|
1251
|
-
trackLive,
|
|
1252
|
-
senderLive
|
|
1253
|
-
},
|
|
1254
|
-
"MICROPHONE_DROPPED",
|
|
1255
|
-
"microphone dropped"
|
|
1256
|
-
);
|
|
1257
|
-
retries += 1;
|
|
1258
|
-
if (trackLive && !senderLive && track) {
|
|
1259
|
-
await rtc.replaceAudioTrack(track);
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
let nextStream;
|
|
1263
|
-
try {
|
|
1264
|
-
const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
|
|
1265
|
-
nextStream = await this.requestMicrophoneStreamInternal(deviceId);
|
|
1266
|
-
} catch (err) {
|
|
1267
|
-
console.warn("[sip] mic recovery failed to get stream", err);
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
const nextTrack = nextStream.getAudioTracks()[0];
|
|
1271
|
-
if (!nextTrack)
|
|
1272
|
-
return;
|
|
1273
|
-
await rtc.replaceAudioTrack(nextTrack);
|
|
1274
|
-
this.sessionManager.setSessionMedia(resolved, nextStream);
|
|
1275
|
-
};
|
|
1276
|
-
const timer = setInterval(() => {
|
|
1277
|
-
void tick();
|
|
1278
|
-
}, intervalMs);
|
|
1279
|
-
void tick();
|
|
1280
|
-
const session = this.sessionManager.getSession(resolved);
|
|
1281
|
-
const pc = session?.connection;
|
|
1282
|
-
const onIceChange = () => {
|
|
1283
|
-
const state = pc?.iceConnectionState;
|
|
1284
|
-
if (state === "failed" || state === "disconnected")
|
|
1285
|
-
void tick();
|
|
1286
|
-
};
|
|
1287
|
-
pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1288
|
-
const stop = () => {
|
|
1289
|
-
stopped = true;
|
|
1290
|
-
clearInterval(timer);
|
|
1291
|
-
pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1292
|
-
};
|
|
1293
|
-
this.micRecovery.set(resolved, { stop });
|
|
1294
|
-
return stop;
|
|
1295
|
-
}
|
|
1296
|
-
disableMicrophoneRecovery(sessionId) {
|
|
1297
|
-
const resolved = this.resolveExistingSessionId(sessionId);
|
|
1298
|
-
if (!resolved)
|
|
1299
|
-
return false;
|
|
1300
|
-
const entry = this.micRecovery.get(resolved);
|
|
1301
|
-
if (!entry)
|
|
1302
|
-
return false;
|
|
1303
|
-
entry.stop();
|
|
1304
|
-
this.micRecovery.delete(resolved);
|
|
1305
|
-
return true;
|
|
1306
|
-
}
|
|
1307
1900
|
switchCameraSession(sessionId, track) {
|
|
1308
1901
|
if (!this.sessionExists(sessionId))
|
|
1309
1902
|
return false;
|
|
@@ -1352,14 +1945,17 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1352
1945
|
syncDebugInspector(debug) {
|
|
1353
1946
|
if (typeof window === "undefined")
|
|
1354
1947
|
return;
|
|
1355
|
-
this.
|
|
1948
|
+
const persisted = this.getPersistedDebug();
|
|
1949
|
+
const effectiveDebug = debug ?? persisted ?? this.debugPattern;
|
|
1950
|
+
this.lifecycle.setDebugEnabled(Boolean(effectiveDebug));
|
|
1951
|
+
this.toggleStateLogger(Boolean(effectiveDebug));
|
|
1356
1952
|
const win = window;
|
|
1357
1953
|
const disabledInspector = () => {
|
|
1358
1954
|
console.warn("SIP debug inspector disabled; enable debug to inspect.");
|
|
1359
1955
|
return null;
|
|
1360
1956
|
};
|
|
1361
|
-
win.sipState = () =>
|
|
1362
|
-
win.sipSessions = () =>
|
|
1957
|
+
win.sipState = () => effectiveDebug ? this.stateStore.getState() : disabledInspector();
|
|
1958
|
+
win.sipSessions = () => effectiveDebug ? this.getSessions() : disabledInspector();
|
|
1363
1959
|
}
|
|
1364
1960
|
toggleStateLogger(enabled) {
|
|
1365
1961
|
if (!enabled) {
|
|
@@ -1372,22 +1968,10 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1372
1968
|
let prev = this.stateStore.getState();
|
|
1373
1969
|
console.info("[sip][state]", { initial: true }, prev);
|
|
1374
1970
|
this.stateLogOff = this.stateStore.onChange((next) => {
|
|
1375
|
-
|
|
1376
|
-
if (changes) {
|
|
1377
|
-
console.info("[sip][state]", changes, next);
|
|
1378
|
-
}
|
|
1971
|
+
console.info("[sip][state]", next);
|
|
1379
1972
|
prev = next;
|
|
1380
1973
|
});
|
|
1381
1974
|
}
|
|
1382
|
-
diffState(prev, next) {
|
|
1383
|
-
const changed = {};
|
|
1384
|
-
for (const key of Object.keys(next)) {
|
|
1385
|
-
if (prev[key] !== next[key]) {
|
|
1386
|
-
changed[key] = { from: prev[key], to: next[key] };
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
return Object.keys(changed).length ? changed : null;
|
|
1390
|
-
}
|
|
1391
1975
|
getPersistedDebug() {
|
|
1392
1976
|
if (typeof window === "undefined")
|
|
1393
1977
|
return void 0;
|
|
@@ -1469,8 +2053,6 @@ function useSipActions() {
|
|
|
1469
2053
|
getSessionIds: () => client.getSessionIds(),
|
|
1470
2054
|
getSessions: () => client.getSessions(),
|
|
1471
2055
|
setSessionMedia: (...args) => client.setSessionMedia(...args),
|
|
1472
|
-
enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
|
|
1473
|
-
disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
|
|
1474
2056
|
switchCamera: (...args) => client.switchCameraSession(...args),
|
|
1475
2057
|
enableVideo: (...args) => client.enableVideoSession(...args),
|
|
1476
2058
|
disableVideo: (...args) => client.disableVideoSession(...args)
|
|
@@ -1544,6 +2126,7 @@ function createCallPlayer(audioEl) {
|
|
|
1544
2126
|
return () => session.off("peerconnection", onPeer);
|
|
1545
2127
|
};
|
|
1546
2128
|
function bindToSession(session) {
|
|
2129
|
+
clearAudioStream(audioEl.srcObject);
|
|
1547
2130
|
if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
|
|
1548
2131
|
cleanupTrackListener = dispose(cleanupTrackListener);
|
|
1549
2132
|
cleanupTrackListener = attachTracks(session.connection);
|
|
@@ -1560,6 +2143,7 @@ function createCallPlayer(audioEl) {
|
|
|
1560
2143
|
const e = payload?.data;
|
|
1561
2144
|
cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
|
|
1562
2145
|
cleanupTrackListener = dispose(cleanupTrackListener);
|
|
2146
|
+
clearAudioStream(audioEl.srcObject);
|
|
1563
2147
|
if (!e?.session)
|
|
1564
2148
|
return;
|
|
1565
2149
|
cleanupSessionPeerListener = listenSessionPeerconnection(e.session);
|