react-jssip-kit 0.7.5 → 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 -162
- 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 -162
- 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 {
|
|
@@ -485,7 +484,6 @@ function createSessionHandlers(deps) {
|
|
|
485
484
|
},
|
|
486
485
|
accepted: (e) => {
|
|
487
486
|
emitter.emit("accepted", e);
|
|
488
|
-
onSessionAccepted?.(sessionId);
|
|
489
487
|
state.batchSet({
|
|
490
488
|
sessions: state.getState().sessions.map(
|
|
491
489
|
(s) => s.id === sessionId ? {
|
|
@@ -498,7 +496,7 @@ function createSessionHandlers(deps) {
|
|
|
498
496
|
},
|
|
499
497
|
confirmed: (e) => {
|
|
500
498
|
emitter.emit("confirmed", e);
|
|
501
|
-
|
|
499
|
+
deps.enableMicrophoneRecovery?.(sessionId);
|
|
502
500
|
},
|
|
503
501
|
ended: (e) => {
|
|
504
502
|
emitter.emit("ended", e);
|
|
@@ -610,8 +608,8 @@ var WebRTCSessionController = class {
|
|
|
610
608
|
}
|
|
611
609
|
cleanup(stopTracks = true) {
|
|
612
610
|
const pc = this.getPC();
|
|
611
|
+
const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
|
|
613
612
|
if (pc && typeof pc.getSenders === "function") {
|
|
614
|
-
const isClosed = pc.connectionState === "closed" || pc.signalingState === "closed";
|
|
615
613
|
if (!isClosed) {
|
|
616
614
|
for (const s of pc.getSenders()) {
|
|
617
615
|
try {
|
|
@@ -622,8 +620,14 @@ var WebRTCSessionController = class {
|
|
|
622
620
|
}
|
|
623
621
|
}
|
|
624
622
|
if (stopTracks && this.mediaStream) {
|
|
625
|
-
|
|
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;
|
|
626
629
|
t.stop();
|
|
630
|
+
}
|
|
627
631
|
}
|
|
628
632
|
this.mediaStream = null;
|
|
629
633
|
this.currentSession = null;
|
|
@@ -702,29 +706,14 @@ var WebRTCSessionController = class {
|
|
|
702
706
|
var SessionManager = class {
|
|
703
707
|
constructor() {
|
|
704
708
|
this.entries = /* @__PURE__ */ new Map();
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
enqueueOutgoingMedia(stream) {
|
|
713
|
-
this.pendingMediaQueue.push({ stream, addedAt: Date.now() });
|
|
714
|
-
}
|
|
715
|
-
dequeueOutgoingMedia() {
|
|
716
|
-
const now = Date.now();
|
|
717
|
-
while (this.pendingMediaQueue.length) {
|
|
718
|
-
const next = this.pendingMediaQueue.shift();
|
|
719
|
-
if (!next)
|
|
720
|
-
break;
|
|
721
|
-
if (now - next.addedAt <= this.pendingMediaTtlMs) {
|
|
722
|
-
return next.stream;
|
|
723
|
-
} else {
|
|
724
|
-
next.stream.getTracks().forEach((t) => t.stop());
|
|
725
|
-
}
|
|
709
|
+
}
|
|
710
|
+
stopMediaStream(stream) {
|
|
711
|
+
if (!stream)
|
|
712
|
+
return;
|
|
713
|
+
for (const t of stream.getTracks()) {
|
|
714
|
+
if (t.readyState !== "ended")
|
|
715
|
+
t.stop();
|
|
726
716
|
}
|
|
727
|
-
return null;
|
|
728
717
|
}
|
|
729
718
|
getOrCreateRtc(sessionId, session) {
|
|
730
719
|
let entry = this.entries.get(sessionId);
|
|
@@ -766,6 +755,9 @@ var SessionManager = class {
|
|
|
766
755
|
session: null,
|
|
767
756
|
media: null
|
|
768
757
|
};
|
|
758
|
+
if (entry.media && entry.media !== stream) {
|
|
759
|
+
this.stopMediaStream(entry.media);
|
|
760
|
+
}
|
|
769
761
|
entry.media = stream;
|
|
770
762
|
entry.rtc.setMediaStream(stream);
|
|
771
763
|
this.entries.set(sessionId, entry);
|
|
@@ -795,15 +787,16 @@ var SessionManager = class {
|
|
|
795
787
|
const entry = this.entries.get(sessionId);
|
|
796
788
|
if (entry) {
|
|
797
789
|
entry.rtc.cleanup();
|
|
790
|
+
this.stopMediaStream(entry.media);
|
|
798
791
|
this.entries.delete(sessionId);
|
|
799
792
|
}
|
|
800
793
|
}
|
|
801
794
|
cleanupAllSessions() {
|
|
802
795
|
for (const [, entry] of this.entries.entries()) {
|
|
803
796
|
entry.rtc.cleanup();
|
|
797
|
+
this.stopMediaStream(entry.media);
|
|
804
798
|
}
|
|
805
799
|
this.entries.clear();
|
|
806
|
-
this.pendingMediaQueue = [];
|
|
807
800
|
}
|
|
808
801
|
answer(sessionId, options) {
|
|
809
802
|
const rtc = this.getRtc(sessionId);
|
|
@@ -851,35 +844,102 @@ var SessionLifecycle = class {
|
|
|
851
844
|
}
|
|
852
845
|
handleNewRTCSession(e) {
|
|
853
846
|
const session = e.session;
|
|
854
|
-
const sessionId = String(
|
|
847
|
+
const sessionId = String(
|
|
848
|
+
session?.id ?? crypto.randomUUID?.() ?? Date.now()
|
|
849
|
+
);
|
|
855
850
|
const currentSessions = this.state.getState().sessions;
|
|
856
851
|
if (currentSessions.length >= this.getMaxSessionCount()) {
|
|
857
852
|
try {
|
|
858
|
-
session.terminate?.({
|
|
853
|
+
session.terminate?.({
|
|
854
|
+
status_code: 486,
|
|
855
|
+
reason_phrase: "Busy Here"
|
|
856
|
+
});
|
|
859
857
|
} catch {
|
|
860
858
|
}
|
|
861
859
|
if (e.originator === "remote") {
|
|
862
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
|
+
);
|
|
863
867
|
}
|
|
864
|
-
this.emitError("max session count reached", "MAX_SESSIONS_REACHED", "max session count reached");
|
|
865
868
|
return;
|
|
866
869
|
}
|
|
867
|
-
const outgoingMedia = e.originator === "local" ? this.sessionManager.dequeueOutgoingMedia() : null;
|
|
868
|
-
if (outgoingMedia)
|
|
869
|
-
this.sessionManager.setSessionMedia(sessionId, outgoingMedia);
|
|
870
870
|
const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
|
|
871
|
-
if (outgoingMedia)
|
|
872
|
-
rtc.setMediaStream(outgoingMedia);
|
|
873
871
|
this.sessionManager.setSession(sessionId, session);
|
|
874
872
|
this.attachSessionHandlers(sessionId, session);
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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);
|
|
881
935
|
}
|
|
882
|
-
|
|
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
|
+
});
|
|
883
943
|
const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
|
|
884
944
|
upsertSessionState(this.state, sessionId, {
|
|
885
945
|
direction: e.originator,
|
|
@@ -893,6 +953,126 @@ var SessionLifecycle = class {
|
|
|
893
953
|
}
|
|
894
954
|
};
|
|
895
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
|
+
|
|
896
1076
|
// src/jssip-lib/sip/client.ts
|
|
897
1077
|
var SESSION_DEBUG_KEY = "sip-debug-enabled";
|
|
898
1078
|
var SipClient = class extends EventTargetEmitter {
|
|
@@ -903,12 +1083,6 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
903
1083
|
this.sessionHandlers = /* @__PURE__ */ new Map();
|
|
904
1084
|
this.maxSessionCount = Infinity;
|
|
905
1085
|
this.sessionManager = new SessionManager();
|
|
906
|
-
this.micRecovery = /* @__PURE__ */ new Map();
|
|
907
|
-
this.micRecoveryEnabled = false;
|
|
908
|
-
this.micRecoveryDefaults = {
|
|
909
|
-
intervalMs: 2e3,
|
|
910
|
-
maxRetries: Infinity
|
|
911
|
-
};
|
|
912
1086
|
this.errorHandler = options.errorHandler ?? new SipErrorHandler({
|
|
913
1087
|
formatter: options.formatError,
|
|
914
1088
|
messages: options.errorMessages
|
|
@@ -930,6 +1104,14 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
930
1104
|
attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
|
|
931
1105
|
getMaxSessionCount: () => this.maxSessionCount
|
|
932
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
|
+
});
|
|
933
1115
|
if (typeof window !== "undefined") {
|
|
934
1116
|
window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
|
|
935
1117
|
}
|
|
@@ -946,18 +1128,14 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
946
1128
|
micRecoveryIntervalMs,
|
|
947
1129
|
micRecoveryMaxRetries,
|
|
948
1130
|
maxSessionCount,
|
|
949
|
-
pendingMediaTtlMs,
|
|
950
1131
|
...uaCfg
|
|
951
1132
|
} = config;
|
|
952
1133
|
this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
|
|
953
|
-
this.
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
|
|
959
|
-
}
|
|
960
|
-
this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
|
|
1134
|
+
this.micRecovery.configure({
|
|
1135
|
+
enabled: Boolean(enableMicRecovery),
|
|
1136
|
+
intervalMs: micRecoveryIntervalMs,
|
|
1137
|
+
maxRetries: micRecoveryMaxRetries
|
|
1138
|
+
});
|
|
961
1139
|
const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
|
|
962
1140
|
this.userAgent.start(uri, password, uaCfg, { debug });
|
|
963
1141
|
this.attachUAHandlers();
|
|
@@ -977,10 +1155,15 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
977
1155
|
call(target, callOptions = {}) {
|
|
978
1156
|
try {
|
|
979
1157
|
const opts = this.ensureMediaConstraints(callOptions);
|
|
980
|
-
if (opts.mediaStream)
|
|
981
|
-
this.sessionManager.enqueueOutgoingMedia(opts.mediaStream);
|
|
982
1158
|
const ua = this.userAgent.getUA();
|
|
983
|
-
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
|
+
}
|
|
984
1167
|
} catch (e) {
|
|
985
1168
|
const err = this.emitError(e, "CALL_FAILED", "call failed");
|
|
986
1169
|
this.cleanupAllSessions();
|
|
@@ -1070,14 +1253,13 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1070
1253
|
cleanupSession(sessionId, session) {
|
|
1071
1254
|
const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
|
|
1072
1255
|
this.detachSessionHandlers(sessionId, targetSession);
|
|
1073
|
-
this.
|
|
1256
|
+
this.micRecovery.disable(sessionId);
|
|
1074
1257
|
this.sessionManager.cleanupSession(sessionId);
|
|
1075
1258
|
removeSessionState(this.stateStore, sessionId);
|
|
1076
1259
|
}
|
|
1077
1260
|
cleanupAllSessions() {
|
|
1078
1261
|
this.sessionManager.cleanupAllSessions();
|
|
1079
|
-
this.micRecovery.
|
|
1080
|
-
this.micRecovery.clear();
|
|
1262
|
+
this.micRecovery.cleanupAll();
|
|
1081
1263
|
this.sessionHandlers.clear();
|
|
1082
1264
|
this.stateStore.setState({
|
|
1083
1265
|
sessions: [],
|
|
@@ -1091,7 +1273,9 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1091
1273
|
state: this.stateStore,
|
|
1092
1274
|
rtc,
|
|
1093
1275
|
detachSessionHandlers: () => this.cleanupSession(sessionId, session),
|
|
1276
|
+
emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
|
|
1094
1277
|
onSessionFailed: (err, event) => this.onSessionFailed(err, event),
|
|
1278
|
+
enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
|
|
1095
1279
|
sessionId
|
|
1096
1280
|
});
|
|
1097
1281
|
}
|
|
@@ -1199,102 +1383,6 @@ var SipClient = class extends EventTargetEmitter {
|
|
|
1199
1383
|
setSessionMedia(sessionId, stream) {
|
|
1200
1384
|
this.sessionManager.setSessionMedia(sessionId, stream);
|
|
1201
1385
|
}
|
|
1202
|
-
enableMicrophoneRecovery(sessionId, options = {}) {
|
|
1203
|
-
const resolved = this.resolveExistingSessionId(sessionId);
|
|
1204
|
-
if (!resolved)
|
|
1205
|
-
return () => {
|
|
1206
|
-
};
|
|
1207
|
-
this.disableMicrophoneRecovery(resolved);
|
|
1208
|
-
const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
|
|
1209
|
-
const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
|
|
1210
|
-
let retries = 0;
|
|
1211
|
-
let stopped = false;
|
|
1212
|
-
const startedAt = Date.now();
|
|
1213
|
-
const warmupMs = Math.max(intervalMs * 2, 2e3);
|
|
1214
|
-
const tick = async () => {
|
|
1215
|
-
if (stopped || retries >= maxRetries)
|
|
1216
|
-
return;
|
|
1217
|
-
const rtc = this.sessionManager.getRtc(resolved);
|
|
1218
|
-
const session2 = this.sessionManager.getSession(resolved);
|
|
1219
|
-
if (!rtc || !session2)
|
|
1220
|
-
return;
|
|
1221
|
-
const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
|
|
1222
|
-
if (sessionState?.muted)
|
|
1223
|
-
return;
|
|
1224
|
-
const stream = rtc.mediaStream;
|
|
1225
|
-
const track = stream?.getAudioTracks?.()[0];
|
|
1226
|
-
const pc2 = session2?.connection;
|
|
1227
|
-
const sender = pc2?.getSenders?.().find((s) => s.track?.kind === "audio");
|
|
1228
|
-
if (!track && !sender)
|
|
1229
|
-
return;
|
|
1230
|
-
if (Date.now() - startedAt < warmupMs)
|
|
1231
|
-
return;
|
|
1232
|
-
if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
const trackLive = track?.readyState === "live";
|
|
1236
|
-
const senderLive = sender?.track?.readyState === "live";
|
|
1237
|
-
if (trackLive && senderLive)
|
|
1238
|
-
return;
|
|
1239
|
-
this.emitError(
|
|
1240
|
-
{
|
|
1241
|
-
cause: "microphone dropped",
|
|
1242
|
-
trackLive,
|
|
1243
|
-
senderLive
|
|
1244
|
-
},
|
|
1245
|
-
"MICROPHONE_DROPPED",
|
|
1246
|
-
"microphone dropped"
|
|
1247
|
-
);
|
|
1248
|
-
retries += 1;
|
|
1249
|
-
if (trackLive && !senderLive && track) {
|
|
1250
|
-
await rtc.replaceAudioTrack(track);
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
let nextStream;
|
|
1254
|
-
try {
|
|
1255
|
-
const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
|
|
1256
|
-
nextStream = await this.requestMicrophoneStreamInternal(deviceId);
|
|
1257
|
-
} catch (err) {
|
|
1258
|
-
console.warn("[sip] mic recovery failed to get stream", err);
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
const nextTrack = nextStream.getAudioTracks()[0];
|
|
1262
|
-
if (!nextTrack)
|
|
1263
|
-
return;
|
|
1264
|
-
await rtc.replaceAudioTrack(nextTrack);
|
|
1265
|
-
this.sessionManager.setSessionMedia(resolved, nextStream);
|
|
1266
|
-
};
|
|
1267
|
-
const timer = setInterval(() => {
|
|
1268
|
-
void tick();
|
|
1269
|
-
}, intervalMs);
|
|
1270
|
-
void tick();
|
|
1271
|
-
const session = this.sessionManager.getSession(resolved);
|
|
1272
|
-
const pc = session?.connection;
|
|
1273
|
-
const onIceChange = () => {
|
|
1274
|
-
const state = pc?.iceConnectionState;
|
|
1275
|
-
if (state === "failed" || state === "disconnected")
|
|
1276
|
-
void tick();
|
|
1277
|
-
};
|
|
1278
|
-
pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1279
|
-
const stop = () => {
|
|
1280
|
-
stopped = true;
|
|
1281
|
-
clearInterval(timer);
|
|
1282
|
-
pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
|
|
1283
|
-
};
|
|
1284
|
-
this.micRecovery.set(resolved, { stop });
|
|
1285
|
-
return stop;
|
|
1286
|
-
}
|
|
1287
|
-
disableMicrophoneRecovery(sessionId) {
|
|
1288
|
-
const resolved = this.resolveExistingSessionId(sessionId);
|
|
1289
|
-
if (!resolved)
|
|
1290
|
-
return false;
|
|
1291
|
-
const entry = this.micRecovery.get(resolved);
|
|
1292
|
-
if (!entry)
|
|
1293
|
-
return false;
|
|
1294
|
-
entry.stop();
|
|
1295
|
-
this.micRecovery.delete(resolved);
|
|
1296
|
-
return true;
|
|
1297
|
-
}
|
|
1298
1386
|
switchCameraSession(sessionId, track) {
|
|
1299
1387
|
if (!this.sessionExists(sessionId))
|
|
1300
1388
|
return false;
|
|
@@ -1460,8 +1548,6 @@ function useSipActions() {
|
|
|
1460
1548
|
getSessionIds: () => client.getSessionIds(),
|
|
1461
1549
|
getSessions: () => client.getSessions(),
|
|
1462
1550
|
setSessionMedia: (...args) => client.setSessionMedia(...args),
|
|
1463
|
-
enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
|
|
1464
|
-
disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
|
|
1465
1551
|
switchCamera: (...args) => client.switchCameraSession(...args),
|
|
1466
1552
|
enableVideo: (...args) => client.enableVideoSession(...args),
|
|
1467
1553
|
disableVideo: (...args) => client.disableVideoSession(...args)
|
|
@@ -1535,6 +1621,7 @@ function createCallPlayer(audioEl) {
|
|
|
1535
1621
|
return () => session.off("peerconnection", onPeer);
|
|
1536
1622
|
};
|
|
1537
1623
|
function bindToSession(session) {
|
|
1624
|
+
clearAudioStream(audioEl.srcObject);
|
|
1538
1625
|
if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
|
|
1539
1626
|
cleanupTrackListener = dispose(cleanupTrackListener);
|
|
1540
1627
|
cleanupTrackListener = attachTracks(session.connection);
|
|
@@ -1551,6 +1638,7 @@ function createCallPlayer(audioEl) {
|
|
|
1551
1638
|
const e = payload?.data;
|
|
1552
1639
|
cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
|
|
1553
1640
|
cleanupTrackListener = dispose(cleanupTrackListener);
|
|
1641
|
+
clearAudioStream(audioEl.srcObject);
|
|
1554
1642
|
if (!e?.session)
|
|
1555
1643
|
return;
|
|
1556
1644
|
cleanupSessionPeerListener = listenSessionPeerconnection(e.session);
|