react-jssip-kit 0.6.8 → 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
@@ -13,11 +13,8 @@ var SipDebugger = class {
13
13
  initFromSession(storage = safeSessionStorage()) {
14
14
  try {
15
15
  const saved = storage?.getItem(this.storageKey);
16
- if (saved === "true") {
17
- this.enable(this.defaultPattern, storage);
18
- } else if (saved) {
19
- storage?.removeItem?.(this.storageKey);
20
- }
16
+ if (saved)
17
+ this.enable(saved, storage);
21
18
  } catch {
22
19
  }
23
20
  }
@@ -27,9 +24,9 @@ var SipDebugger = class {
27
24
  JsSIP.debug.enable(pattern);
28
25
  this.logger = console;
29
26
  }
30
- storage?.setItem?.(this.storageKey, "true");
27
+ storage?.setItem?.(this.storageKey, pattern || this.defaultPattern);
31
28
  try {
32
- window.sipDebugBridge?.(true);
29
+ window.sipDebugBridge?.(pattern);
33
30
  } catch {
34
31
  }
35
32
  this.enabled = true;
@@ -175,8 +172,9 @@ var SipUserAgent = class {
175
172
  }
176
173
  }
177
174
  applyDebug(debug) {
178
- const enabled = debug === void 0 ? this.readSessionFlag() : !!debug;
179
- const pattern = typeof debug === "string" ? debug : "JsSIP:*";
175
+ const stored = debug === void 0 ? this.readSessionFlag() : debug;
176
+ const enabled = !!stored;
177
+ const pattern = typeof stored === "string" ? stored : "JsSIP:*";
180
178
  if (enabled) {
181
179
  JsSIP.debug.enable(pattern);
182
180
  const dbg = JsSIP.debug;
@@ -184,7 +182,7 @@ var SipUserAgent = class {
184
182
  dbg.setLogger(console);
185
183
  else if (dbg)
186
184
  dbg.logger = console;
187
- this.persistSessionFlag();
185
+ this.persistSessionFlag(typeof stored === "string" ? stored : void 0);
188
186
  } else {
189
187
  JsSIP.debug?.disable?.();
190
188
  this.clearSessionFlag();
@@ -197,15 +195,21 @@ var SipUserAgent = class {
197
195
  try {
198
196
  if (typeof window === "undefined")
199
197
  return false;
200
- return window.sessionStorage.getItem("sip-debug-enabled") === "true";
198
+ const value = window.sessionStorage.getItem("sip-debug-enabled");
199
+ if (!value)
200
+ return false;
201
+ return value;
201
202
  } catch {
202
203
  return false;
203
204
  }
204
205
  }
205
- persistSessionFlag() {
206
+ persistSessionFlag(pattern) {
206
207
  try {
207
208
  if (typeof window !== "undefined") {
208
- window.sessionStorage.setItem("sip-debug-enabled", "true");
209
+ window.sessionStorage.setItem(
210
+ "sip-debug-enabled",
211
+ pattern || "JsSIP:*"
212
+ );
209
213
  }
210
214
  } catch {
211
215
  }
@@ -670,6 +674,23 @@ var WebRTCSessionController = class {
670
674
  old.stop();
671
675
  return true;
672
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
+ }
673
694
  };
674
695
 
675
696
  // src/jssip-lib/sip/sessionManager.ts
@@ -877,6 +898,12 @@ var SipClient = class extends EventTargetEmitter {
877
898
  this.sessionHandlers = /* @__PURE__ */ new Map();
878
899
  this.maxSessionCount = Infinity;
879
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
+ };
880
907
  this.errorHandler = options.errorHandler ?? new SipErrorHandler({
881
908
  formatter: options.formatError,
882
909
  messages: options.errorMessages
@@ -910,11 +937,21 @@ var SipClient = class extends EventTargetEmitter {
910
937
  this.stateStore.setState({ sipStatus: SipStatus.Connecting });
911
938
  const {
912
939
  debug: cfgDebug,
940
+ enableMicRecovery,
941
+ micRecoveryIntervalMs,
942
+ micRecoveryMaxRetries,
913
943
  maxSessionCount,
914
944
  pendingMediaTtlMs,
915
945
  ...uaCfg
916
946
  } = config;
917
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
+ }
918
955
  this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
919
956
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
920
957
  this.userAgent.start(uri, password, uaCfg, { debug });
@@ -922,6 +959,14 @@ var SipClient = class extends EventTargetEmitter {
922
959
  this.attachBeforeUnload();
923
960
  this.syncDebugInspector(debug);
924
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
+ }
925
970
  registerUA() {
926
971
  this.userAgent.register();
927
972
  }
@@ -933,6 +978,21 @@ var SipClient = class extends EventTargetEmitter {
933
978
  this.stateStore.reset();
934
979
  }
935
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
+ }
936
996
  try {
937
997
  const opts = this.ensureMediaConstraints(callOptions);
938
998
  if (opts.mediaStream)
@@ -1003,6 +1063,9 @@ var SipClient = class extends EventTargetEmitter {
1003
1063
  if (h)
1004
1064
  session.on(ev, h);
1005
1065
  });
1066
+ if (this.requestMicrophoneStream && this.micRecoveryEnabled) {
1067
+ this.enableMicrophoneRecovery(sessionId);
1068
+ }
1006
1069
  }
1007
1070
  detachSessionHandlers(sessionId, session) {
1008
1071
  const handlers = this.sessionHandlers.get(sessionId);
@@ -1028,11 +1091,14 @@ var SipClient = class extends EventTargetEmitter {
1028
1091
  cleanupSession(sessionId, session) {
1029
1092
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1030
1093
  this.detachSessionHandlers(sessionId, targetSession);
1094
+ this.disableMicrophoneRecovery(sessionId);
1031
1095
  this.sessionManager.cleanupSession(sessionId);
1032
1096
  removeSessionState(this.stateStore, sessionId);
1033
1097
  }
1034
1098
  cleanupAllSessions() {
1035
1099
  this.sessionManager.cleanupAllSessions();
1100
+ this.micRecovery.forEach((entry) => entry.stop());
1101
+ this.micRecovery.clear();
1036
1102
  this.sessionHandlers.clear();
1037
1103
  this.stateStore.setState({
1038
1104
  sessions: [],
@@ -1093,6 +1159,21 @@ var SipClient = class extends EventTargetEmitter {
1093
1159
  answerSession(sessionId, options = {}) {
1094
1160
  if (!sessionId || !this.sessionExists(sessionId))
1095
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
+ }
1096
1177
  const opts = this.ensureMediaConstraints(options);
1097
1178
  return this.sessionManager.answer(sessionId, opts);
1098
1179
  }
@@ -1151,6 +1232,83 @@ var SipClient = class extends EventTargetEmitter {
1151
1232
  setSessionMedia(sessionId, stream) {
1152
1233
  this.sessionManager.setSessionMedia(sessionId, stream);
1153
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
+ }
1154
1312
  switchCameraSession(sessionId, track) {
1155
1313
  if (!this.sessionExists(sessionId))
1156
1314
  return false;
@@ -1242,8 +1400,6 @@ var SipClient = class extends EventTargetEmitter {
1242
1400
  const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
1243
1401
  if (!persisted)
1244
1402
  return void 0;
1245
- if (persisted === "true")
1246
- return true;
1247
1403
  return persisted;
1248
1404
  } catch {
1249
1405
  return void 0;
@@ -1301,6 +1457,8 @@ function useSipActions() {
1301
1457
  getSessionIds: () => client.getSessionIds(),
1302
1458
  getSessions: () => client.getSessions(),
1303
1459
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1460
+ enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1461
+ disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1304
1462
  switchCamera: (...args) => client.switchCameraSession(...args),
1305
1463
  enableVideo: (...args) => client.enableVideoSession(...args),
1306
1464
  disableVideo: (...args) => client.disableVideoSession(...args)
@@ -1439,12 +1597,16 @@ function CallPlayer({ sessionId }) {
1439
1597
  function SipProvider({
1440
1598
  client,
1441
1599
  children,
1442
- sipEventManager
1600
+ sipEventManager,
1601
+ requestMicrophoneStream
1443
1602
  }) {
1444
1603
  const manager = useMemo(
1445
1604
  () => sipEventManager ?? createSipEventManager(client),
1446
1605
  [client, sipEventManager]
1447
1606
  );
1607
+ React.useEffect(() => {
1608
+ client.setMicrophoneProvider(requestMicrophoneStream);
1609
+ }, [client, requestMicrophoneStream]);
1448
1610
  const contextValue = useMemo(() => ({ client, sipEventManager: manager }), [client, manager]);
1449
1611
  return /* @__PURE__ */ jsx(SipContext.Provider, { value: contextValue, children });
1450
1612
  }