react-jssip-kit 0.6.9 → 0.7.0

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,11 +188,16 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
172
188
  private maxSessionCount;
173
189
  private sessionManager;
174
190
  private lifecycle;
191
+ private micRecovery;
192
+ private requestMicrophoneStream?;
193
+ private micRecoveryEnabled;
194
+ private micRecoveryDefaults;
175
195
  private unloadHandler?;
176
196
  private stateLogOff?;
177
197
  get state(): SipState;
178
198
  constructor(options?: SipClientOptions);
179
199
  connect(uri: string, password: string, config: SipConfiguration): void;
200
+ setMicrophoneProvider(fn?: () => Promise<MediaStream>): void;
180
201
  registerUA(): void;
181
202
  disconnect(): void;
182
203
  call(target: string, callOptions?: CallOptions): void;
@@ -210,6 +231,8 @@ declare class SipClient extends EventTargetEmitter<JsSIPEventMap> {
210
231
  sendDTMFSession(sessionId: string, tones: string | number, options?: DTMFOptions): boolean;
211
232
  transferSession(sessionId: string, target: string, options?: ReferOptions): boolean;
212
233
  setSessionMedia(sessionId: string, stream: MediaStream): void;
234
+ enableMicrophoneRecovery(sessionId: string, options?: MicrophoneRecoveryOptions): () => void;
235
+ disableMicrophoneRecovery(sessionId: string): boolean;
213
236
  switchCameraSession(sessionId: string, track: MediaStreamTrack): false | Promise<boolean>;
214
237
  enableVideoSession(sessionId: string): boolean;
215
238
  disableVideoSession(sessionId: string): boolean;
@@ -252,6 +275,8 @@ declare function useSipActions(): {
252
275
  session: jssip_src_RTCSession.RTCSession;
253
276
  }[];
254
277
  setSessionMedia: (sessionId: string, stream: MediaStream) => void;
278
+ enableMicrophoneRecovery: (sessionId: string, options?: MicrophoneRecoveryOptions | undefined) => () => void;
279
+ disableMicrophoneRecovery: (sessionId: string) => boolean;
255
280
  switchCamera: (sessionId: string, track: MediaStreamTrack) => false | Promise<boolean>;
256
281
  enableVideo: (sessionId: string) => boolean;
257
282
  disableVideo: (sessionId: string) => boolean;
@@ -268,10 +293,11 @@ declare function CallPlayer({ sessionId }: {
268
293
  sessionId?: string;
269
294
  }): react_jsx_runtime.JSX.Element;
270
295
 
271
- declare function SipProvider({ client, children, sipEventManager, }: {
296
+ declare function SipProvider({ client, children, sipEventManager, requestMicrophoneStream, }: {
272
297
  sipEventManager?: SipEventManager;
273
298
  client: SipClient;
274
299
  children: react__default.ReactNode;
300
+ requestMicrophoneStream?: () => Promise<MediaStream>;
275
301
  }): react_jsx_runtime.JSX.Element;
276
302
 
277
303
  export { type CallDirection, type CallDirection as CallDirectionType, CallPlayer, CallStatus, CallStatus as CallStatusType, type JsSIPEventMap, type JsSIPEventName, type SessionEventName, type SessionEventPayload, type SipConfiguration, SipContext, type SipContextType, type SipEventHandlers, type SipEventManager, SipProvider, type SipSessionState, type SipState, SipStatus, SipStatus as SipStatusType, type UAEventName, type UAEventPayload, createSipClientInstance, createSipEventManager, useSip, useSipActions, useSipEvent, useSipSessionEvent, useSipSessions, useSipState };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import JsSIP from 'jssip';
2
2
  export { WebSocketInterface } from 'jssip';
3
- import { createContext, useContext, useCallback, useSyncExternalStore, useMemo, useEffect, useRef } from 'react';
3
+ import React, { createContext, useContext, useCallback, useSyncExternalStore, useMemo, useEffect, useRef } from 'react';
4
4
  import { jsx } from 'react/jsx-runtime';
5
5
 
6
6
  // src/jssip-lib/sip/debugger.ts
@@ -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 });
@@ -926,6 +959,14 @@ var SipClient = class extends EventTargetEmitter {
926
959
  this.attachBeforeUnload();
927
960
  this.syncDebugInspector(debug);
928
961
  }
962
+ setMicrophoneProvider(fn) {
963
+ this.requestMicrophoneStream = fn;
964
+ if (fn && this.micRecoveryEnabled) {
965
+ this.sessionManager.getSessions().forEach(({ id }) => {
966
+ this.enableMicrophoneRecovery(id);
967
+ });
968
+ }
969
+ }
929
970
  registerUA() {
930
971
  this.userAgent.register();
931
972
  }
@@ -937,6 +978,21 @@ var SipClient = class extends EventTargetEmitter {
937
978
  this.stateStore.reset();
938
979
  }
939
980
  call(target, callOptions = {}) {
981
+ if (!callOptions.mediaStream && this.requestMicrophoneStream) {
982
+ void this.requestMicrophoneStream().then((stream) => {
983
+ if (!stream)
984
+ throw new Error("microphone stream unavailable");
985
+ this.call(target, { ...callOptions, mediaStream: stream });
986
+ }).catch((e) => {
987
+ const err = this.emitError(
988
+ e,
989
+ "MICROPHONE_FAILED",
990
+ "microphone failed"
991
+ );
992
+ this.stateStore.batchSet({ error: err.cause });
993
+ });
994
+ return;
995
+ }
940
996
  try {
941
997
  const opts = this.ensureMediaConstraints(callOptions);
942
998
  if (opts.mediaStream)
@@ -1007,6 +1063,9 @@ var SipClient = class extends EventTargetEmitter {
1007
1063
  if (h)
1008
1064
  session.on(ev, h);
1009
1065
  });
1066
+ if (this.requestMicrophoneStream && this.micRecoveryEnabled) {
1067
+ this.enableMicrophoneRecovery(sessionId);
1068
+ }
1010
1069
  }
1011
1070
  detachSessionHandlers(sessionId, session) {
1012
1071
  const handlers = this.sessionHandlers.get(sessionId);
@@ -1032,11 +1091,14 @@ var SipClient = class extends EventTargetEmitter {
1032
1091
  cleanupSession(sessionId, session) {
1033
1092
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1034
1093
  this.detachSessionHandlers(sessionId, targetSession);
1094
+ this.disableMicrophoneRecovery(sessionId);
1035
1095
  this.sessionManager.cleanupSession(sessionId);
1036
1096
  removeSessionState(this.stateStore, sessionId);
1037
1097
  }
1038
1098
  cleanupAllSessions() {
1039
1099
  this.sessionManager.cleanupAllSessions();
1100
+ this.micRecovery.forEach((entry) => entry.stop());
1101
+ this.micRecovery.clear();
1040
1102
  this.sessionHandlers.clear();
1041
1103
  this.stateStore.setState({
1042
1104
  sessions: [],
@@ -1097,6 +1159,21 @@ var SipClient = class extends EventTargetEmitter {
1097
1159
  answerSession(sessionId, options = {}) {
1098
1160
  if (!sessionId || !this.sessionExists(sessionId))
1099
1161
  return false;
1162
+ if (!options.mediaStream && this.requestMicrophoneStream) {
1163
+ void this.requestMicrophoneStream().then((stream) => {
1164
+ if (!stream)
1165
+ throw new Error("microphone stream unavailable");
1166
+ this.answerSession(sessionId, { ...options, mediaStream: stream });
1167
+ }).catch((e) => {
1168
+ const err = this.emitError(
1169
+ e,
1170
+ "MICROPHONE_FAILED",
1171
+ "microphone failed"
1172
+ );
1173
+ this.stateStore.batchSet({ error: err.cause });
1174
+ });
1175
+ return true;
1176
+ }
1100
1177
  const opts = this.ensureMediaConstraints(options);
1101
1178
  return this.sessionManager.answer(sessionId, opts);
1102
1179
  }
@@ -1155,6 +1232,83 @@ var SipClient = class extends EventTargetEmitter {
1155
1232
  setSessionMedia(sessionId, stream) {
1156
1233
  this.sessionManager.setSessionMedia(sessionId, stream);
1157
1234
  }
1235
+ enableMicrophoneRecovery(sessionId, options = {}) {
1236
+ const resolved = this.resolveExistingSessionId(sessionId);
1237
+ if (!resolved)
1238
+ return () => {
1239
+ };
1240
+ if (!this.requestMicrophoneStream)
1241
+ return () => {
1242
+ };
1243
+ this.disableMicrophoneRecovery(resolved);
1244
+ const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
1245
+ const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
1246
+ let retries = 0;
1247
+ let stopped = false;
1248
+ const tick = async () => {
1249
+ if (stopped || retries >= maxRetries)
1250
+ return;
1251
+ const rtc = this.sessionManager.getRtc(resolved);
1252
+ const session2 = this.sessionManager.getSession(resolved);
1253
+ if (!rtc || !session2)
1254
+ return;
1255
+ const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1256
+ if (sessionState?.muted)
1257
+ return;
1258
+ const stream = rtc.mediaStream;
1259
+ const track = stream?.getAudioTracks?.()[0];
1260
+ const sender = session2?.connection?.getSenders?.().find((s) => s.track?.kind === "audio");
1261
+ const trackLive = track?.readyState === "live";
1262
+ const senderLive = sender?.track?.readyState === "live";
1263
+ if (trackLive && senderLive)
1264
+ return;
1265
+ retries += 1;
1266
+ let nextStream;
1267
+ try {
1268
+ if (!this.requestMicrophoneStream)
1269
+ return;
1270
+ nextStream = await this.requestMicrophoneStream();
1271
+ } catch (err) {
1272
+ console.warn("[sip] mic recovery failed to get stream", err);
1273
+ return;
1274
+ }
1275
+ const nextTrack = nextStream.getAudioTracks()[0];
1276
+ if (!nextTrack)
1277
+ return;
1278
+ await rtc.replaceAudioTrack(nextTrack);
1279
+ this.sessionManager.setSessionMedia(resolved, nextStream);
1280
+ };
1281
+ const timer = setInterval(() => {
1282
+ void tick();
1283
+ }, intervalMs);
1284
+ void tick();
1285
+ const session = this.sessionManager.getSession(resolved);
1286
+ const pc = session?.connection;
1287
+ const onIceChange = () => {
1288
+ const state = pc?.iceConnectionState;
1289
+ if (state === "failed" || state === "disconnected")
1290
+ void tick();
1291
+ };
1292
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1293
+ const stop = () => {
1294
+ stopped = true;
1295
+ clearInterval(timer);
1296
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1297
+ };
1298
+ this.micRecovery.set(resolved, { stop });
1299
+ return stop;
1300
+ }
1301
+ disableMicrophoneRecovery(sessionId) {
1302
+ const resolved = this.resolveExistingSessionId(sessionId);
1303
+ if (!resolved)
1304
+ return false;
1305
+ const entry = this.micRecovery.get(resolved);
1306
+ if (!entry)
1307
+ return false;
1308
+ entry.stop();
1309
+ this.micRecovery.delete(resolved);
1310
+ return true;
1311
+ }
1158
1312
  switchCameraSession(sessionId, track) {
1159
1313
  if (!this.sessionExists(sessionId))
1160
1314
  return false;
@@ -1246,8 +1400,6 @@ var SipClient = class extends EventTargetEmitter {
1246
1400
  const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
1247
1401
  if (!persisted)
1248
1402
  return void 0;
1249
- if (persisted === "true")
1250
- return true;
1251
1403
  return persisted;
1252
1404
  } catch {
1253
1405
  return void 0;
@@ -1305,6 +1457,8 @@ function useSipActions() {
1305
1457
  getSessionIds: () => client.getSessionIds(),
1306
1458
  getSessions: () => client.getSessions(),
1307
1459
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1460
+ enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1461
+ disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1308
1462
  switchCamera: (...args) => client.switchCameraSession(...args),
1309
1463
  enableVideo: (...args) => client.enableVideoSession(...args),
1310
1464
  disableVideo: (...args) => client.disableVideoSession(...args)
@@ -1443,12 +1597,16 @@ function CallPlayer({ sessionId }) {
1443
1597
  function SipProvider({
1444
1598
  client,
1445
1599
  children,
1446
- sipEventManager
1600
+ sipEventManager,
1601
+ requestMicrophoneStream
1447
1602
  }) {
1448
1603
  const manager = useMemo(
1449
1604
  () => sipEventManager ?? createSipEventManager(client),
1450
1605
  [client, sipEventManager]
1451
1606
  );
1607
+ React.useEffect(() => {
1608
+ client.setMicrophoneProvider(requestMicrophoneStream);
1609
+ }, [client, requestMicrophoneStream]);
1452
1610
  const contextValue = useMemo(() => ({ client, sipEventManager: manager }), [client, manager]);
1453
1611
  return /* @__PURE__ */ jsx(SipContext.Provider, { value: contextValue, children });
1454
1612
  }