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