react-jssip-kit 0.7.6 → 0.7.7
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 +250 -166
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -14
- package/dist/index.d.ts +0 -14
- package/dist/index.js +250 -166
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -57,10 +57,6 @@ type SipConfiguration = Omit<UAConfiguration, "password" | "uri"> & {
|
|
|
57
57
|
* Maximum allowed concurrent sessions. Additional sessions are rejected.
|
|
58
58
|
*/
|
|
59
59
|
maxSessionCount?: number;
|
|
60
|
-
/**
|
|
61
|
-
* Milliseconds to keep enqueued outgoing media before dropping. Defaults to 30000.
|
|
62
|
-
*/
|
|
63
|
-
pendingMediaTtlMs?: number;
|
|
64
60
|
};
|
|
65
61
|
|
|
66
62
|
type StartOpts = {
|
|
@@ -173,10 +169,6 @@ type SipClientOptions = {
|
|
|
173
169
|
errorHandler?: SipErrorHandler;
|
|
174
170
|
debug?: boolean | string;
|
|
175
171
|
};
|
|
176
|
-
type MicrophoneRecoveryOptions = {
|
|
177
|
-
intervalMs?: number;
|
|
178
|
-
maxRetries?: number;
|
|
179
|
-
};
|
|
180
172
|
declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
|
|
181
173
|
readonly userAgent: SipUserAgent;
|
|
182
174
|
readonly stateStore: SipStateStore;
|
|
@@ -189,8 +181,6 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
|
|
|
189
181
|
private sessionManager;
|
|
190
182
|
private lifecycle;
|
|
191
183
|
private micRecovery;
|
|
192
|
-
private micRecoveryEnabled;
|
|
193
|
-
private micRecoveryDefaults;
|
|
194
184
|
private unloadHandler?;
|
|
195
185
|
private stateLogOff?;
|
|
196
186
|
get state(): SipState;
|
|
@@ -229,8 +219,6 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
|
|
|
229
219
|
sendDTMFSession(sessionId: string, tones: string | number, options?: DTMFOptions): boolean;
|
|
230
220
|
transferSession(sessionId: string, target: string, options?: ReferOptions): boolean;
|
|
231
221
|
setSessionMedia(sessionId: string, stream: MediaStream): void;
|
|
232
|
-
enableMicrophoneRecovery(sessionId: string, options?: MicrophoneRecoveryOptions): () => void;
|
|
233
|
-
disableMicrophoneRecovery(sessionId: string): boolean;
|
|
234
222
|
switchCameraSession(sessionId: string, track: MediaStreamTrack): false | Promise<boolean>;
|
|
235
223
|
enableVideoSession(sessionId: string): boolean;
|
|
236
224
|
disableVideoSession(sessionId: string): boolean;
|
|
@@ -274,8 +262,6 @@ declare function useSipActions(): {
|
|
|
274
262
|
session: jssip_src_RTCSession.RTCSession;
|
|
275
263
|
}[];
|
|
276
264
|
setSessionMedia: (sessionId: string, stream: MediaStream) => void;
|
|
277
|
-
enableMicrophoneRecovery: (sessionId: string, options?: MicrophoneRecoveryOptions | undefined) => () => void;
|
|
278
|
-
disableMicrophoneRecovery: (sessionId: string) => boolean;
|
|
279
265
|
switchCamera: (sessionId: string, track: MediaStreamTrack) => false | Promise<boolean>;
|
|
280
266
|
enableVideo: (sessionId: string) => boolean;
|
|
281
267
|
disableVideo: (sessionId: string) => boolean;
|
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);
|
|
@@ -850,35 +844,102 @@ var SessionLifecycle = class {
|
|
|
850
844
|
}
|
|
851
845
|
handleNewRTCSession(e) {
|
|
852
846
|
const session = e.session;
|
|
853
|
-
const sessionId = String(
|
|
847
|
+
const sessionId = String(
|
|
848
|
+
session?.id ?? crypto.randomUUID?.() ?? Date.now()
|
|
849
|
+
);
|
|
854
850
|
const currentSessions = this.state.getState().sessions;
|
|
855
851
|
if (currentSessions.length >= this.getMaxSessionCount()) {
|
|
856
852
|
try {
|
|
857
|
-
session.terminate?.({
|
|
853
|
+
session.terminate?.({
|
|
854
|
+
status_code: 486,
|
|
855
|
+
reason_phrase: "Busy Here"
|
|
856
|
+
});
|
|
858
857
|
} catch {
|
|
859
858
|
}
|
|
860
859
|
if (e.originator === "remote") {
|
|
861
860
|
this.emit("missed", e);
|
|
861
|
+
} else {
|
|
862
|
+
this.emitError(
|
|
863
|
+
"max session count reached",
|
|
864
|
+
"MAX_SESSIONS_REACHED",
|
|
865
|
+
"max session count reached"
|
|
866
|
+
);
|
|
862
867
|
}
|
|
863
|
-
this.emitError("max session count reached", "MAX_SESSIONS_REACHED", "max session count reached");
|
|
864
868
|
return;
|
|
865
869
|
}
|
|
866
|
-
const outgoingMedia = e.originator === "local" ? this.sessionManager.dequeueOutgoingMedia() : null;
|
|
867
|
-
if (outgoingMedia)
|
|
868
|
-
this.sessionManager.setSessionMedia(sessionId, outgoingMedia);
|
|
869
870
|
const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
|
|
870
|
-
if (outgoingMedia)
|
|
871
|
-
rtc.setMediaStream(outgoingMedia);
|
|
872
871
|
this.sessionManager.setSession(sessionId, session);
|
|
873
872
|
this.attachSessionHandlers(sessionId, session);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
873
|
+
if (e.originator === "local" && !rtc.mediaStream) {
|
|
874
|
+
const maxAttempts = 5;
|
|
875
|
+
const retryDelayMs = 500;
|
|
876
|
+
let attempts = 0;
|
|
877
|
+
let retryScheduled = false;
|
|
878
|
+
let retryTimer = null;
|
|
879
|
+
let stopped = false;
|
|
880
|
+
const tryBindFromPc = (pc) => {
|
|
881
|
+
if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
|
|
885
|
+
const audioTrack = audioSender?.track;
|
|
886
|
+
if (!audioTrack)
|
|
887
|
+
return false;
|
|
888
|
+
const outgoingStream = new MediaStream([audioTrack]);
|
|
889
|
+
this.sessionManager.setSessionMedia(sessionId, outgoingStream);
|
|
890
|
+
return true;
|
|
891
|
+
};
|
|
892
|
+
const scheduleRetry = (pc) => {
|
|
893
|
+
if (stopped || retryScheduled || attempts >= maxAttempts) {
|
|
894
|
+
session.off?.("peerconnection", onPeer);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
retryScheduled = true;
|
|
898
|
+
attempts += 1;
|
|
899
|
+
retryTimer = setTimeout(() => {
|
|
900
|
+
retryScheduled = false;
|
|
901
|
+
retryTimer = null;
|
|
902
|
+
if (tryBindFromPc(pc)) {
|
|
903
|
+
session.off?.("peerconnection", onPeer);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
scheduleRetry(pc);
|
|
907
|
+
}, retryDelayMs);
|
|
908
|
+
};
|
|
909
|
+
const onPeer = (data) => {
|
|
910
|
+
if (stopped)
|
|
911
|
+
return;
|
|
912
|
+
if (tryBindFromPc(data.peerconnection)) {
|
|
913
|
+
session.off?.("peerconnection", onPeer);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
scheduleRetry(data.peerconnection);
|
|
917
|
+
};
|
|
918
|
+
const stopRetry = () => {
|
|
919
|
+
if (stopped)
|
|
920
|
+
return;
|
|
921
|
+
stopped = true;
|
|
922
|
+
if (retryTimer) {
|
|
923
|
+
clearTimeout(retryTimer);
|
|
924
|
+
retryTimer = null;
|
|
925
|
+
}
|
|
926
|
+
session.off?.("peerconnection", onPeer);
|
|
927
|
+
session.off?.("ended", stopRetry);
|
|
928
|
+
session.off?.("failed", stopRetry);
|
|
929
|
+
};
|
|
930
|
+
const existingPc = session?.connection;
|
|
931
|
+
if (!tryBindFromPc(existingPc)) {
|
|
932
|
+
if (existingPc)
|
|
933
|
+
scheduleRetry(existingPc);
|
|
934
|
+
session.on?.("peerconnection", onPeer);
|
|
880
935
|
}
|
|
881
|
-
|
|
936
|
+
session.on?.("ended", stopRetry);
|
|
937
|
+
session.on?.("failed", stopRetry);
|
|
938
|
+
}
|
|
939
|
+
holdOtherSessions(this.state, sessionId, (id) => {
|
|
940
|
+
const otherRtc = this.sessionManager.getRtc(id);
|
|
941
|
+
otherRtc?.hold();
|
|
942
|
+
});
|
|
882
943
|
const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
|
|
883
944
|
upsertSessionState(this.state, sessionId, {
|
|
884
945
|
direction: e.originator,
|
|
@@ -892,6 +953,126 @@ var SessionLifecycle = class {
|
|
|
892
953
|
}
|
|
893
954
|
};
|
|
894
955
|
|
|
956
|
+
// src/jssip-lib/sip/micRecovery.ts
|
|
957
|
+
var MicRecoveryManager = class {
|
|
958
|
+
constructor(deps) {
|
|
959
|
+
this.enabled = false;
|
|
960
|
+
this.defaults = {
|
|
961
|
+
intervalMs: 2e3,
|
|
962
|
+
maxRetries: Infinity
|
|
963
|
+
};
|
|
964
|
+
this.active = /* @__PURE__ */ new Map();
|
|
965
|
+
this.deps = deps;
|
|
966
|
+
}
|
|
967
|
+
configure(config) {
|
|
968
|
+
if (typeof config.enabled === "boolean") {
|
|
969
|
+
this.enabled = config.enabled;
|
|
970
|
+
}
|
|
971
|
+
if (typeof config.intervalMs === "number") {
|
|
972
|
+
this.defaults.intervalMs = config.intervalMs;
|
|
973
|
+
}
|
|
974
|
+
if (typeof config.maxRetries === "number") {
|
|
975
|
+
this.defaults.maxRetries = config.maxRetries;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
enable(sessionId, options = {}) {
|
|
979
|
+
if (!this.enabled)
|
|
980
|
+
return () => {
|
|
981
|
+
};
|
|
982
|
+
this.disable(sessionId);
|
|
983
|
+
const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
|
|
984
|
+
const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
|
|
985
|
+
let retries = 0;
|
|
986
|
+
let stopped = false;
|
|
987
|
+
const startedAt = Date.now();
|
|
988
|
+
const warmupMs = Math.max(intervalMs * 2, 2e3);
|
|
989
|
+
const tick = async () => {
|
|
990
|
+
if (stopped || retries >= maxRetries)
|
|
991
|
+
return;
|
|
992
|
+
const rtc = this.deps.getRtc(sessionId);
|
|
993
|
+
const session2 = this.deps.getSession(sessionId);
|
|
994
|
+
if (!rtc || !session2)
|
|
995
|
+
return;
|
|
996
|
+
const sessionState = this.deps.getSessionState(sessionId);
|
|
997
|
+
if (sessionState?.muted)
|
|
998
|
+
return;
|
|
999
|
+
const stream = rtc.mediaStream;
|
|
1000
|
+
const track = stream?.getAudioTracks?.()[0];
|
|
1001
|
+
const pc2 = session2?.connection;
|
|
1002
|
+
const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
|
|
1003
|
+
if (!track && !sender)
|
|
1004
|
+
return;
|
|
1005
|
+
if (Date.now() - startedAt < warmupMs)
|
|
1006
|
+
return;
|
|
1007
|
+
if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const trackLive = track?.readyState === "live";
|
|
1011
|
+
const senderLive = sender?.track?.readyState === "live";
|
|
1012
|
+
if (trackLive && senderLive)
|
|
1013
|
+
return;
|
|
1014
|
+
this.deps.emitError(
|
|
1015
|
+
{
|
|
1016
|
+
cause: "microphone dropped",
|
|
1017
|
+
trackLive,
|
|
1018
|
+
senderLive
|
|
1019
|
+
},
|
|
1020
|
+
"MICROPHONE_DROPPED",
|
|
1021
|
+
"microphone dropped"
|
|
1022
|
+
);
|
|
1023
|
+
retries += 1;
|
|
1024
|
+
if (trackLive && !senderLive && track) {
|
|
1025
|
+
await rtc.replaceAudioTrack(track);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
let nextStream;
|
|
1029
|
+
try {
|
|
1030
|
+
const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
|
|
1031
|
+
nextStream = await this.deps.requestMicrophoneStream(deviceId);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
console.warn("[sip] mic recovery failed to get stream", err);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const nextTrack = nextStream.getAudioTracks()[0];
|
|
1037
|
+
if (!nextTrack)
|
|
1038
|
+
return;
|
|
1039
|
+
await rtc.replaceAudioTrack(nextTrack);
|
|
1040
|
+
this.deps.setSessionMedia(sessionId, nextStream);
|
|
1041
|
+
};
|
|
1042
|
+
const timer = setInterval(() => {
|
|
1043
|
+
void tick();
|
|
1044
|
+
}, intervalMs);
|
|
1045
|
+
void tick();
|
|
1046
|
+
const session = this.deps.getSession(sessionId);
|
|
1047
|
+
const pc = session?.connection;
|
|
1048
|
+
const onIceChange = () => {
|
|
1049
|
+
const state = pc?.iceConnectionState;
|
|
1050
|
+
if (state === "failed" || state === "disconnected")
|
|
1051
|
+
void tick();
|
|
1052
|
+
};
|
|
1053
|
+
pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1054
|
+
const stop = () => {
|
|
1055
|
+
stopped = true;
|
|
1056
|
+
clearInterval(timer);
|
|
1057
|
+
pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1058
|
+
};
|
|
1059
|
+
this.active.set(sessionId, { stop });
|
|
1060
|
+
return stop;
|
|
1061
|
+
}
|
|
1062
|
+
disable(sessionId) {
|
|
1063
|
+
const entry = this.active.get(sessionId);
|
|
1064
|
+
if (!entry)
|
|
1065
|
+
return false;
|
|
1066
|
+
entry.stop();
|
|
1067
|
+
this.active.delete(sessionId);
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
cleanupAll() {
|
|
1071
|
+
this.active.forEach((entry) => entry.stop());
|
|
1072
|
+
this.active.clear();
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
895
1076
|
// src/jssip-lib/sip/client.ts
|
|
896
1077
|
var SESSION_DEBUG_KEY = "sip-debug-enabled";
|
|
897
1078
|
var SipClient = class extends EventTargetEmitter {
|
|
@@ -902,12 +1083,6 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
902
1083
|
this.sessionHandlers = /* @__PURE__ */ new Map();
|
|
903
1084
|
this.maxSessionCount = Infinity;
|
|
904
1085
|
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
1086
|
this.errorHandler = options.errorHandler ?? new SipErrorHandler({
|
|
912
1087
|
formatter: options.formatError,
|
|
913
1088
|
messages: options.errorMessages
|
|
@@ -929,6 +1104,14 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
929
1104
|
attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
|
|
930
1105
|
getMaxSessionCount: () => this.maxSessionCount
|
|
931
1106
|
});
|
|
1107
|
+
this.micRecovery = new MicRecoveryManager({
|
|
1108
|
+
getRtc: (sessionId) => this.sessionManager.getRtc(sessionId),
|
|
1109
|
+
getSession: (sessionId) => this.sessionManager.getSession(sessionId),
|
|
1110
|
+
getSessionState: (sessionId) => this.stateStore.getState().sessions.find((s) => s.id === sessionId),
|
|
1111
|
+
setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream),
|
|
1112
|
+
emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
|
|
1113
|
+
requestMicrophoneStream: (deviceId) => this.requestMicrophoneStreamInternal(deviceId)
|
|
1114
|
+
});
|
|
932
1115
|
if (typeof window !== "undefined") {
|
|
933
1116
|
window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
|
|
934
1117
|
}
|
|
@@ -945,18 +1128,14 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
945
1128
|
micRecoveryIntervalMs,
|
|
946
1129
|
micRecoveryMaxRetries,
|
|
947
1130
|
maxSessionCount,
|
|
948
|
-
pendingMediaTtlMs,
|
|
949
1131
|
...uaCfg
|
|
950
1132
|
} = config;
|
|
951
1133
|
this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
|
|
952
|
-
this.
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
|
|
958
|
-
}
|
|
959
|
-
this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
|
|
1134
|
+
this.micRecovery.configure({
|
|
1135
|
+
enabled: Boolean(enableMicRecovery),
|
|
1136
|
+
intervalMs: micRecoveryIntervalMs,
|
|
1137
|
+
maxRetries: micRecoveryMaxRetries
|
|
1138
|
+
});
|
|
960
1139
|
const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
|
|
961
1140
|
this.userAgent.start(uri, password, uaCfg, { debug });
|
|
962
1141
|
this.attachUAHandlers();
|
|
@@ -976,10 +1155,15 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
976
1155
|
call(target, callOptions = {}) {
|
|
977
1156
|
try {
|
|
978
1157
|
const opts = this.ensureMediaConstraints(callOptions);
|
|
979
|
-
if (opts.mediaStream)
|
|
980
|
-
this.sessionManager.enqueueOutgoingMedia(opts.mediaStream);
|
|
981
1158
|
const ua = this.userAgent.getUA();
|
|
982
|
-
ua?.call(target, opts);
|
|
1159
|
+
const session = ua?.call(target, opts);
|
|
1160
|
+
if (session && opts.mediaStream) {
|
|
1161
|
+
const sessionId = String(session?.id ?? "");
|
|
1162
|
+
if (sessionId) {
|
|
1163
|
+
this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
|
|
1164
|
+
this.sessionManager.setSession(sessionId, session);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
983
1167
|
} catch (e) {
|
|
984
1168
|
const err = this.emitError(e, "CALL_FAILED", "call failed");
|
|
985
1169
|
this.cleanupAllSessions();
|
|
@@ -1069,14 +1253,13 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1069
1253
|
cleanupSession(sessionId, session) {
|
|
1070
1254
|
const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
|
|
1071
1255
|
this.detachSessionHandlers(sessionId, targetSession);
|
|
1072
|
-
this.
|
|
1256
|
+
this.micRecovery.disable(sessionId);
|
|
1073
1257
|
this.sessionManager.cleanupSession(sessionId);
|
|
1074
1258
|
removeSessionState(this.stateStore, sessionId);
|
|
1075
1259
|
}
|
|
1076
1260
|
cleanupAllSessions() {
|
|
1077
1261
|
this.sessionManager.cleanupAllSessions();
|
|
1078
|
-
this.micRecovery.
|
|
1079
|
-
this.micRecovery.clear();
|
|
1262
|
+
this.micRecovery.cleanupAll();
|
|
1080
1263
|
this.sessionHandlers.clear();
|
|
1081
1264
|
this.stateStore.setState({
|
|
1082
1265
|
sessions: [],
|
|
@@ -1090,12 +1273,9 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1090
1273
|
state: this.stateStore,
|
|
1091
1274
|
rtc,
|
|
1092
1275
|
detachSessionHandlers: () => this.cleanupSession(sessionId, session),
|
|
1276
|
+
emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
|
|
1093
1277
|
onSessionFailed: (err, event) => this.onSessionFailed(err, event),
|
|
1094
|
-
|
|
1095
|
-
if (this.micRecoveryEnabled) {
|
|
1096
|
-
this.enableMicrophoneRecovery(confirmedSessionId);
|
|
1097
|
-
}
|
|
1098
|
-
},
|
|
1278
|
+
enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
|
|
1099
1279
|
sessionId
|
|
1100
1280
|
});
|
|
1101
1281
|
}
|
|
@@ -1203,102 +1383,6 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1203
1383
|
setSessionMedia(sessionId, stream) {
|
|
1204
1384
|
this.sessionManager.setSessionMedia(sessionId, stream);
|
|
1205
1385
|
}
|
|
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
1386
|
switchCameraSession(sessionId, track) {
|
|
1303
1387
|
if (!this.sessionExists(sessionId))
|
|
1304
1388
|
return false;
|
|
@@ -1464,8 +1548,6 @@ function useSipActions() {
|
|
|
1464
1548
|
getSessionIds: () => client.getSessionIds(),
|
|
1465
1549
|
getSessions: () => client.getSessions(),
|
|
1466
1550
|
setSessionMedia: (...args) => client.setSessionMedia(...args),
|
|
1467
|
-
enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
|
|
1468
|
-
disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
|
|
1469
1551
|
switchCamera: (...args) => client.switchCameraSession(...args),
|
|
1470
1552
|
enableVideo: (...args) => client.enableVideoSession(...args),
|
|
1471
1553
|
disableVideo: (...args) => client.disableVideoSession(...args)
|
|
@@ -1539,6 +1621,7 @@ function createCallPlayer(audioEl) {
|
|
|
1539
1621
|
return () => session.off("peerconnection", onPeer);
|
|
1540
1622
|
};
|
|
1541
1623
|
function bindToSession(session) {
|
|
1624
|
+
clearAudioStream(audioEl.srcObject);
|
|
1542
1625
|
if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
|
|
1543
1626
|
cleanupTrackListener = dispose(cleanupTrackListener);
|
|
1544
1627
|
cleanupTrackListener = attachTracks(session.connection);
|
|
@@ -1555,6 +1638,7 @@ function createCallPlayer(audioEl) {
|
|
|
1555
1638
|
const e = payload?.data;
|
|
1556
1639
|
cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
|
|
1557
1640
|
cleanupTrackListener = dispose(cleanupTrackListener);
|
|
1641
|
+
clearAudioStream(audioEl.srcObject);
|
|
1558
1642
|
if (!e?.session)
|
|
1559
1643
|
return;
|
|
1560
1644
|
cleanupSessionPeerListener = listenSessionPeerconnection(e.session);
|