react-jssip-kit 0.6.9 → 0.7.1

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.d.ts CHANGED
@@ -41,6 +41,18 @@ type SipConfiguration = Omit<UAConfiguration, "password" | "uri"> & {
41
41
  * Enable JsSIP debug logging. If string, treated as debug pattern.
42
42
  */
43
43
  debug?: boolean | string;
44
+ /**
45
+ * Enable automatic microphone recovery for sessions.
46
+ */
47
+ enableMicRecovery?: boolean;
48
+ /**
49
+ * Interval between recovery attempts in milliseconds.
50
+ */
51
+ micRecoveryIntervalMs?: number;
52
+ /**
53
+ * Maximum number of recovery attempts per session.
54
+ */
55
+ micRecoveryMaxRetries?: number;
44
56
  /**
45
57
  * Maximum allowed concurrent sessions. Additional sessions are rejected.
46
58
  */
@@ -161,6 +173,10 @@ type SipClientOptions = {
161
173
  errorHandler?: SipErrorHandler;
162
174
  debug?: boolean | string;
163
175
  };
176
+ type MicrophoneRecoveryOptions = {
177
+ intervalMs?: number;
178
+ maxRetries?: number;
179
+ };
164
180
  declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
165
181
  readonly userAgent: SipUserAgent;
166
182
  readonly stateStore: SipStateStore;
@@ -172,6 +188,9 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
172
188
  private maxSessionCount;
173
189
  private sessionManager;
174
190
  private lifecycle;
191
+ private micRecovery;
192
+ private micRecoveryEnabled;
193
+ private micRecoveryDefaults;
175
194
  private unloadHandler?;
176
195
  private stateLogOff?;
177
196
  get state(): SipState;
@@ -210,6 +229,8 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
210
229
  sendDTMFSession(sessionId: string, tones: string | number, options?: DTMFOptions): boolean;
211
230
  transferSession(sessionId: string, target: string, options?: ReferOptions): boolean;
212
231
  setSessionMedia(sessionId: string, stream: MediaStream): void;
232
+ enableMicrophoneRecovery(sessionId: string, options?: MicrophoneRecoveryOptions): () => void;
233
+ disableMicrophoneRecovery(sessionId: string): boolean;
213
234
  switchCameraSession(sessionId: string, track: MediaStreamTrack): false | Promise<boolean>;
214
235
  enableVideoSession(sessionId: string): boolean;
215
236
  disableVideoSession(sessionId: string): boolean;
@@ -225,6 +246,7 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
225
246
  private toggleStateLogger;
226
247
  private diffState;
227
248
  private getPersistedDebug;
249
+ private requestMicrophoneStreamInternal;
228
250
  }
229
251
  declare function createSipClientInstance(options?: SipClientOptions): SipClient;
230
252
  declare function createSipEventManager(client: SipClient): SipEventManager;
@@ -252,6 +274,8 @@ declare function useSipActions(): {
252
274
  session: jssip_src_RTCSession.RTCSession;
253
275
  }[];
254
276
  setSessionMedia: (sessionId: string, stream: MediaStream) => void;
277
+ enableMicrophoneRecovery: (sessionId: string, options?: MicrophoneRecoveryOptions | undefined) => () => void;
278
+ disableMicrophoneRecovery: (sessionId: string) => boolean;
255
279
  switchCamera: (sessionId: string, track: MediaStreamTrack) => false | Promise<boolean>;
256
280
  enableVideo: (sessionId: string) => boolean;
257
281
  disableVideo: (sessionId: string) => boolean;
package/dist/index.js CHANGED
@@ -674,6 +674,23 @@ var WebRTCSessionController = class {
674
674
  old.stop();
675
675
  return true;
676
676
  }
677
+ async replaceAudioTrack(nextAudioTrack) {
678
+ const pc = this.getPC();
679
+ if (!pc)
680
+ return false;
681
+ if (!this.mediaStream)
682
+ this.mediaStream = new MediaStream();
683
+ const old = this.mediaStream.getAudioTracks()[0];
684
+ this.mediaStream.addTrack(nextAudioTrack);
685
+ if (old)
686
+ this.mediaStream.removeTrack(old);
687
+ const sender = pc.getSenders?.().find((s) => s.track?.kind === "audio");
688
+ if (sender)
689
+ await sender.replaceTrack(nextAudioTrack);
690
+ if (old && old !== nextAudioTrack)
691
+ old.stop();
692
+ return true;
693
+ }
677
694
  };
678
695
 
