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.cjs CHANGED
@@ -1,12 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  var JsSIP = require('jssip');
4
- var react = require('react');
4
+ var React = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
9
9
  var JsSIP__default = /*#__PURE__*/_interopDefault(JsSIP);
10
+ var React__default = /*#__PURE__*/_interopDefault(React);
10
11
 
11
12
  // src/jssip-lib/sip/debugger.ts
12
13
  var SipDebugger = class {
@@ -679,6 +680,23 @@ var WebRTCSessionController = class {
679
680
  old.stop();
680
681
  return true;
681
682
  }
683
+ async replaceAudioTrack(nextAudioTrack) {
684
+ const pc = this.getPC();
685
+ if (!pc)
686
+ return false;
687
+ if (!this.mediaStream)
688
+ this.mediaStream = new MediaStream();
689
+ const old = this.mediaStream.getAudioTracks()[0];
690
+ this.mediaStream.addTrack(nextAudioTrack);
691
+ if (old)
692
+ this.mediaStream.removeTrack(old);
693
+ const sender = pc.getSenders?.().find((s) => s.track?.kind === "audio");
694
+ if (sender)
695
+ await sender.replaceTrack(nextAudioTrack);
696
+ if (old && old !== nextAudioTrack)
697
+ old.stop();
698
+ return true;
699
+ }
682
700
  };
683
701
 
684
702
  // src/jssip-lib/sip/sessionManager.ts