679
696
  // src/jssip-lib/sip/sessionManager.ts
@@ -881,6 +898,12 @@ var SipClient = class extends EventTargetEmitter {
881
898
  this.sessionHandlers = /* @__PURE__ */ new Map();
882
899
  this.maxSessionCount = Infinity;
883
900
  this.sessionManager = new SessionManager();
901
+ this.micRecovery = /* @__PURE__ */ new Map();
902
+ this.micRecoveryEnabled = false;
903
+ this.micRecoveryDefaults = {
904
+ intervalMs: 2e3,
905
+ maxRetries: Infinity
906
+ };
884
907
  this.errorHandler = options.errorHandler ?? new SipErrorHandler({
885
908
  formatter: options.formatError,
886
909
  messages: options.errorMessages
@@ -914,11 +937,21 @@ var SipClient = class extends EventTargetEmitter {
914
937
  this.stateStore.setState({ sipStatus: SipStatus.Connecting });
915
938
  const {
916
939
  debug: cfgDebug,
940
+ enableMicRecovery,
941
+ micRecoveryIntervalMs,
942
+ micRecoveryMaxRetries,
917
943
  maxSessionCount,
918
944
  pendingMediaTtlMs,
919
945
  ...uaCfg
920
946
  } = config;
921
947
  this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
948
+ this.micRecoveryEnabled = Boolean(enableMicRecovery);
949
+ if (typeof micRecoveryIntervalMs === "number") {
950
+ this.micRecoveryDefaults.intervalMs = micRecoveryIntervalMs;
951
+ }
952
+ if (typeof micRecoveryMaxRetries === "number") {
953
+ this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
954
+ }
922
955
  this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
923
956
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
924
957
  this.userAgent.start(uri, password, uaCfg, { debug });
@@ -1007,6 +1040,9 @@ var SipClient = class extends EventTargetEmitter {
1007
1040
  if (h)
1008
1041
  session.on(ev, h);
1009
1042
  });
1043
+ if (this.micRecoveryEnabled) {
1044
+ this.enableMicrophoneRecovery(sessionId);
1045
+ }
1010
1046
  }
1011
1047
  detachSessionHandlers(sessionId, session) {
1012
1048
  const handlers = this.sessionHandlers.get(sessionId);
@@ -1032,11 +1068,14 @@ var SipClient = class extends EventTargetEmitter {
1032
1068
  cleanupSession(sessionId, session) {
1033
1069
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1034
1070
  this.detachSessionHandlers(sessionId, targetSession);
1071
+ this.disableMicrophoneRecovery(sessionId);
1035
1072
  this.sessionManager.cleanupSession(sessionId);
1036
1073
  removeSessionState(this.stateStore, sessionId);
1037
1074
  }
1038
1075
  cleanupAllSessions() {
1039
1076
  this.sessionManager.cleanupAllSessions();
1077
+ this.micRecovery.forEach((entry) => entry.stop());
1078
+ this.micRecovery.clear();
1040
1079
  this.sessionHandlers.clear();
1041
1080
  this.stateStore.setState({
1042
1081
  sessions: [],
@@ -1098,6 +1137,9 @@ var SipClient = class extends EventTargetEmitter {
1098
1137
  if (!sessionId || !this.sessionExists(sessionId))
1099
1138
  return false;
1100
1139
  const opts = this.ensureMediaConstraints(options);
1140
+ if (opts.mediaStream) {
1141
+ this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
1142
+ }
1101
1143
  return this.sessionManager.answer(sessionId, opts);
1102
1144
  }
1103
1145
  hangupSession(sessionId, options) {
@@ -1155,6 +1197,92 @@ var SipClient = class extends EventTargetEmitter {
1155
1197
  setSessionMedia(sessionId, stream) {
1156
1198
  this.sessionManager.setSessionMedia(sessionId, stream);
1157
1199
  }
1200
+ enableMicrophoneRecovery(sessionId, options = {}) {
1201
+ const resolved = this.resolveExistingSessionId(sessionId);
1202
+ if (!resolved)
1203
+ return () => {
1204
+ };
1205
+ this.disableMicrophoneRecovery(resolved);
1206
+ const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
1207
+ const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
1208
+ let retries = 0;
1209
+ let stopped = false;
1210
+ const tick = async () => {
1211
+ if (stopped || retries >= maxRetries)
1212
+ return;
1213
+ const rtc = this.sessionManager.getRtc(resolved);
1214
+ const session2 = this.sessionManager.getSession(resolved);
1215
+ if (!rtc || !session2)
1216
+ return;
1217
+ const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1218
+ if (sessionState?.muted)
1219
+ return;
1220
+ const stream = rtc.mediaStream;
1221
+ const track = stream?.getAudioTracks?.()[0];
1222
+ const sender = session2?.connection?.getSenders?.().find((s) => s.track?.kind === "audio");
1223
+ const trackLive = track?.readyState === "live";
1224
+ const senderLive = sender?.track?.readyState === "live";
1225
+ if (trackLive && senderLive)
1226
+ return;
1227
+ this.emitError(
1228
+ {
1229
+ cause: "microphone dropped",
1230
+ trackLive,
1231
+ senderLive
1232
+ },
1233
+ "MICROPHONE_DROPPED",
1234
+ "microphone dropped"
1235
+ );
1236
+ retries += 1;
1237
+ if (trackLive && !senderLive && track) {
1238
+ await rtc.replaceAudioTrack(track);
1239
+ return;
1240
+ }
1241
+ let nextStream;
1242
+ try {
1243
+ const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1244
+ nextStream = await this.requestMicrophoneStreamInternal(deviceId);
1245
+ } catch (err) {
1246
+ console.warn("[sip] mic recovery failed to get stream", err);
1247
+ return;
1248
+ }
1249
+ const nextTrack = nextStream.getAudioTracks()[0];
1250
+ if (!nextTrack)
1251
+ return;
1252
+ await rtc.replaceAudioTrack(nextTrack);
1253
+ this.sessionManager.setSessionMedia(resolved, nextStream);
1254
+ };
1255
+ const timer = setInterval(() => {
1256
+ void tick();
1257
+ }, intervalMs);
1258
+ void tick();
1259
+ const session = this.sessionManager.getSession(resolved);
1260
+ const pc = session?.connection;
1261
+ const onIceChange = () => {
1262
+ const state = pc?.iceConnectionState;
1263
+ if (state === "failed" || state === "disconnected")
1264
+ void tick();
1265
+ };
1266
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1267
+ const stop = () => {
1268
+ stopped = true;
1269
+ clearInterval(timer);
1270
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1271
+ };
1272
+ this.micRecovery.set(resolved, { stop });
1273
+ return stop;
1274
+ }
1275
+ disableMicrophoneRecovery(sessionId) {
1276
+ const resolved = this.resolveExistingSessionId(sessionId);
1277
+ if (!resolved)
1278
+ return false;
1279
+ const entry = this.micRecovery.get(resolved);
1280
+ if (!entry)
1281
+ return false;
1282
+ entry.stop();
1283
+ this.micRecovery.delete(resolved);
1284
+ return true;
1285
+ }
1158
1286
  switchCameraSession(sessionId, track) {
1159
1287
  if (!this.sessionExists(sessionId))
1160
1288
  return false;
@@ -1246,13 +1374,28 @@ var SipClient = class extends EventTargetEmitter {
1246
1374
  const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
1247
1375
  if (!persisted)
1248
1376
  return void 0;
1249
- if (persisted === "true")
1250
- return true;
1251
1377
  return persisted;
1252
1378
  } catch {
1253
1379
  return void 0;
1254
1380
  }
1255
1381
  }
1382
+ async requestMicrophoneStreamInternal(deviceId) {
1383
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
1384
+ throw new Error("getUserMedia not available");
1385
+ }
1386
+ const audio = deviceId && deviceId !== "default" ? { deviceId: { exact: deviceId } } : true;
1387
+ try {
1388
+ return await navigator.mediaDevices.getUserMedia({ audio });
1389
+ } catch (err) {
1390
+ const cause = err?.name || "getUserMedia failed";
1391
+ this.emitError(
1392
+ { raw: err, cause },
1393
+ "MICROPHONE_UNAVAILABLE",
1394
+ "microphone unavailable"
1395
+ );
1396
+ throw err;
1397
+ }
1398
+ }
1256
1399
  };
1257
1400
  function createSipClientInstance(options) {
1258
1401
  return new SipClient(options);
@@ -1305,6 +1448,8 @@ function useSipActions() {
1305
1448
  getSessionIds: () => client.getSessionIds(),
1306
1449
  getSessions: () => client.getSessions(),
1307
1450
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1451
+ enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1452
+ disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1308
1453
  switchCamera: (...args) => client.switchCameraSession(...args),
1309
1454
  enableVideo: (...args) => client.enableVideoSession(...args),
1310
1455
  disableVideo: (...args) => client.disableVideoSession(...args)