@@ -886,6 +904,12 @@ var SipClient = class extends EventTargetEmitter {
886
904
  this.sessionHandlers = /* @__PURE__ */ new Map();
887
905
  this.maxSessionCount = Infinity;
888
906
  this.sessionManager = new SessionManager();
907
+ this.micRecovery = /* @__PURE__ */ new Map();
908
+ this.micRecoveryEnabled = false;
909
+ this.micRecoveryDefaults = {
910
+ intervalMs: 2e3,
911
+ maxRetries: Infinity
912
+ };
889
913
  this.errorHandler = options.errorHandler ?? new SipErrorHandler({
890
914
  formatter: options.formatError,
891
915
  messages: options.errorMessages
@@ -919,11 +943,21 @@ var SipClient = class extends EventTargetEmitter {
919
943
  this.stateStore.setState({ sipStatus: SipStatus.Connecting });
920
944
  const {
921
945
  debug: cfgDebug,
946
+ enableMicRecovery,
947
+ micRecoveryIntervalMs,
948
+ micRecoveryMaxRetries,
922
949
  maxSessionCount,
923
950
  pendingMediaTtlMs,
924
951
  ...uaCfg
925
952
  } = config;
926
953
  this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
954
+ this.micRecoveryEnabled = Boolean(enableMicRecovery);
955
+ if (typeof micRecoveryIntervalMs === "number") {
956
+ this.micRecoveryDefaults.intervalMs = micRecoveryIntervalMs;
957
+ }
958
+ if (typeof micRecoveryMaxRetries === "number") {
959
+ this.micRecoveryDefaults.maxRetries = micRecoveryMaxRetries;
960
+ }
927
961
  this.sessionManager.setPendingMediaTtl(pendingMediaTtlMs);
928
962
  const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
929
963
  this.userAgent.start(uri, password, uaCfg, { debug });
@@ -931,6 +965,14 @@ var SipClient = class extends EventTargetEmitter {
931
965
  this.attachBeforeUnload();
932
966
  this.syncDebugInspector(debug);
933
967
  }
968
+ setMicrophoneProvider(fn) {
969
+ this.requestMicrophoneStream = fn;
970
+ if (fn && this.micRecoveryEnabled) {
971
+ this.sessionManager.getSessions().forEach(({ id }) => {
972
+ this.enableMicrophoneRecovery(id);
973
+ });
974
+ }
975
+ }
934
976
  registerUA() {
935
977
  this.userAgent.register();
936
978
  }
@@ -942,6 +984,21 @@ var SipClient = class extends EventTargetEmitter {
942
984
  this.stateStore.reset();
943
985
  }
944
986
  call(target, callOptions = {}) {
987
+ if (!callOptions.mediaStream && this.requestMicrophoneStream) {
988
+ void this.requestMicrophoneStream().then((stream) => {
989
+ if (!stream)
990
+ throw new Error("microphone stream unavailable");
991
+ this.call(target, { ...callOptions, mediaStream: stream });
992
+ }).catch((e) => {
993
+ const err = this.emitError(
994
+ e,
995
+ "MICROPHONE_FAILED",
996
+ "microphone failed"
997
+ );
998
+ this.stateStore.batchSet({ error: err.cause });
999
+ });
1000
+ return;
1001
+ }
945
1002
  try {
946
1003
  const opts = this.ensureMediaConstraints(callOptions);
947
1004
  if (opts.mediaStream)
@@ -1012,6 +1069,9 @@ var SipClient = class extends EventTargetEmitter {
1012
1069
  if (h)
1013
1070
  session.on(ev, h);
1014
1071
  });
1072
+ if (this.requestMicrophoneStream && this.micRecoveryEnabled) {
1073
+ this.enableMicrophoneRecovery(sessionId);
1074
+ }
1015
1075
  }
1016
1076
  detachSessionHandlers(sessionId, session) {
1017
1077
  const handlers = this.sessionHandlers.get(sessionId);
@@ -1037,11 +1097,14 @@ var SipClient = class extends EventTargetEmitter {
1037
1097
  cleanupSession(sessionId, session) {
1038
1098
  const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1039
1099
  this.detachSessionHandlers(sessionId, targetSession);
1100
+ this.disableMicrophoneRecovery(sessionId);
1040
1101
  this.sessionManager.cleanupSession(sessionId);
1041
1102
  removeSessionState(this.stateStore, sessionId);
1042
1103
  }
1043
1104
  cleanupAllSessions() {
1044
1105
  this.sessionManager.cleanupAllSessions();
1106
+ this.micRecovery.forEach((entry) => entry.stop());
1107
+ this.micRecovery.clear();
1045
1108
  this.sessionHandlers.clear();
1046
1109
  this.stateStore.setState({
1047
1110
  sessions: [],
@@ -1102,6 +1165,21 @@ var SipClient = class extends EventTargetEmitter {
1102
1165
  answerSession(sessionId, options = {}) {
1103
1166
  if (!sessionId || !this.sessionExists(sessionId))
1104
1167
  return false;
1168
+ if (!options.mediaStream && this.requestMicrophoneStream) {
1169
+ void this.requestMicrophoneStream().then((stream) => {
1170
+ if (!stream)
1171
+ throw new Error("microphone stream unavailable");
1172
+ this.answerSession(sessionId, { ...options, mediaStream: stream });
1173
+ }).catch((e) => {
1174
+ const err = this.emitError(
1175
+ e,
1176
+ "MICROPHONE_FAILED",
1177
+ "microphone failed"
1178
+ );
1179
+ this.stateStore.batchSet({ error: err.cause });
1180
+ });
1181
+ return true;
1182
+ }
1105
1183
  const opts = this.ensureMediaConstraints(options);
1106
1184
  return this.sessionManager.answer(sessionId, opts);
1107
1185
  }
@@ -1160,6 +1238,83 @@ var SipClient = class extends EventTargetEmitter {
1160
1238
  setSessionMedia(sessionId, stream) {
1161
1239
  this.sessionManager.setSessionMedia(sessionId, stream);
1162
1240
  }
1241
+ enableMicrophoneRecovery(sessionId, options = {}) {
1242
+ const resolved = this.resolveExistingSessionId(sessionId);
1243
+ if (!resolved)
1244
+ return () => {
1245
+ };
1246
+ if (!this.requestMicrophoneStream)
1247
+ return () => {
1248
+ };
1249
+ this.disableMicrophoneRecovery(resolved);
1250
+ const intervalMs = options.intervalMs ?? this.micRecoveryDefaults.intervalMs;
1251
+ const maxRetries = options.maxRetries ?? this.micRecoveryDefaults.maxRetries;
1252
+ let retries = 0;
1253
+ let stopped = false;
1254
+ const tick = async () => {
1255
+ if (stopped || retries >= maxRetries)
1256
+ return;
1257
+ const rtc = this.sessionManager.getRtc(resolved);
1258
+ const session2 = this.sessionManager.getSession(resolved);
1259
+ if (!rtc || !session2)
1260
+ return;
1261
+ const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1262
+ if (sessionState?.muted)
1263
+ return;
1264
+ const stream = rtc.mediaStream;
1265
+ const track = stream?.getAudioTracks?.()[0];
1266
+ const sender = session2?.connection?.getSenders?.().find((s) => s.track?.kind === "audio");
1267
+ const trackLive = track?.readyState === "live";
1268
+ const senderLive = sender?.track?.readyState === "live";
1269
+ if (trackLive && senderLive)
1270
+ return;
1271
+ retries += 1;
1272
+ let nextStream;
1273
+ try {
1274
+ if (!this.requestMicrophoneStream)
1275
+ return;
1276
+ nextStream = await this.requestMicrophoneStream();
1277
+ } catch (err) {
1278
+ console.warn("[sip] mic recovery failed to get stream", err);
1279
+ return;
1280
+ }
1281
+ const nextTrack = nextStream.getAudioTracks()[0];
1282
+ if (!nextTrack)
1283
+ return;
1284
+ await rtc.replaceAudioTrack(nextTrack);
1285
+ this.sessionManager.setSessionMedia(resolved, nextStream);
1286
+ };
1287
+ const timer = setInterval(() => {
1288
+ void tick();
1289
+ }, intervalMs);
1290
+ void tick();
1291
+ const session = this.sessionManager.getSession(resolved);
1292
+ const pc = session?.connection;
1293
+ const onIceChange = () => {
1294
+ const state = pc?.iceConnectionState;
1295
+ if (state === "failed" || state === "disconnected")
1296
+ void tick();
1297
+ };
1298
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1299
+ const stop = () => {
1300
+ stopped = true;
1301
+ clearInterval(timer);
1302
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1303
+ };
1304
+ this.micRecovery.set(resolved, { stop });
1305
+ return stop;
1306
+ }
1307
+ disableMicrophoneRecovery(sessionId) {
1308
+ const resolved = this.resolveExistingSessionId(sessionId);
1309
+ if (!resolved)
1310
+ return false;
1311
+ const entry = this.micRecovery.get(resolved);
1312
+ if (!entry)
1313
+ return false;
1314
+ entry.stop();
1315
+ this.micRecovery.delete(resolved);
1316
+ return true;
1317
+ }
1163
1318
  switchCameraSession(sessionId, track) {
1164
1319
  if (!this.sessionExists(sessionId))
1165
1320
  return false;
@@ -1251,8 +1406,6 @@ var SipClient = class extends EventTargetEmitter {
1251
1406
  const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
1252
1407
  if (!persisted)
1253
1408
  return void 0;
1254
- if (persisted === "true")
1255
- return true;
1256
1409
  return persisted;
1257
1410
  } catch {
1258
1411
  return void 0;
@@ -1277,9 +1430,9 @@ function createSipEventManager(client) {
1277
1430
  }
1278
1431
  };
1279
1432
  }
1280
- var SipContext = react.createContext(null);
1433
+ var SipContext = React.createContext(null);
1281
1434
  function useSip() {
1282
- const ctx = react.useContext(SipContext);
1435
+ const ctx = React.useContext(SipContext);
1283
1436
  if (!ctx)
1284
1437
  throw new Error("Must be used within SipProvider");
1285
1438
  return ctx;
@@ -1288,16 +1441,16 @@ function useSip() {
1288
1441
  // src/hooks/useSipState.ts
1289
1442
  function useSipState() {
1290
1443
  const { client } = useSip();
1291
- const subscribe = react.useCallback(
1444
+ const subscribe = React.useCallback(
1292
1445
  (onStoreChange) => client.onChange(onStoreChange),
1293
1446
  [client]
1294
1447
  );
1295
- const getSnapshot = react.useCallback(() => client.state, [client]);
1296
- return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1448
+ const getSnapshot = React.useCallback(() => client.state, [client]);
1449
+ return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
1297
1450
  }
1298
1451
  function useSipActions() {
1299
1452
  const { client } = useSip();
1300
- return react.useMemo(
1453
+ return React.useMemo(
1301
1454
  () => ({
1302
1455
  call: (...args) => client.call(...args),
1303
1456
  answer: (...args) => client.answerSession(...args),
@@ -1310,6 +1463,8 @@ function useSipActions() {
1310
1463
  getSessionIds: () => client.getSessionIds(),
1311
1464
  getSessions: () => client.getSessions(),
1312
1465
  setSessionMedia: (...args) => client.setSessionMedia(...args),
1466
+ enableMicrophoneRecovery: (...args) => client.enableMicrophoneRecovery(...args),
1467
+ disableMicrophoneRecovery: (...args) => client.disableMicrophoneRecovery(...args),
1313
1468
  switchCamera: (...args) => client.switchCameraSession(...args),
1314
1469
  enableVideo: (...args) => client.enableVideoSession(...args),
1315
1470
  disableVideo: (...args) => client.disableVideoSession(...args)
@@ -1325,7 +1480,7 @@ function useSipSessions() {
1325
1480
  }
1326
1481
  function useSipEvent(event, handler) {
1327
1482
  const { sipEventManager } = useSip();
1328
- react.useEffect(() => {
1483
+ React.useEffect(() => {
1329
1484
  if (!handler)
1330
1485
  return;
1331
1486
  return sipEventManager.onUA(event, handler);
@@ -1333,7 +1488,7 @@ function useSipEvent(event, handler) {
1333
1488
  }
1334
1489
  function useSipSessionEvent(sessionId, event, handler) {
1335
1490
  const { sipEventManager } = useSip();
1336
- react.useEffect(() => {
1491
+ React.useEffect(() => {
1337
1492
  if (!handler)
1338
1493
  return;
1339
1494
  return sipEventManager.onSession(sessionId, event, handler);
@@ -1431,8 +1586,8 @@ function createCallPlayer(audioEl) {
1431
1586
  }
1432
1587
  function CallPlayer({ sessionId }) {
1433
1588
  const { client } = useSip();
1434
- const audioRef = react.useRef(null);
1435
- react.useEffect(() => {
1589
+ const audioRef = React.useRef(null);
1590
+ React.useEffect(() => {
1436
1591
  if (!audioRef.current)
1437
1592
  return;
1438
1593
  const player = createCallPlayer(audioRef.current);
@@ -1448,13 +1603,17 @@ function CallPlayer({ sessionId }) {
1448
1603
  function SipProvider({
1449
1604
  client,
1450
1605
  children,
1451
- sipEventManager
1606
+ sipEventManager,
1607
+ requestMicrophoneStream
1452
1608
  }) {
1453
- const manager = react.useMemo(
1609
+ const manager = React.useMemo(
1454
1610
  () => sipEventManager ?? createSipEventManager(client),
1455
1611
  [client, sipEventManager]
1456
1612
  );
1457
- const contextValue = react.useMemo(() => ({ client, sipEventManager: manager }), [client, manager]);
1613
+ React__default.default.useEffect(() => {
1614
+ client.setMicrophoneProvider(requestMicrophoneStream);
1615
+ }, [client, requestMicrophoneStream]);
1616
+ const contextValue = React.useMemo(() => ({ client, sipEventManager: manager }), [client, manager]);
1458
1617
  return /* @__PURE__ */ jsxRuntime.jsx(SipContext.Provider, { value: contextValue, children });
1459
1618
  }
1460
1619