react-jssip-kit 0.7.9 → 1.0.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.cjs CHANGED
@@ -8,7 +8,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
9
9
  var JsSIP__default = /*#__PURE__*/_interopDefault(JsSIP);
10
10
 
11
- // src/jssip-lib/sip/debugger.ts
11
+ // src/core/modules/debug/sip-debugger.ts
12
12
  var SipDebugger = class {
13
13
  constructor(storageKey = "sip-debug-enabled", defaultPattern = "JsSIP:*") {
14
14
  this.enabled = false;
@@ -27,7 +27,6 @@ var SipDebugger = class {
27
27
  try {
28
28
  if (typeof JsSIP__default.default?.debug?.enable === "function") {
29
29
  JsSIP__default.default.debug.enable(pattern);
30
- this.logger = console;
31
30
  }
32
31
  storage?.setItem?.(this.storageKey, pattern || this.defaultPattern);
33
32
  try {
@@ -119,6 +118,25 @@ if (typeof window !== "undefined") {
119
118
  sipDebugger.attachToWindow();
120
119
  sipDebugger.initFromSession();
121
120
  }
121
+
122
+ // src/core/contracts/state.ts
123
+ var SipStatus = {
124
+ Disconnected: "disconnected",
125
+ Connecting: "connecting",
126
+ Connected: "connected",
127
+ Registered: "registered",
128
+ Unregistered: "unregistered",
129
+ RegistrationFailed: "registrationFailed"
130
+ };
131
+ var CallStatus = {
132
+ Idle: "idle",
133
+ Dialing: "dialing",
134
+ Ringing: "ringing",
135
+ Active: "active",
136
+ Hold: "hold"
137
+ };
138
+ Object.values(SipStatus);
139
+ Object.values(CallStatus);
122
140
  var SipUserAgent = class {
123
141
  constructor() {
124
142
  this._ua = null;
@@ -229,26 +247,7 @@ var SipUserAgent = class {
229
247
  }
230
248
  };
231
249
 
232
- // src/jssip-lib/core/types.ts
233
- var SipStatus = {
234
- Disconnected: "disconnected",
235
- Connecting: "connecting",
236
- Connected: "connected",
237
- Registered: "registered",
238
- Unregistered: "unregistered",
239
- RegistrationFailed: "registrationFailed"
240
- };
241
- var CallStatus = {
242
- Idle: "idle",
243
- Dialing: "dialing",
244
- Ringing: "ringing",
245
- Active: "active",
246
- Hold: "hold"
247
- };
248
- Object.values(SipStatus);
249
- Object.values(CallStatus);
250
-
251
- // src/jssip-lib/core/eventEmitter.ts
250
+ // src/core/modules/event/event-target.emitter.ts
252
251
  var EventTargetEmitter = class {
253
252
  constructor() {
254
253
  this.target = new EventTarget();
@@ -265,56 +264,14 @@ var EventTargetEmitter = class {
265
264
  }
266
265
  };
267
266
 
268
- // src/jssip-lib/core/sipErrorHandler.ts
269
- var SipErrorHandler = class {
270
- constructor(options = {}) {
271
- this.formatter = options.formatter;
272
- this.messages = options.messages;
273
- }
274
- format(input) {
275
- const { code, raw, fallback } = input;
276
- const mappedMessage = code && this.messages ? this.messages[code] : void 0;
277
- if (this.formatter) {
278
- const custom = this.formatter({
279
- raw,
280
- code,
281
- fallback: mappedMessage ?? fallback
282
- });
283
- const message2 = custom?.message ?? custom?.cause ?? mappedMessage ?? fallback ?? this.readRawMessage(raw) ?? "unknown error";
284
- return {
285
- cause: custom?.cause ?? message2,
286
- code: custom?.code ?? code,
287
- raw: custom?.raw ?? raw,
288
- message: message2
289
- };
290
- }
291
- const message = mappedMessage ?? this.readRawMessage(raw) ?? fallback ?? "unknown error";
292
- return {
293
- cause: message,
294
- code,
295
- raw,
296
- message
297
- };
298
- }
299
- readRawMessage(raw) {
300
- if (raw == null)
301
- return void 0;
302
- if (typeof raw === "string")
303
- return raw;
304
- if (typeof raw?.cause === "string")
305
- return raw.cause;
306
- if (typeof raw?.message === "string")
307
- return raw.message;
308
- return void 0;
309
- }
310
- };
311
-
312
- // src/jssip-lib/core/sipState.ts
267
+ // src/core/modules/state/sip.state.ts
313
268
  function getInitialSipState() {
314
269
  return {
315
270
  sipStatus: SipStatus.Disconnected,
316
271
  error: null,
317
- sessions: []
272
+ sessions: [],
273
+ sessionsById: {},
274
+ sessionIds: []
318
275
  };
319
276
  }
320
277
  function shallowEqual(objA, objB) {
@@ -333,24 +290,40 @@ function shallowEqual(objA, objB) {
333
290
  return true;
334
291
  }
335
292
 
336
- // src/jssip-lib/core/sipStateStore.ts
293
+ // src/core/modules/state/sip.state.store.ts
337
294
  var SipStateStore = class {
338
295
  constructor() {
339
296
  this.state = getInitialSipState();
340
297
  this.lastState = getInitialSipState();
298
+ this.publicState = {
299
+ sipStatus: this.state.sipStatus,
300
+ error: this.state.error,
301
+ sessions: this.state.sessions
302
+ };
341
303
  this.listeners = /* @__PURE__ */ new Set();
304
+ this.publicListeners = /* @__PURE__ */ new Set();
342
305
  this.pendingState = null;
343
306
  this.updateScheduled = false;
344
307
  }
345
308
  getState() {
346
309
  return this.state;
347
310
  }
311
+ getPublicState() {
312
+ return this.publicState;
313
+ }
348
314
  onChange(fn) {
349
315
  this.listeners.add(fn);
350
316
  fn(this.state);
351
317
  return () => this.listeners.delete(fn);
352
318
  }
319
+ onPublicChange(fn) {
320
+ this.publicListeners.add(fn);
321
+ return () => this.publicListeners.delete(fn);
322
+ }
353
323
  subscribe(fn) {
324
+ return this.onPublicChange(fn);
325
+ }
326
+ subscribeInternal(fn) {
354
327
  return this.onChange(fn);
355
328
  }
356
329
  setState(partial) {
@@ -362,6 +335,11 @@ var SipStateStore = class {
362
335
  }
363
336
  this.state = next;
364
337
  this.lastState = next;
338
+ this.publicState = {
339
+ sipStatus: next.sipStatus,
340
+ error: next.error,
341
+ sessions: next.sessions
342
+ };
365
343
  this.emit();
366
344
  }
367
345
  batchSet(partial) {
@@ -382,98 +360,120 @@ var SipStateStore = class {
382
360
  emit() {
383
361
  for (const fn of this.listeners)
384
362
  fn(this.state);
363
+ for (const fn of this.publicListeners)
364
+ fn(this.publicState);
385
365
  }
386
366
  };
387
367
 
388
- // src/jssip-lib/sip/handlers/uaHandlers.ts
389
- function createUAHandlers(deps) {
390
- const { emitter, state, cleanupAllSessions, emitError, onNewRTCSession } = deps;
368
+ // src/core/modules/debug/sip-debug.runtime.ts
369
+ var SESSION_DEBUG_KEY = "sip-debug-enabled";
370
+ var SipDebugRuntime = class {
371
+ constructor(deps) {
372
+ this.deps = deps;
373
+ }
374
+ attachBridge(setDebug) {
375
+ if (typeof window === "undefined")
376
+ return;
377
+ window.sipDebugBridge = (debug) => setDebug(debug ?? true);
378
+ }
379
+ getPersistedDebug() {
380
+ if (typeof window === "undefined")
381
+ return void 0;
382
+ try {
383
+ const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
384
+ if (!persisted)
385
+ return void 0;
386
+ return persisted;
387
+ } catch {
388
+ return void 0;
389
+ }
390
+ }
391
+ syncInspector(effectiveDebug) {
392
+ if (typeof window === "undefined")
393
+ return;
394
+ const enabled = Boolean(effectiveDebug);
395
+ this.deps.setDebugEnabled(enabled);
396
+ this.toggleStateLogger(enabled);
397
+ const win = window;
398
+ const disabledInspector = () => {
399
+ console.warn("SIP debug inspector disabled; enable debug to inspect.");
400
+ return null;
401
+ };
402
+ win.sipState = () => enabled ? this.deps.getState() : disabledInspector();
403
+ win.sipSessions = () => enabled ? this.deps.getSessions() : disabledInspector();
404
+ }
405
+ cleanup() {
406
+ this.toggleStateLogger(false);
407
+ }
408
+ toggleStateLogger(enabled) {
409
+ if (!enabled) {
410
+ this.stateLogOff?.();
411
+ this.stateLogOff = void 0;
412
+ return;
413
+ }
414
+ if (this.stateLogOff)
415
+ return;
416
+ let prev = this.deps.getState();
417
+ console.info("[sip][state]", { initial: true }, prev);
418
+ this.stateLogOff = this.deps.onChange((next) => {
419
+ console.info("[sip][state]", next);
420
+ prev = next;
421
+ });
422
+ }
423
+ };
424
+
425
+ // src/core/modules/event/sip-event-manager.adapter.ts
426
+ function getSessionFromPayload(payload) {
427
+ return payload?.session ?? null;
428
+ }
429
+ function getSessionId(session) {
430
+ return String(session.id ?? "");
431
+ }
432
+ function createSipEventManager(client) {
391
433
  return {
392
- connecting: (e) => {
393
- emitter.emit("connecting", e);
394
- state.batchSet({ sipStatus: SipStatus.Connecting });
395
- },
396
- connected: (e) => {
397
- emitter.emit("connected", e);
398
- state.batchSet({ sipStatus: SipStatus.Connected });
399
- },
400
- disconnected: (e) => {
401
- emitter.emit("disconnected", e);
402
- cleanupAllSessions();
403
- state.reset();
404
- },
405
- registered: (e) => {
406
- emitter.emit("registered", e);
407
- state.batchSet({ sipStatus: SipStatus.Registered, error: null });
408
- },
409
- unregistered: (e) => {
410
- emitter.emit("unregistered", e);
411
- state.batchSet({ sipStatus: SipStatus.Unregistered });
434
+ onUA(event, handler) {
435
+ return client.on(event, handler);
412
436
  },
413
- registrationFailed: (e) => {
414
- emitter.emit("registrationFailed", e);
415
- cleanupAllSessions();
416
- emitError(
417
- {
418
- raw: e,
419
- cause: e?.cause,
420
- statusCode: e?.response?.status_code,
421
- statusText: e?.response?.reason_phrase
422
- },
423
- "REGISTRATION_FAILED",
424
- "registration failed"
425
- );
426
- state.batchSet({
427
- sipStatus: SipStatus.RegistrationFailed,
428
- error: e?.cause || "registration failed"
437
+ onSession(sessionId, event, handler) {
438
+ const wrapped = (payload) => {
439
+ handler(payload);
440
+ };
441
+ let attachedSession = null;
442
+ const detach = () => {
443
+ if (!attachedSession)
444
+ return;
445
+ attachedSession.off(event, wrapped);
446
+ attachedSession = null;
447
+ };
448
+ const attach = (session) => {
449
+ if (!session)
450
+ return;
451
+ const id = getSessionId(session);
452
+ if (!id || id !== sessionId)
453
+ return;
454
+ if (attachedSession === session)
455
+ return;
456
+ detach();
457
+ attachedSession = session;
458
+ attachedSession.on(event, wrapped);
459
+ };
460
+ const offNewSession = client.on("newRTCSession", (payload) => {
461
+ attach(getSessionFromPayload(payload));
429
462
  });
430
- },
431
- newRTCSession: onNewRTCSession,
432
- newMessage: (e) => emitter.emit("newMessage", e),
433
- sipEvent: (e) => emitter.emit("sipEvent", e),
434
- newOptions: (e) => emitter.emit("newOptions", e)
435
- };
436
- }
437
-
438
- // src/jssip-lib/sip/sessionState.ts
439
- function holdOtherSessions(state, sessionId, holdFn) {
440
- const current = state.getState();
441
- current.sessions.forEach((s) => {
442
- if (s.id === sessionId)
443
- return;
444
- if (s.status === CallStatus.Active) {
445
- holdFn(s.id);
463
+ attach(client.getSession(sessionId) ?? null);
464
+ const offDisconnected = client.on("disconnected", () => {
465
+ detach();
466
+ });
467
+ return () => {
468
+ offNewSession();
469
+ offDisconnected();
470
+ detach();
471
+ };
446
472
  }
447
- });
448
- }
449
- function upsertSessionState(state, sessionId, partial) {
450
- const current = state.getState();
451
- const existing = current.sessions.find((s) => s.id === sessionId);
452
- const base = existing ?? {
453
- id: sessionId,
454
- status: CallStatus.Idle,
455
- direction: null,
456
- from: null,
457
- to: null,
458
- muted: false,
459
- acceptedAt: null,
460
- mediaKind: "audio",
461
- remoteVideoEnabled: false
462
473
  };
463
- const nextSession = { ...base, ...partial };
464
- const sessions = existing ? current.sessions.map((s) => s.id === sessionId ? nextSession : s) : [...current.sessions, nextSession];
465
- state.setState({ sessions });
466
- }
467
- function removeSessionState(state, sessionId) {
468
- const current = state.getState();
469
- const sessions = current.sessions.filter((s) => s.id !== sessionId);
470
- state.setState({
471
- sessions,
472
- error: null
473
- });
474
474
  }
475
475
 
476
- // src/jssip-lib/sip/debugLogging.ts
476
+ // src/core/modules/debug/sip-debug.logger.ts
477
477
  var describePc = (pc) => ({
478
478
  connectionState: pc?.connectionState,
479
479
  signalingState: pc?.signalingState,
@@ -642,190 +642,138 @@ function collectAudioStats(report) {
642
642
  return { outboundAudio, inboundAudio };
643
643
  }
644
644
 
645
- // src/jssip-lib/sip/handlers/sessionHandlers.ts
646
- function createSessionHandlers(deps) {
647
- const {
648
- emitter,
649
- state,
650
- rtc,
651
- detachSessionHandlers,
652
- onSessionFailed,
653
- sessionId,
654
- iceCandidateReadyDelayMs
655
- } = deps;
656
- let iceReadyCalled = false;
657
- let iceReadyTimer = null;
658
- const clearIceReadyTimer = () => {
659
- if (!iceReadyTimer)
660
- return;
661
- clearTimeout(iceReadyTimer);
662
- iceReadyTimer = null;
663
- };
664
- if (typeof iceCandidateReadyDelayMs === "number") {
665
- sipDebugLogger.logIceReadyConfig(sessionId, iceCandidateReadyDelayMs);
645
+ // src/core/modules/media/mic-recovery.manager.ts
646
+ var MicRecoveryManager = class {
647
+ constructor(deps) {
648
+ this.enabled = false;
649
+ this.defaults = {
650
+ intervalMs: 2e3,
651
+ maxRetries: Infinity
652
+ };
653
+ this.active = /* @__PURE__ */ new Map();
654
+ this.syncedSenderTrackId = /* @__PURE__ */ new Map();
655
+ this.deps = deps;
666
656
  }
667
- return {
668
- progress: (e) => {
669
- emitter.emit("progress", e);
670
- },
671
- accepted: (e) => {
672
- emitter.emit("accepted", e);
673
- state.batchSet({
674
- sessions: state.getState().sessions.map(
675
- (s) => s.id === sessionId ? {
676
- ...s,
677
- status: CallStatus.Active,
678
- acceptedAt: s.acceptedAt ?? Date.now()
679
- } : s
680
- )
681
- });
682
- },
683
- confirmed: (e) => {
684
- emitter.emit("confirmed", e);
685
- deps.enableMicrophoneRecovery?.(sessionId);
686
- },
687
- ended: (e) => {
688
- emitter.emit("ended", e);
689
- clearIceReadyTimer();
690
- detachSessionHandlers();
691
- rtc.cleanup();
692
- const nextSessions = state.getState().sessions.filter((s) => s.id !== sessionId);
693
- state.batchSet({
694
- sessions: nextSessions
695
- });
696
- },
697
- failed: (e) => {
698
- emitter.emit("failed", e);
699
- clearIceReadyTimer();
700
- detachSessionHandlers();
701
- rtc.cleanup();
702
- const cause = e?.cause || "call failed";
703
- onSessionFailed(cause, e);
704
- const nextSessions = state.getState().sessions.filter((s) => s.id !== sessionId);
705
- state.batchSet({
706
- sessions: nextSessions
707
- });
708
- },
709
- muted: () => {
710
- emitter.emit("muted", void 0);
711
- upsertSessionState(state, sessionId, { muted: true });
712
- },
713
- unmuted: () => {
714
- emitter.emit("unmuted", void 0);
715
- upsertSessionState(state, sessionId, { muted: false });
716
- },
717
- hold: () => {
718
- emitter.emit("hold", void 0);
719
- upsertSessionState(state, sessionId, { status: CallStatus.Hold });
720
- },
721
- unhold: () => {
722
- emitter.emit("unhold", void 0);
723
- upsertSessionState(state, sessionId, { status: CallStatus.Active });
724
- },
725
- reinvite: (e) => emitter.emit("reinvite", e),
726
- update: (e) => emitter.emit("update", e),
727
- sdp: (e) => emitter.emit("sdp", e),
728
- icecandidate: (e) => {
729
- const candidate = e?.candidate;
730
- const ready = typeof e?.ready === "function" ? e.ready : null;
731
- const delayMs = typeof iceCandidateReadyDelayMs === "number" ? iceCandidateReadyDelayMs : null;
732
- if (!iceReadyCalled && ready && delayMs != null) {
733
- if (candidate?.type === "srflx" && candidate?.relatedAddress != null && candidate?.relatedPort != null) {
734
- iceReadyCalled = true;
735
- if (iceReadyTimer) {
736
- clearTimeout(iceReadyTimer);
737
- iceReadyTimer = null;
738
- }
739
- sipDebugLogger.logIceReady(sessionId, {
740
- source: "srflx",
741
- delayMs,
742
- candidateType: candidate?.type
743
- });
744
- ready();
745
- } else if (!iceReadyTimer && delayMs > 0) {
746
- iceReadyTimer = setTimeout(() => {
747
- iceReadyTimer = null;
748
- if (iceReadyCalled)
749
- return;
750
- iceReadyCalled = true;
751
- sipDebugLogger.logIceReady(sessionId, {
752
- source: "timer",
753
- delayMs,
754
- candidateType: candidate?.type
755
- });
756
- ready();
757
- }, delayMs);
758
- } else if (delayMs === 0) {
759
- iceReadyCalled = true;
760
- sipDebugLogger.logIceReady(sessionId, {
761
- source: "immediate",
762
- delayMs,
763
- candidateType: candidate?.type
764
- });
765
- ready();
766
- }
657
+ configure(config) {
658
+ if (typeof config.enabled === "boolean") {
659
+ this.enabled = config.enabled;
660
+ }
661
+ if (typeof config.intervalMs === "number") {
662
+ this.defaults.intervalMs = config.intervalMs;
663
+ }
664
+ if (typeof config.maxRetries === "number") {
665
+ this.defaults.maxRetries = config.maxRetries;
666
+ }
667
+ }
668
+ enable(sessionId, options = {}) {
669
+ if (!this.enabled)
670
+ return () => {
671
+ };
672
+ this.disable(sessionId);
673
+ const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
674
+ const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
675
+ let retries = 0;
676
+ let stopped = false;
677
+ const startedAt = Date.now();
678
+ const warmupMs = Math.max(intervalMs * 2, 2e3);
679
+ const tick = async () => {
680
+ if (stopped || retries >= maxRetries)
681
+ return;
682
+ const rtc = this.deps.getRtc(sessionId);
683
+ const session2 = this.deps.getSession(sessionId);
684
+ if (!rtc || !session2)
685
+ return;
686
+ const sessionState = this.deps.getSessionState(sessionId);
687
+ if (sessionState?.muted)
688
+ return;
689
+ const stream = rtc.mediaStream;
690
+ const track = stream?.getAudioTracks?.()[0];
691
+ const pc2 = session2?.connection;
692
+ const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
693
+ if (!track && !sender)
694
+ return;
695
+ if (!track && sender?.track?.readyState === "live") {
696
+ const nextId = sender.track.id;
697
+ const prevId = this.syncedSenderTrackId.get(sessionId);
698
+ if (prevId === nextId)
699
+ return;
700
+ this.syncedSenderTrackId.set(sessionId, nextId);
701
+ this.deps.setSessionMedia(sessionId, new MediaStream([sender.track]));
702
+ return;
767
703
  }
768
- emitter.emit("icecandidate", e);
769
- },
770
- refer: (e) => emitter.emit("refer", e),
771
- replaces: (e) => emitter.emit("replaces", e),
772
- newDTMF: (e) => emitter.emit("newDTMF", e),
773
- newInfo: (e) => emitter.emit("newInfo", e),
774
- getusermediafailed: (e) => {
775
- emitter.emit("getusermediafailed", e);
776
- clearIceReadyTimer();
777
- detachSessionHandlers();
778
- rtc.cleanup();
779
- onSessionFailed("getUserMedia failed", e);
780
- state.batchSet({
781
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
782
- });
783
- },
784
- "peerconnection:createofferfailed": (e) => {
785
- emitter.emit("peerconnection:createofferfailed", e);
786
- clearIceReadyTimer();
787
- detachSessionHandlers();
788
- rtc.cleanup();
789
- onSessionFailed("peer connection createOffer failed", e);
790
- state.batchSet({
791
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
792
- });
793
- },
794
- "peerconnection:createanswerfailed": (e) => {
795
- emitter.emit("peerconnection:createanswerfailed", e);
796
- clearIceReadyTimer();
797
- detachSessionHandlers();
798
- rtc.cleanup();
799
- onSessionFailed("peer connection createAnswer failed", e);
800
- state.batchSet({
801
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
802
- });
803
- },
804
- "peerconnection:setlocaldescriptionfailed": (e) => {
805
- emitter.emit("peerconnection:setlocaldescriptionfailed", e);
806
- clearIceReadyTimer();
807
- detachSessionHandlers();
808
- rtc.cleanup();
809
- onSessionFailed("peer connection setLocalDescription failed", e);
810
- state.batchSet({
811
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
812
- });
813
- },
814
- "peerconnection:setremotedescriptionfailed": (e) => {
815
- emitter.emit("peerconnection:setremotedescriptionfailed", e);
816
- clearIceReadyTimer();
817
- detachSessionHandlers();
818
- rtc.cleanup();
819
- onSessionFailed("peer connection setRemoteDescription failed", e);
820
- state.batchSet({
821
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
704
+ if (Date.now() - startedAt < warmupMs)
705
+ return;
706
+ if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
707
+ return;
708
+ }
709
+ const trackLive = track?.readyState === "live";
710
+ const senderLive = sender?.track?.readyState === "live";
711
+ if (trackLive && senderLive)
712
+ return;
713
+ sipDebugLogger.logMicRecoveryDrop({
714
+ sessionId,
715
+ trackLive,
716
+ senderLive
822
717
  });
823
- },
824
- peerconnection: (e) => emitter.emit("peerconnection", e)
825
- };
826
- }
718
+ retries += 1;
719
+ if (trackLive && !senderLive && track) {
720
+ await rtc.replaceAudioTrack(track);
721
+ return;
722
+ }
723
+ };
724
+ const timer = setInterval(() => {
725
+ void tick();
726
+ }, intervalMs);
727
+ void tick();
728
+ const session = this.deps.getSession(sessionId);
729
+ const pc = session?.connection;
730
+ const onIceChange = () => {
731
+ const state = pc?.iceConnectionState;
732
+ if (state === "failed" || state === "disconnected")
733
+ void tick();
734
+ };
735
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
736
+ const stop = () => {
737
+ stopped = true;
738
+ clearInterval(timer);
739
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
740
+ };
741
+ this.active.set(sessionId, { stop });
742
+ return stop;
743
+ }
744
+ disable(sessionId) {
745
+ const entry = this.active.get(sessionId);
746
+ if (!entry)
747
+ return false;
748
+ entry.stop();
749
+ this.active.delete(sessionId);
750
+ this.syncedSenderTrackId.delete(sessionId);
751
+ return true;
752
+ }
753
+ cleanupAll() {
754
+ this.active.forEach((entry) => entry.stop());
755
+ this.active.clear();
756
+ this.syncedSenderTrackId.clear();
757
+ }
758
+ };
827
759
 
828
- // src/jssip-lib/sip/sessionController.ts
760
+ // src/core/modules/runtime/browser-unload.runtime.ts
761
+ var BrowserUnloadRuntime = class {
762
+ attach(onBeforeUnload) {
763
+ if (typeof window === "undefined" || this.handler)
764
+ return;
765
+ this.handler = () => onBeforeUnload();
766
+ window.addEventListener("beforeunload", this.handler);
767
+ }
768
+ detach() {
769
+ if (typeof window === "undefined" || !this.handler)
770
+ return;
771
+ window.removeEventListener("beforeunload", this.handler);
772
+ this.handler = void 0;
773
+ }
774
+ };
775
+
776
+ // src/core/modules/media/webrtc-session.controller.ts
829
777
  var WebRTCSessionController = class {
830
778
  constructor() {
831
779
  this.currentSession = null;
@@ -845,9 +793,9 @@ var WebRTCSessionController = class {
845
793
  const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
846
794
  if (pc && typeof pc.getSenders === "function") {
847
795
  if (!isClosed) {
848
- for (const s of pc.getSenders()) {
796
+ for (const sender of pc.getSenders()) {
849
797
  try {
850
- s.replaceTrack(null);
798
+ sender.replaceTrack(null);
851
799
  } catch {
852
800
  }
853
801
  }
@@ -855,12 +803,12 @@ var WebRTCSessionController = class {
855
803
  }
856
804
  if (stopTracks && this.mediaStream) {
857
805
  const senderTracks = pc && !isClosed ? new Set(
858
- pc.getSenders().map((s) => s.track).filter((t) => Boolean(t))
806
+ pc.getSenders().map((sender) => sender.track).filter((track) => Boolean(track))
859
807
  ) : null;
860
- for (const t of this.mediaStream.getTracks()) {
861
- if (senderTracks?.has(t))
808
+ for (const track of this.mediaStream.getTracks()) {
809
+ if (senderTracks?.has(track))
862
810
  continue;
863
- t.stop();
811
+ track.stop();
864
812
  }
865
813
  }
866
814
  this.mediaStream = null;
@@ -875,11 +823,11 @@ var WebRTCSessionController = class {
875
823
  ), true) : false;
876
824
  }
877
825
  mute() {
878
- this.mediaStream?.getAudioTracks().forEach((t) => t.enabled = false);
826
+ this.mediaStream?.getAudioTracks().forEach((track) => track.enabled = false);
879
827
  return this.currentSession ? (this.currentSession.mute({ audio: true }), true) : false;
880
828
  }
881
829
  unmute() {
882
- this.mediaStream?.getAudioTracks().forEach((t) => t.enabled = true);
830
+ this.mediaStream?.getAudioTracks().forEach((track) => track.enabled = true);
883
831
  return this.currentSession ? (this.currentSession.unmute({ audio: true }), true) : false;
884
832
  }
885
833
  hold() {
@@ -894,29 +842,6 @@ var WebRTCSessionController = class {
894
842
  transfer(target, options) {
895
843
  return this.currentSession ? (this.currentSession.refer(target, options), true) : false;
896
844
  }
897
- enableVideo() {
898
- this.mediaStream?.getVideoTracks().forEach((t) => t.enabled = true);
899
- }
900
- disableVideo() {
901
- this.mediaStream?.getVideoTracks().forEach((t) => t.enabled = false);
902
- }
903
- async switchCamera(nextVideoTrack) {
904
- const pc = this.getPC();
905
- if (!pc)
906
- return false;
907
- if (!this.mediaStream)
908
- this.mediaStream = new MediaStream();
909
- const old = this.mediaStream.getVideoTracks()[0];
910
- this.mediaStream.addTrack(nextVideoTrack);
911
- if (old)
912
- this.mediaStream.removeTrack(old);
913
- const sender = pc.getSenders?.().find((s) => s.track?.kind === "video");
914
- if (sender)
915
- await sender.replaceTrack(nextVideoTrack);
916
- if (old && old !== nextVideoTrack)
917
- old.stop();
918
- return true;
919
- }
920
845
  async replaceAudioTrack(nextAudioTrack) {
921
846
  const pc = this.getPC();
922
847
  if (!pc)
@@ -927,7 +852,7 @@ var WebRTCSessionController = class {
927
852
  this.mediaStream.addTrack(nextAudioTrack);
928
853
  if (old)
929
854
  this.mediaStream.removeTrack(old);
930
- const sender = pc.getSenders?.().find((s) => s.track?.kind === "audio");
855
+ const sender = pc.getSenders?.().find((entry) => entry.track?.kind === "audio");
931
856
  if (sender)
932
857
  await sender.replaceTrack(nextAudioTrack);
933
858
  if (old && old !== nextAudioTrack)
@@ -936,7 +861,7 @@ var WebRTCSessionController = class {
936
861
  }
937
862
  };
938
863
 
939
- // src/jssip-lib/sip/sessionManager.ts
864
+ // src/core/modules/session/session.manager.ts
940
865
  var SessionManager = class {
941
866
  constructor() {
942
867
  this.entries = /* @__PURE__ */ new Map();
@@ -944,9 +869,9 @@ var SessionManager = class {
944
869
  stopMediaStream(stream) {
945
870
  if (!stream)
946
871
  return;
947
- for (const t of stream.getTracks()) {
948
- if (t.readyState !== "ended")
949
- t.stop();
872
+ for (const track of stream.getTracks()) {
873
+ if (track.readyState !== "ended")
874
+ track.stop();
950
875
  }
951
876
  }
952
877
  getOrCreateRtc(sessionId, session) {
@@ -1008,15 +933,6 @@ var SessionManager = class {
1008
933
  session: entry.session
1009
934
  }));
1010
935
  }
1011
- getActiveSessionId(activeStatuses = ["active"]) {
1012
- for (const [id, entry] of Array.from(this.entries.entries()).reverse()) {
1013
- const status = entry.session?.status;
1014
- if (status && activeStatuses.includes(String(status).toLowerCase())) {
1015
- return id;
1016
- }
1017
- }
1018
- return null;
1019
- }
1020
936
  cleanupSession(sessionId) {
1021
937
  const entry = this.entries.get(sessionId);
1022
938
  if (entry) {
@@ -1066,95 +982,289 @@ var SessionManager = class {
1066
982
  }
1067
983
  };
1068
984
 
1069
- // src/jssip-lib/sip/sessionLifecycle.ts
1070
- var SessionLifecycle = class {
1071
- constructor(deps) {
1072
- this.state = deps.state;
1073
- this.sessionManager = deps.sessionManager;
1074
- this.emit = deps.emit;
1075
- this.emitError = deps.emitError;
1076
- this.attachSessionHandlers = deps.attachSessionHandlers;
1077
- this.getMaxSessionCount = deps.getMaxSessionCount;
1078
- }
1079
- setDebugEnabled(enabled) {
1080
- sipDebugLogger.setEnabled(enabled);
1081
- }
1082
- handleNewRTCSession(e) {
1083
- const session = e.session;
1084
- const sessionId = String(
1085
- session?.id ?? crypto.randomUUID?.() ?? Date.now()
1086
- );
1087
- const currentSessions = this.state.getState().sessions;
1088
- if (currentSessions.length >= this.getMaxSessionCount()) {
1089
- try {
1090
- session.terminate?.({
1091
- status_code: 486,
1092
- reason_phrase: "Busy Here"
1093
- });
1094
- } catch {
1095
- }
1096
- if (e.originator === "remote") {
1097
- this.emit("missed", e);
1098
- } else {
1099
- this.emitError(
1100
- "max session count reached",
1101
- "MAX_SESSIONS_REACHED",
1102
- "max session count reached"
1103
- );
1104
- }
985
+ // src/core/modules/session/session.state.projector.ts
986
+ function holdOtherSessions(state, sessionId, holdFn) {
987
+ const current = state.getState();
988
+ current.sessionIds.forEach((id) => {
989
+ if (id === sessionId)
1105
990
  return;
991
+ const session = current.sessionsById[id];
992
+ if (session?.status === CallStatus.Active) {
993
+ holdFn(id);
1106
994
  }
1107
- const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
1108
- this.sessionManager.setSession(sessionId, session);
1109
- this.attachSessionHandlers(sessionId, session);
1110
- this.attachCallStatsLogging(sessionId, session);
1111
- if (e.originator === "local" && !rtc.mediaStream) {
1112
- this.bindLocalOutgoingAudio(sessionId, session);
1113
- }
1114
- if (e.originator === "remote") {
1115
- this.bindRemoteIncomingAudio(sessionId, session);
1116
- }
1117
- holdOtherSessions(this.state, sessionId, (id) => {
1118
- const otherRtc = this.sessionManager.getRtc(id);
1119
- otherRtc?.hold();
1120
- });
1121
- const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
1122
- upsertSessionState(this.state, sessionId, {
1123
- direction: e.originator,
1124
- from: e.originator === "remote" ? e.request.from.uri.user : null,
1125
- to: e.request.to.uri.user,
1126
- status: e.originator === "remote" ? CallStatus.Ringing : CallStatus.Dialing,
1127
- mediaKind: sdpHasVideo ? "video" : "audio",
1128
- remoteVideoEnabled: sdpHasVideo
1129
- });
1130
- this.emit("newRTCSession", e);
1131
- }
1132
- bindLocalOutgoingAudio(sessionId, session) {
1133
- const maxAttempts = 50;
1134
- const retryDelayMs = 500;
1135
- let attempts = 0;
1136
- let retryScheduled = false;
1137
- let retryTimer = null;
1138
- let stopped = false;
1139
- let exhausted = false;
1140
- let exhaustedCheckUsed = false;
1141
- let attachedPc = null;
1142
- const logLocalAudioError = (message, pc, extra) => {
1143
- sipDebugLogger.logLocalAudioError(sessionId, message, pc, extra);
1144
- };
1145
- const tryBindFromPc = (pc) => {
1146
- if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
1147
- return false;
1148
- }
1149
- const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1150
- const audioTrack = audioSender?.track;
1151
- if (!audioTrack) {
1152
- logLocalAudioError(
1153
- "[sip] outgoing audio bind failed: no audio track",
1154
- pc
1155
- );
1156
- return false;
1157
- }
995
+ });
996
+ }
997
+ function upsertSessionState(state, sessionId, partial) {
998
+ const current = state.getState();
999
+ const existing = current.sessionsById[sessionId];
1000
+ const base = existing ?? {
1001
+ id: sessionId,
1002
+ status: CallStatus.Idle,
1003
+ direction: null,
1004
+ from: null,
1005
+ to: null,
1006
+ muted: false,
1007
+ acceptedAt: null
1008
+ };
1009
+ const nextSession = { ...base, ...partial };
1010
+ const sessionsById = {
1011
+ ...current.sessionsById,
1012
+ [sessionId]: nextSession
1013
+ };
1014
+ const sessionIds = existing ? current.sessionIds : [...current.sessionIds, sessionId];
1015
+ const sessions = existing ? current.sessions.map(
1016
+ (session) => session.id === sessionId ? nextSession : session
1017
+ ) : [...current.sessions, nextSession];
1018
+ state.setState({ sessionsById, sessionIds, sessions });
1019
+ }
1020
+ function removeSessionState(state, sessionId) {
1021
+ const current = state.getState();
1022
+ if (!current.sessionsById[sessionId])
1023
+ return;
1024
+ const sessionsById = { ...current.sessionsById };
1025
+ delete sessionsById[sessionId];
1026
+ const sessionIds = current.sessionIds.filter((id) => id !== sessionId);
1027
+ const sessions = current.sessions.filter(
1028
+ (session) => session.id !== sessionId
1029
+ );
1030
+ state.setState({
1031
+ sessions,
1032
+ sessionsById,
1033
+ sessionIds,
1034
+ error: null
1035
+ });
1036
+ }
1037
+
1038
+ // src/core/modules/session/session.handlers.ts
1039
+ function createSessionHandlers(deps) {
1040
+ const {
1041
+ emitter,
1042
+ state,
1043
+ rtc,
1044
+ detachSessionHandlers,
1045
+ sessionId,
1046
+ iceCandidateReadyDelayMs
1047
+ } = deps;
1048
+ let iceReadyCalled = false;
1049
+ let iceReadyTimer = null;
1050
+ const clearIceReadyTimer = () => {
1051
+ if (!iceReadyTimer)
1052
+ return;
1053
+ clearTimeout(iceReadyTimer);
1054
+ iceReadyTimer = null;
1055
+ };
1056
+ if (typeof iceCandidateReadyDelayMs === "number") {
1057
+ sipDebugLogger.logIceReadyConfig(sessionId, iceCandidateReadyDelayMs);
1058
+ }
1059
+ return {
1060
+ progress: (e) => {
1061
+ emitter.emit("progress", e);
1062
+ },
1063
+ accepted: (e) => {
1064
+ emitter.emit("accepted", e);
1065
+ const existing = state.getState().sessionsById[sessionId];
1066
+ upsertSessionState(state, sessionId, {
1067
+ status: CallStatus.Active,
1068
+ acceptedAt: existing?.acceptedAt ?? Date.now()
1069
+ });
1070
+ },
1071
+ confirmed: (e) => {
1072
+ emitter.emit("confirmed", e);
1073
+ deps.enableMicrophoneRecovery?.(sessionId);
1074
+ },
1075
+ ended: (e) => {
1076
+ emitter.emit("ended", e);
1077
+ clearIceReadyTimer();
1078
+ detachSessionHandlers();
1079
+ rtc.cleanup();
1080
+ removeSessionState(state, sessionId);
1081
+ },
1082
+ failed: (e) => {
1083
+ emitter.emit("failed", e);
1084
+ clearIceReadyTimer();
1085
+ detachSessionHandlers();
1086
+ rtc.cleanup();
1087
+ removeSessionState(state, sessionId);
1088
+ },
1089
+ muted: (e) => {
1090
+ emitter.emit("muted", e);
1091
+ upsertSessionState(state, sessionId, { muted: true });
1092
+ },
1093
+ unmuted: (e) => {
1094
+ emitter.emit("unmuted", e);
1095
+ upsertSessionState(state, sessionId, { muted: false });
1096
+ },
1097
+ hold: (e) => {
1098
+ emitter.emit("hold", e);
1099
+ upsertSessionState(state, sessionId, { status: CallStatus.Hold });
1100
+ },
1101
+ unhold: (e) => {
1102
+ emitter.emit("unhold", e);
1103
+ upsertSessionState(state, sessionId, { status: CallStatus.Active });
1104
+ },
1105
+ reinvite: (e) => emitter.emit("reinvite", e),
1106
+ update: (e) => emitter.emit("update", e),
1107
+ sdp: (e) => emitter.emit("sdp", e),
1108
+ icecandidate: (e) => {
1109
+ const candidate = e?.candidate;
1110
+ const ready = typeof e?.ready === "function" ? e.ready : null;
1111
+ const delayMs = typeof iceCandidateReadyDelayMs === "number" ? iceCandidateReadyDelayMs : null;
1112
+ if (!iceReadyCalled && ready && delayMs != null) {
1113
+ if (candidate?.type === "srflx" && candidate?.relatedAddress != null && candidate?.relatedPort != null) {
1114
+ iceReadyCalled = true;
1115
+ if (iceReadyTimer) {
1116
+ clearTimeout(iceReadyTimer);
1117
+ iceReadyTimer = null;
1118
+ }
1119
+ sipDebugLogger.logIceReady(sessionId, {
1120
+ source: "srflx",
1121
+ delayMs,
1122
+ candidateType: candidate?.type
1123
+ });
1124
+ ready();
1125
+ } else if (!iceReadyTimer && delayMs > 0) {
1126
+ iceReadyTimer = setTimeout(() => {
1127
+ iceReadyTimer = null;
1128
+ if (iceReadyCalled)
1129
+ return;
1130
+ iceReadyCalled = true;
1131
+ sipDebugLogger.logIceReady(sessionId, {
1132
+ source: "timer",
1133
+ delayMs,
1134
+ candidateType: candidate?.type
1135
+ });
1136
+ ready();
1137
+ }, delayMs);
1138
+ } else if (delayMs === 0) {
1139
+ iceReadyCalled = true;
1140
+ sipDebugLogger.logIceReady(sessionId, {
1141
+ source: "immediate",
1142
+ delayMs,
1143
+ candidateType: candidate?.type
1144
+ });
1145
+ ready();
1146
+ }
1147
+ }
1148
+ emitter.emit("icecandidate", e);
1149
+ },
1150
+ refer: (e) => emitter.emit("refer", e),
1151
+ replaces: (e) => emitter.emit("replaces", e),
1152
+ newDTMF: (e) => emitter.emit("newDTMF", e),
1153
+ newInfo: (e) => emitter.emit("newInfo", e),
1154
+ getusermediafailed: (e) => {
1155
+ emitter.emit("getusermediafailed", e);
1156
+ clearIceReadyTimer();
1157
+ detachSessionHandlers();
1158
+ rtc.cleanup();
1159
+ removeSessionState(state, sessionId);
1160
+ },
1161
+ "peerconnection:createofferfailed": (e) => {
1162
+ emitter.emit("peerconnection:createofferfailed", e);
1163
+ clearIceReadyTimer();
1164
+ detachSessionHandlers();
1165
+ rtc.cleanup();
1166
+ removeSessionState(state, sessionId);
1167
+ },
1168
+ "peerconnection:createanswerfailed": (e) => {
1169
+ emitter.emit("peerconnection:createanswerfailed", e);
1170
+ clearIceReadyTimer();
1171
+ detachSessionHandlers();
1172
+ rtc.cleanup();
1173
+ removeSessionState(state, sessionId);
1174
+ },
1175
+ "peerconnection:setlocaldescriptionfailed": (e) => {
1176
+ emitter.emit("peerconnection:setlocaldescriptionfailed", e);
1177
+ clearIceReadyTimer();
1178
+ detachSessionHandlers();
1179
+ rtc.cleanup();
1180
+ removeSessionState(state, sessionId);
1181
+ },
1182
+ "peerconnection:setremotedescriptionfailed": (e) => {
1183
+ emitter.emit("peerconnection:setremotedescriptionfailed", e);
1184
+ clearIceReadyTimer();
1185
+ detachSessionHandlers();
1186
+ rtc.cleanup();
1187
+ removeSessionState(state, sessionId);
1188
+ },
1189
+ peerconnection: (e) => emitter.emit("peerconnection", e)
1190
+ };
1191
+ }
1192
+
1193
+ // src/core/modules/session/session.lifecycle.ts
1194
+ var SessionLifecycle = class {
1195
+ constructor(deps) {
1196
+ this.state = deps.state;
1197
+ this.sessionManager = deps.sessionManager;
1198
+ this.emit = deps.emit;
1199
+ this.attachSessionHandlers = deps.attachSessionHandlers;
1200
+ this.getMaxSessionCount = deps.getMaxSessionCount;
1201
+ }
1202
+ setDebugEnabled(enabled) {
1203
+ sipDebugLogger.setEnabled(enabled);
1204
+ }
1205
+ handleNewRTCSession(e) {
1206
+ const session = e.session;
1207
+ const sessionId = String(session.id ?? crypto.randomUUID?.() ?? Date.now());
1208
+ const currentSessions = this.state.getState().sessions;
1209
+ if (currentSessions.length >= this.getMaxSessionCount()) {
1210
+ try {
1211
+ const terminateOptions = {
1212
+ status_code: 486,
1213
+ reason_phrase: "Busy Here"
1214
+ };
1215
+ session.terminate(terminateOptions);
1216
+ } catch {
1217
+ }
1218
+ return;
1219
+ }
1220
+ const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
1221
+ this.sessionManager.setSession(sessionId, session);
1222
+ this.attachSessionHandlers(sessionId, session);
1223
+ this.attachCallStatsLogging(sessionId, session);
1224
+ if (e.originator === "local" && !rtc.mediaStream) {
1225
+ this.bindLocalOutgoingAudio(sessionId, session);
1226
+ }
1227
+ if (e.originator === "remote") {
1228
+ this.bindRemoteIncomingAudio(sessionId, session);
1229
+ }
1230
+ holdOtherSessions(this.state, sessionId, (id) => {
1231
+ const otherRtc = this.sessionManager.getRtc(id);
1232
+ otherRtc?.hold();
1233
+ });
1234
+ upsertSessionState(this.state, sessionId, {
1235
+ direction: e.originator,
1236
+ from: e.originator === "remote" ? e.request.from.uri.user : null,
1237
+ to: e.request.to.uri.user,
1238
+ status: e.originator === "remote" ? CallStatus.Ringing : CallStatus.Dialing
1239
+ });
1240
+ this.emit("newRTCSession", e);
1241
+ }
1242
+ bindLocalOutgoingAudio(sessionId, session) {
1243
+ const maxAttempts = 50;
1244
+ const retryDelayMs = 500;
1245
+ let attempts = 0;
1246
+ let retryScheduled = false;
1247
+ let retryTimer = null;
1248
+ let stopped = false;
1249
+ let exhausted = false;
1250
+ let exhaustedCheckUsed = false;
1251
+ let attachedPc = null;
1252
+ const logLocalAudioError = (message, pc, extra) => {
1253
+ sipDebugLogger.logLocalAudioError(sessionId, message, pc, extra);
1254
+ };
1255
+ const tryBindFromPc = (pc) => {
1256
+ if (stopped || !pc || this.sessionManager.getRtc(sessionId)?.mediaStream) {
1257
+ return false;
1258
+ }
1259
+ const audioSender = pc?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1260
+ const audioTrack = audioSender?.track;
1261
+ if (!audioTrack) {
1262
+ logLocalAudioError(
1263
+ "[sip] outgoing audio bind failed: no audio track",
1264
+ pc
1265
+ );
1266
+ return false;
1267
+ }
1158
1268
  const outgoingStream = new MediaStream([audioTrack]);
1159
1269
  this.sessionManager.setSessionMedia(sessionId, outgoingStream);
1160
1270
  return true;
@@ -1193,7 +1303,10 @@ var SessionLifecycle = class {
1193
1303
  attachedPc = pc;
1194
1304
  attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1195
1305
  attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1196
- attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1306
+ attachedPc.addEventListener?.(
1307
+ "iceconnectionstatechange",
1308
+ onPcStateChange
1309
+ );
1197
1310
  };
1198
1311
  const clearRetryTimer = () => {
1199
1312
  if (!retryTimer)
@@ -1302,8 +1415,8 @@ var SessionLifecycle = class {
1302
1415
  session.on?.("peerconnection", onPeer);
1303
1416
  }
1304
1417
  session.on?.("confirmed", onConfirmed);
1305
- session.on?.("ended", stopRetry);
1306
- session.on?.("failed", stopRetry);
1418
+ session.on?.("ended", () => stopRetry());
1419
+ session.on?.("failed", () => stopRetry());
1307
1420
  }
1308
1421
  bindRemoteIncomingAudio(sessionId, session) {
1309
1422
  const maxAttempts = 50;
@@ -1369,11 +1482,11 @@ var SessionLifecycle = class {
1369
1482
  return;
1370
1483
  exhaustedCheckUsed = true;
1371
1484
  if (checkRemoteTrack(attachedPc))
1372
- stopRetry();
1485
+ stopRetry({ keepTrack: true });
1373
1486
  return;
1374
1487
  }
1375
1488
  if (checkRemoteTrack(attachedPc))
1376
- stopRetry();
1489
+ stopRetry({ keepTrack: true });
1377
1490
  };
1378
1491
  const attachPcListeners = (pc) => {
1379
1492
  if (!pc || pc === attachedPc)
@@ -1396,7 +1509,10 @@ var SessionLifecycle = class {
1396
1509
  attachedPc = pc;
1397
1510
  attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1398
1511
  attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1399
- attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1512
+ attachedPc.addEventListener?.(
1513
+ "iceconnectionstatechange",
1514
+ onPcStateChange
1515
+ );
1400
1516
  attachedPc.addEventListener?.("track", onTrack);
1401
1517
  };
1402
1518
  const clearRetryTimer = () => {
@@ -1405,7 +1521,7 @@ var SessionLifecycle = class {
1405
1521
  clearTimeout(retryTimer);
1406
1522
  retryTimer = null;
1407
1523
  };
1408
- const stopRetry = () => {
1524
+ const stopRetry = (opts = {}) => {
1409
1525
  if (stopped)
1410
1526
  return;
1411
1527
  stopped = true;
@@ -1426,7 +1542,7 @@ var SessionLifecycle = class {
1426
1542
  attachedPc.removeEventListener?.("track", onTrack);
1427
1543
  attachedPc = null;
1428
1544
  }
1429
- if (attachedTrack) {
1545
+ if (attachedTrack && !opts.keepTrack) {
1430
1546
  attachedTrack.removeEventListener?.("ended", onRemoteEnded);
1431
1547
  attachedTrack.removeEventListener?.("mute", onRemoteMuted);
1432
1548
  attachedTrack = null;
@@ -1455,7 +1571,7 @@ var SessionLifecycle = class {
1455
1571
  retryScheduled = false;
1456
1572
  retryTimer = null;
1457
1573
  if (checkRemoteTrack(pc)) {
1458
- stopRetry();
1574
+ stopRetry({ keepTrack: true });
1459
1575
  return;
1460
1576
  }
1461
1577
  if (!pc)
@@ -1471,11 +1587,11 @@ var SessionLifecycle = class {
1471
1587
  return;
1472
1588
  exhaustedCheckUsed = true;
1473
1589
  if (checkRemoteTrack(attachedPc))
1474
- stopRetry();
1590
+ stopRetry({ keepTrack: true });
1475
1591
  return;
1476
1592
  }
1477
1593
  if (checkRemoteTrack(attachedPc))
1478
- stopRetry();
1594
+ stopRetry({ keepTrack: true });
1479
1595
  };
1480
1596
  const onPeer = (data) => {
1481
1597
  if (stopped)
@@ -1486,11 +1602,11 @@ var SessionLifecycle = class {
1486
1602
  return;
1487
1603
  exhaustedCheckUsed = true;
1488
1604
  if (checkRemoteTrack(data.peerconnection))
1489
- stopRetry();
1605
+ stopRetry({ keepTrack: true });
1490
1606
  return;
1491
1607
  }
1492
1608
  if (checkRemoteTrack(data.peerconnection)) {
1493
- stopRetry();
1609
+ stopRetry({ keepTrack: true });
1494
1610
  return;
1495
1611
  }
1496
1612
  scheduleRetry(data.peerconnection);
@@ -1504,11 +1620,11 @@ var SessionLifecycle = class {
1504
1620
  return;
1505
1621
  exhaustedCheckUsed = true;
1506
1622
  if (checkRemoteTrack(currentPc))
1507
- stopRetry();
1623
+ stopRetry({ keepTrack: true });
1508
1624
  return;
1509
1625
  }
1510
1626
  if (checkRemoteTrack(currentPc)) {
1511
- stopRetry();
1627
+ stopRetry({ keepTrack: true });
1512
1628
  return;
1513
1629
  }
1514
1630
  logMissingReceiver(currentPc, "confirmed without remote track");
@@ -1523,8 +1639,8 @@ var SessionLifecycle = class {
1523
1639
  session.on?.("peerconnection", onPeer);
1524
1640
  }
1525
1641
  session.on?.("confirmed", onConfirmed);
1526
- session.on?.("ended", stopRetry);
1527
- session.on?.("failed", stopRetry);
1642
+ session.on?.("ended", () => stopRetry());
1643
+ session.on?.("failed", () => stopRetry());
1528
1644
  }
1529
1645
  attachCallStatsLogging(sessionId, session) {
1530
1646
  const onConfirmed = () => {
@@ -1539,167 +1655,335 @@ var SessionLifecycle = class {
1539
1655
  }
1540
1656
  };
1541
1657
 
1542
- // src/jssip-lib/sip/micRecovery.ts
1543
- var MicRecoveryManager = class {
1658
+ // src/core/modules/session/session.module.ts
1659
+ var SessionModule = class {
1544
1660
  constructor(deps) {
1545
- this.enabled = false;
1546
- this.defaults = {
1547
- intervalMs: 2e3,
1548
- maxRetries: Infinity
1549
- };
1550
- this.active = /* @__PURE__ */ new Map();
1551
1661
  this.deps = deps;
1662
+ this.sessionHandlers = /* @__PURE__ */ new Map();
1663
+ this.lifecycle = new SessionLifecycle({
1664
+ state: deps.state,
1665
+ sessionManager: deps.sessionManager,
1666
+ emit: (event, payload) => deps.emitter.emit(event, payload),
1667
+ attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
1668
+ getMaxSessionCount: deps.getMaxSessionCount
1669
+ });
1552
1670
  }
1553
- configure(config) {
1554
- if (typeof config.enabled === "boolean") {
1555
- this.enabled = config.enabled;
1671
+ setDebugEnabled(enabled) {
1672
+ this.lifecycle.setDebugEnabled(enabled);
1673
+ }
1674
+ handleNewRTCSession(e) {
1675
+ this.lifecycle.handleNewRTCSession(e);
1676
+ }
1677
+ setSessionMedia(sessionId, stream) {
1678
+ this.deps.sessionManager.setSessionMedia(sessionId, stream);
1679
+ }
1680
+ setSession(sessionId, session) {
1681
+ this.deps.sessionManager.setSession(sessionId, session);
1682
+ }
1683
+ answerSession(sessionId, options = {}) {
1684
+ if (!sessionId || !this.sessionExists(sessionId))
1685
+ return false;
1686
+ return this.deps.sessionManager.answer(sessionId, options);
1687
+ }
1688
+ hangupSession(sessionId, options) {
1689
+ if (!sessionId || !this.sessionExists(sessionId))
1690
+ return false;
1691
+ return this.deps.sessionManager.hangup(sessionId, options);
1692
+ }
1693
+ hangupAll(options) {
1694
+ const ids = this.getSessionIds();
1695
+ if (ids.length === 0)
1696
+ return false;
1697
+ return ids.every((id) => this.hangupSession(id, options));
1698
+ }
1699
+ toggleMuteSession(sessionId) {
1700
+ const resolved = this.resolveExistingSessionId(sessionId);
1701
+ if (!resolved)
1702
+ return false;
1703
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1704
+ const muted = sessionState?.muted ?? false;
1705
+ if (muted) {
1706
+ this.deps.sessionManager.unmute(resolved);
1707
+ return true;
1556
1708
  }
1557
- if (typeof config.intervalMs === "number") {
1558
- this.defaults.intervalMs = config.intervalMs;
1709
+ this.deps.sessionManager.mute(resolved);
1710
+ return true;
1711
+ }
1712
+ toggleHoldSession(sessionId) {
1713
+ const resolved = this.resolveExistingSessionId(sessionId);
1714
+ if (!resolved)
1715
+ return false;
1716
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1717
+ const isOnHold = sessionState?.status === CallStatus.Hold;
1718
+ if (isOnHold) {
1719
+ this.deps.sessionManager.unhold(resolved);
1720
+ return true;
1559
1721
  }
1560
- if (typeof config.maxRetries === "number") {
1561
- this.defaults.maxRetries = config.maxRetries;
1722
+ if (sessionState?.status === CallStatus.Active) {
1723
+ this.deps.sessionManager.hold(resolved);
1724
+ return true;
1562
1725
  }
1726
+ return false;
1563
1727
  }
1564
- enable(sessionId, options = {}) {
1565
- if (!this.enabled)
1566
- return () => {
1567
- };
1568
- this.disable(sessionId);
1569
- const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
1570
- const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
1571
- let retries = 0;
1572
- let stopped = false;
1573
- const startedAt = Date.now();
1574
- const warmupMs = Math.max(intervalMs * 2, 2e3);
1575
- const tick = async () => {
1576
- if (stopped || retries >= maxRetries)
1577
- return;
1578
- const rtc = this.deps.getRtc(sessionId);
1579
- const session2 = this.deps.getSession(sessionId);
1580
- if (!rtc || !session2)
1581
- return;
1582
- const sessionState = this.deps.getSessionState(sessionId);
1583
- if (sessionState?.muted)
1584
- return;
1585
- const stream = rtc.mediaStream;
1586
- const track = stream?.getAudioTracks?.()[0];
1587
- const pc2 = session2?.connection;
1588
- const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1589
- if (!track && !sender)
1590
- return;
1591
- if (Date.now() - startedAt < warmupMs)
1592
- return;
1593
- if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
1594
- return;
1595
- }
1596
- const trackLive = track?.readyState === "live";
1597
- const senderLive = sender?.track?.readyState === "live";
1598
- if (trackLive && senderLive)
1599
- return;
1600
- sipDebugLogger.logMicRecoveryDrop({
1601
- sessionId,
1602
- trackLive,
1603
- senderLive
1604
- });
1605
- retries += 1;
1606
- if (trackLive && !senderLive && track) {
1607
- await rtc.replaceAudioTrack(track);
1608
- return;
1609
- }
1610
- let nextStream;
1611
- try {
1612
- const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1613
- nextStream = await this.deps.requestMicrophoneStream(deviceId);
1614
- } catch (err) {
1615
- console.warn("[sip] mic recovery failed to get stream", err);
1616
- return;
1617
- }
1618
- const nextTrack = nextStream.getAudioTracks()[0];
1619
- if (!nextTrack)
1620
- return;
1621
- await rtc.replaceAudioTrack(nextTrack);
1622
- this.deps.setSessionMedia(sessionId, nextStream);
1623
- };
1624
- const timer = setInterval(() => {
1625
- void tick();
1626
- }, intervalMs);
1627
- void tick();
1628
- const session = this.deps.getSession(sessionId);
1629
- const pc = session?.connection;
1630
- const onIceChange = () => {
1631
- const state = pc?.iceConnectionState;
1632
- if (state === "failed" || state === "disconnected")
1633
- void tick();
1634
- };
1635
- pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1636
- const stop = () => {
1637
- stopped = true;
1638
- clearInterval(timer);
1639
- pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1640
- };
1641
- this.active.set(sessionId, { stop });
1642
- return stop;
1728
+ sendDTMFSession(sessionId, tones, options) {
1729
+ const resolved = this.resolveExistingSessionId(sessionId);
1730
+ if (!resolved)
1731
+ return false;
1732
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1733
+ if (sessionState?.status === CallStatus.Active) {
1734
+ this.deps.sessionManager.sendDTMF(resolved, tones, options);
1735
+ return true;
1736
+ }
1737
+ return false;
1643
1738
  }
1644
- disable(sessionId) {
1645
- const entry = this.active.get(sessionId);
1646
- if (!entry)
1739
+ transferSession(sessionId, target, options) {
1740
+ const resolved = this.resolveExistingSessionId(sessionId);
1741
+ if (!resolved)
1647
1742
  return false;
1648
- entry.stop();
1649
- this.active.delete(sessionId);
1743
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1744
+ if (sessionState?.status === CallStatus.Active) {
1745
+ this.deps.sessionManager.transfer(resolved, target, options);
1746
+ return true;
1747
+ }
1748
+ return false;
1749
+ }
1750
+ sendInfoSession(sessionId, contentType, body, options) {
1751
+ const resolved = this.resolveExistingSessionId(sessionId);
1752
+ if (!resolved)
1753
+ return false;
1754
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1755
+ if (sessionState?.status !== CallStatus.Active && sessionState?.status !== CallStatus.Hold) {
1756
+ return false;
1757
+ }
1758
+ const session = this.deps.sessionManager.getSession(resolved);
1759
+ if (!session)
1760
+ return false;
1761
+ session.sendInfo(contentType, body, options);
1650
1762
  return true;
1651
1763
  }
1652
- cleanupAll() {
1653
- this.active.forEach((entry) => entry.stop());
1654
- this.active.clear();
1764
+ updateSession(sessionId, options) {
1765
+ const resolved = this.resolveExistingSessionId(sessionId);
1766
+ if (!resolved)
1767
+ return false;
1768
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1769
+ if (sessionState?.status !== CallStatus.Active && sessionState?.status !== CallStatus.Hold) {
1770
+ return false;
1771
+ }
1772
+ const session = this.deps.sessionManager.getSession(resolved);
1773
+ if (!session)
1774
+ return false;
1775
+ return session.renegotiate(options);
1776
+ }
1777
+ getSession(sessionId) {
1778
+ return this.deps.sessionManager.getSession(sessionId);
1779
+ }
1780
+ getSessionIds() {
1781
+ return this.deps.sessionManager.getSessionIds();
1782
+ }
1783
+ getSessions() {
1784
+ return this.deps.sessionManager.getSessions();
1785
+ }
1786
+ cleanupAllSessions() {
1787
+ this.deps.sessionManager.cleanupAllSessions();
1788
+ this.deps.micRecovery.cleanupAll();
1789
+ this.sessionHandlers.clear();
1790
+ this.deps.state.setState({
1791
+ sessions: [],
1792
+ sessionsById: {},
1793
+ sessionIds: [],
1794
+ error: null
1795
+ });
1796
+ }
1797
+ attachSessionHandlers(sessionId, session) {
1798
+ const handlers = this.createSessionHandlersFor(sessionId, session);
1799
+ this.sessionHandlers.set(sessionId, handlers);
1800
+ Object.keys(handlers).forEach((ev) => {
1801
+ const h = handlers[ev];
1802
+ if (h)
1803
+ session.on(ev, h);
1804
+ });
1805
+ }
1806
+ detachSessionHandlers(sessionId, session) {
1807
+ const handlers = this.sessionHandlers.get(sessionId);
1808
+ if (!handlers || !session)
1809
+ return;
1810
+ Object.keys(handlers).forEach((ev) => {
1811
+ const h = handlers[ev];
1812
+ if (h)
1813
+ session.off(ev, h);
1814
+ });
1815
+ this.sessionHandlers.delete(sessionId);
1816
+ }
1817
+ cleanupSession(sessionId, session) {
1818
+ const targetSession = session ?? this.deps.sessionManager.getSession(sessionId) ?? this.deps.sessionManager.getRtc(sessionId)?.currentSession;
1819
+ if (targetSession) {
1820
+ this.detachSessionHandlers(sessionId, targetSession);
1821
+ }
1822
+ this.deps.micRecovery.disable(sessionId);
1823
+ this.deps.sessionManager.cleanupSession(sessionId);
1824
+ removeSessionState(this.deps.state, sessionId);
1825
+ }
1826
+ createSessionHandlersFor(sessionId, session) {
1827
+ const rtc = this.deps.sessionManager.getOrCreateRtc(sessionId, session);
1828
+ return createSessionHandlers({
1829
+ emitter: this.deps.emitter,
1830
+ state: this.deps.state,
1831
+ rtc,
1832
+ detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1833
+ enableMicrophoneRecovery: (confirmedSessionId) => this.deps.micRecovery.enable(confirmedSessionId),
1834
+ iceCandidateReadyDelayMs: this.deps.getIceCandidateReadyDelayMs(),
1835
+ sessionId
1836
+ });
1837
+ }
1838
+ resolveSessionId(sessionId) {
1839
+ if (sessionId)
1840
+ return sessionId;
1841
+ const state = this.deps.state.getState();
1842
+ const activeId = state.sessionIds.find(
1843
+ (id) => state.sessionsById[id]?.status === CallStatus.Active
1844
+ );
1845
+ return activeId ?? state.sessionIds[0] ?? null;
1846
+ }
1847
+ sessionExists(sessionId) {
1848
+ return !!this.deps.sessionManager.getSession(sessionId) || !!this.deps.sessionManager.getRtc(sessionId);
1849
+ }
1850
+ resolveExistingSessionId(sessionId) {
1851
+ const id = this.resolveSessionId(sessionId);
1852
+ if (!id)
1853
+ return null;
1854
+ return this.sessionExists(id) ? id : null;
1655
1855
  }
1656
1856
  };
1657
1857
 
1658
- // src/jssip-lib/sip/client.ts
1659
- var SESSION_DEBUG_KEY = "sip-debug-enabled";
1858
+ // src/core/modules/ua/ua.handlers.ts
1859
+ function createUAHandlers(deps) {
1860
+ const { emitter, state, cleanupAllSessions, onNewRTCSession } = deps;
1861
+ return {
1862
+ connecting: (e) => {
1863
+ emitter.emit("connecting", e);
1864
+ state.batchSet({ sipStatus: SipStatus.Connecting });
1865
+ },
1866
+ connected: (e) => {
1867
+ emitter.emit("connected", e);
1868
+ state.batchSet({ sipStatus: SipStatus.Connected });
1869
+ },
1870
+ disconnected: (e) => {
1871
+ emitter.emit("disconnected", e);
1872
+ cleanupAllSessions();
1873
+ state.reset();
1874
+ },
1875
+ registered: (e) => {
1876
+ emitter.emit("registered", e);
1877
+ state.batchSet({ sipStatus: SipStatus.Registered, error: null });
1878
+ },
1879
+ unregistered: (e) => {
1880
+ emitter.emit("unregistered", e);
1881
+ state.batchSet({ sipStatus: SipStatus.Unregistered });
1882
+ },
1883
+ registrationFailed: (e) => {
1884
+ emitter.emit("registrationFailed", e);
1885
+ cleanupAllSessions();
1886
+ state.batchSet({
1887
+ sipStatus: SipStatus.RegistrationFailed,
1888
+ error: e?.cause || "registration failed"
1889
+ });
1890
+ },
1891
+ newRTCSession: onNewRTCSession,
1892
+ newMessage: (e) => emitter.emit("newMessage", e),
1893
+ sipEvent: (e) => emitter.emit("sipEvent", e),
1894
+ newOptions: (e) => emitter.emit("newOptions", e)
1895
+ };
1896
+ }
1897
+
1898
+ // src/core/modules/ua/ua.module.ts
1899
+ var UaModule = class {
1900
+ constructor(deps) {
1901
+ this.userAgent = deps.userAgent;
1902
+ this.uaHandlers = deps.createHandlers();
1903
+ this.uaHandlerKeys = Object.keys(this.uaHandlers);
1904
+ }
1905
+ start(uri, password, config, debug) {
1906
+ this.userAgent.start(uri, password, config, { debug });
1907
+ this.attachHandlers();
1908
+ }
1909
+ stop() {
1910
+ this.detachHandlers();
1911
+ this.userAgent.stop();
1912
+ }
1913
+ register() {
1914
+ this.userAgent.register();
1915
+ }
1916
+ setDebug(debug) {
1917
+ this.userAgent.setDebug(debug);
1918
+ }
1919
+ attachHandlers() {
1920
+ const ua = this.userAgent.ua;
1921
+ if (!ua)
1922
+ return;
1923
+ this.detachHandlers();
1924
+ this.uaHandlerKeys.forEach((event) => {
1925
+ const handler = this.uaHandlers[event];
1926
+ if (handler)
1927
+ ua.on(event, handler);
1928
+ });
1929
+ }
1930
+ detachHandlers() {
1931
+ const ua = this.userAgent.ua;
1932
+ if (!ua)
1933
+ return;
1934
+ this.uaHandlerKeys.forEach((event) => {
1935
+ const handler = this.uaHandlers[event];
1936
+ if (handler)
1937
+ ua.off(event, handler);
1938
+ });
1939
+ }
1940
+ };
1941
+
1942
+ // src/core/client/sip.client.ts
1660
1943
  var SipClient = class extends EventTargetEmitter {
1661
1944
  constructor(options = {}) {
1662
1945
  super();
1663
1946
  this.userAgent = new SipUserAgent();
1664
1947
  this.stateStore = new SipStateStore();
1665
- this.sessionHandlers = /* @__PURE__ */ new Map();
1666
1948
  this.maxSessionCount = Infinity;
1667
1949
  this.sessionManager = new SessionManager();
1668
- this.errorHandler = options.errorHandler ?? new SipErrorHandler({
1669
- formatter: options.formatError,
1670
- messages: options.errorMessages
1671
- });
1950
+ this.unloadRuntime = new BrowserUnloadRuntime();
1672
1951
  this.debugPattern = options.debug;
1673
- this.uaHandlers = createUAHandlers({
1674
- emitter: this,
1675
- state: this.stateStore,
1676
- cleanupAllSessions: () => this.cleanupAllSessions(),
1677
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1678
- onNewRTCSession: (e) => this.onNewRTCSession(e)
1679
- });
1680
- this.uaHandlerKeys = Object.keys(this.uaHandlers);
1681
- this.lifecycle = new SessionLifecycle({
1682
- state: this.stateStore,
1683
- sessionManager: this.sessionManager,
1684
- emit: (event, payload) => this.emit(event, payload),
1685
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1686
- attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
1687
- getMaxSessionCount: () => this.maxSessionCount
1952
+ this.uaModule = new UaModule({
1953
+ userAgent: this.userAgent,
1954
+ createHandlers: () => createUAHandlers({
1955
+ emitter: this,
1956
+ state: this.stateStore,
1957
+ cleanupAllSessions: () => this.cleanupAllSessions(),
1958
+ onNewRTCSession: (e) => this.onNewRTCSession(e)
1959
+ })
1688
1960
  });
1689
1961
  this.micRecovery = new MicRecoveryManager({
1690
1962
  getRtc: (sessionId) => this.sessionManager.getRtc(sessionId),
1691
1963
  getSession: (sessionId) => this.sessionManager.getSession(sessionId),
1692
- getSessionState: (sessionId) => this.stateStore.getState().sessions.find((s) => s.id === sessionId),
1693
- setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream),
1694
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1695
- requestMicrophoneStream: (deviceId) => this.requestMicrophoneStreamInternal(deviceId)
1964
+ getSessionState: (sessionId) => this.stateStore.getState().sessionsById[sessionId],
1965
+ setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream)
1696
1966
  });
1697
- if (typeof window !== "undefined") {
1698
- window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
1699
- }
1967
+ this.sessionModule = new SessionModule({
1968
+ state: this.stateStore,
1969
+ emitter: this,
1970
+ sessionManager: this.sessionManager,
1971
+ micRecovery: this.micRecovery,
1972
+ getMaxSessionCount: () => this.maxSessionCount,
1973
+ getIceCandidateReadyDelayMs: () => this.iceCandidateReadyDelayMs
1974
+ });
1975
+ this.debugRuntime = new SipDebugRuntime({
1976
+ getState: () => this.stateStore.getPublicState(),
1977
+ onChange: (listener) => this.stateStore.onPublicChange(listener),
1978
+ getSessions: () => this.getSessions(),
1979
+ setDebugEnabled: (enabled) => this.sessionModule.setDebugEnabled(enabled)
1980
+ });
1981
+ this.debugRuntime.attachBridge(
1982
+ (debug) => this.setDebug(debug)
1983
+ );
1700
1984
  }
1701
1985
  get state() {
1702
- return this.stateStore.getState();
1986
+ return this.stateStore.getPublicState();
1703
1987
  }
1704
1988
  connect(uri, password, config) {
1705
1989
  this.disconnect();
@@ -1720,381 +2004,223 @@ var SipClient = class extends EventTargetEmitter {
1720
2004
  intervalMs: micRecoveryIntervalMs,
1721
2005
  maxRetries: micRecoveryMaxRetries
1722
2006
  });
1723
- const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
1724
- this.userAgent.start(uri, password, uaCfg, { debug });
1725
- this.lifecycle.setDebugEnabled(Boolean(debug));
1726
- this.attachUAHandlers();
1727
- this.attachBeforeUnload();
1728
- this.syncDebugInspector(debug);
2007
+ const debug = cfgDebug ?? this.debugRuntime.getPersistedDebug() ?? this.debugPattern;
2008
+ this.uaModule.start(uri, password, uaCfg, debug);
2009
+ this.sessionModule.setDebugEnabled(Boolean(debug));
2010
+ this.unloadRuntime.attach(() => {
2011
+ this.hangupAll();
2012
+ this.disconnect();
2013
+ });
2014
+ this.debugRuntime.syncInspector(debug);
1729
2015
  }
1730
2016
  registerUA() {
1731
- this.userAgent.register();
2017
+ this.uaModule.register();
1732
2018
  }
1733
2019
  disconnect() {
1734
- this.detachBeforeUnload();
1735
- this.detachUAHandlers();
1736
- this.userAgent.stop();
2020
+ this.unloadRuntime.detach();
2021
+ this.uaModule.stop();
1737
2022
  this.cleanupAllSessions();
1738
2023
  this.stateStore.reset();
2024
+ this.debugRuntime.cleanup();
1739
2025
  }
1740
2026
  call(target, callOptions = {}) {
1741
2027
  try {
1742
- const opts = this.ensureMediaConstraints(callOptions);
1743
2028
  const ua = this.userAgent.getUA();
1744
- const session = ua?.call(target, opts);
1745
- if (session && opts.mediaStream) {
1746
- const sessionId = String(session?.id ?? "");
2029
+ const session = ua?.call(target, callOptions);
2030
+ if (session && callOptions.mediaStream) {
2031
+ const sessionId = String(session.id ?? "");
1747
2032
  if (sessionId) {
1748
- this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
1749
- this.sessionManager.setSession(sessionId, session);
2033
+ this.sessionModule.setSessionMedia(
2034
+ sessionId,
2035
+ callOptions.mediaStream
2036
+ );
2037
+ this.sessionModule.setSession(sessionId, session);
1750
2038
  }
1751
2039
  }
1752
2040
  } catch (e) {
1753
- const err = this.emitError(e, "CALL_FAILED", "call failed");
2041
+ console.error(e);
1754
2042
  this.cleanupAllSessions();
1755
- this.stateStore.batchSet({
1756
- error: err.cause
1757
- });
1758
2043
  }
1759
2044
  }
1760
- answer(sessionId, options = {}) {
1761
- const resolved = this.resolveExistingSessionId(sessionId);
1762
- if (!resolved)
2045
+ sendMessage(target, body, options) {
2046
+ try {
2047
+ const ua = this.userAgent.getUA();
2048
+ if (!ua)
2049
+ return false;
2050
+ ua.sendMessage(target, body, options);
2051
+ return true;
2052
+ } catch (e) {
2053
+ console.error(e);
1763
2054
  return false;
1764
- return this.answerSession(resolved, options);
2055
+ }
1765
2056
  }
1766
- hangup(sessionId, options) {
1767
- const resolved = this.resolveExistingSessionId(sessionId);
1768
- if (!resolved)
2057
+ sendOptions(target, body, options) {
2058
+ try {
2059
+ const ua = this.userAgent.getUA();
2060
+ if (!ua)
2061
+ return false;
2062
+ const optionsUa = ua;
2063
+ if (typeof optionsUa.sendOptions !== "function")
2064
+ return false;
2065
+ optionsUa.sendOptions(target, body, options);
2066
+ return true;
2067
+ } catch (e) {
2068
+ console.error(e);
1769
2069
  return false;
1770
- return this.hangupSession(resolved, options);
2070
+ }
1771
2071
  }
1772
2072
  hangupAll(options) {
1773
2073
  const ids = this.getSessionIds();
1774
2074
  ids.forEach((id) => this.hangupSession(id, options));
1775
2075
  return ids.length > 0;
1776
2076
  }
1777
- toggleMute(sessionId) {
1778
- return this.toggleMuteSession(sessionId);
1779
- }
1780
- toggleHold(sessionId) {
1781
- return this.toggleHoldSession(sessionId);
1782
- }
1783
- sendDTMF(sessionId, tones, options) {
1784
- return this.sendDTMFSession(sessionId, tones, options);
1785
- }
1786
- transfer(sessionId, target, options) {
1787
- return this.transferSession(sessionId, target, options);
1788
- }
1789
2077
  onChange(fn) {
1790
- return this.stateStore.onChange(fn);
1791
- }
1792
- attachUAHandlers() {
1793
- const ua = this.userAgent.ua;
1794
- if (!ua)
1795
- return;
1796
- this.detachUAHandlers();
1797
- this.uaHandlerKeys.forEach((ev) => {
1798
- const h = this.uaHandlers[ev];
1799
- if (h)
1800
- ua.on(ev, h);
1801
- });
2078
+ return this.stateStore.onPublicChange(fn);
1802
2079
  }
1803
2080
  setDebug(debug) {
1804
2081
  this.debugPattern = debug;
1805
- this.userAgent.setDebug(debug);
1806
- this.lifecycle.setDebugEnabled(Boolean(debug));
1807
- this.syncDebugInspector(debug);
1808
- }
1809
- attachSessionHandlers(sessionId, session) {
1810
- const handlers = this.createSessionHandlersFor(sessionId, session);
1811
- this.sessionHandlers.set(sessionId, handlers);
1812
- Object.keys(handlers).forEach((ev) => {
1813
- const h = handlers[ev];
1814
- if (h)
1815
- session.on(ev, h);
1816
- });
1817
- }
1818
- detachSessionHandlers(sessionId, session) {
1819
- const handlers = this.sessionHandlers.get(sessionId);
1820
- if (!handlers || !session)
1821
- return;
1822
- Object.keys(handlers).forEach((ev) => {
1823
- const h = handlers[ev];
1824
- if (h)
1825
- session.off(ev, h);
1826
- });
1827
- this.sessionHandlers.delete(sessionId);
1828
- }
1829
- detachUAHandlers() {
1830
- const ua = this.userAgent.ua;
1831
- if (!ua)
1832
- return;
1833
- this.uaHandlerKeys.forEach((ev) => {
1834
- const h = this.uaHandlers[ev];
1835
- if (h)
1836
- ua.off(ev, h);
1837
- });
1838
- }
1839
- cleanupSession(sessionId, session) {
1840
- const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1841
- this.detachSessionHandlers(sessionId, targetSession);
1842
- this.micRecovery.disable(sessionId);
1843
- this.sessionManager.cleanupSession(sessionId);
1844
- removeSessionState(this.stateStore, sessionId);
2082
+ this.uaModule.setDebug(debug);
2083
+ this.sessionModule.setDebugEnabled(Boolean(debug));
2084
+ const effectiveDebug = debug ?? this.debugRuntime.getPersistedDebug() ?? this.debugPattern;
2085
+ this.debugRuntime.syncInspector(effectiveDebug);
1845
2086
  }
1846
2087
  cleanupAllSessions() {
1847
- this.sessionManager.cleanupAllSessions();
1848
- this.micRecovery.cleanupAll();
1849
- this.sessionHandlers.clear();
1850
- this.stateStore.setState({
1851
- sessions: [],
1852
- error: null
1853
- });
1854
- }
1855
- createSessionHandlersFor(sessionId, session) {
1856
- const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
1857
- return createSessionHandlers({
1858
- emitter: this,
1859
- state: this.stateStore,
1860
- rtc,
1861
- detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1862
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1863
- onSessionFailed: (err, event) => this.onSessionFailed(err, event),
1864
- enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
1865
- iceCandidateReadyDelayMs: this.iceCandidateReadyDelayMs,
1866
- sessionId
1867
- });
2088
+ this.sessionModule.cleanupAllSessions();
1868
2089
  }
1869
2090
  onNewRTCSession(e) {
1870
- this.lifecycle.handleNewRTCSession(e);
1871
- }
1872
- onSessionFailed(error, event) {
1873
- const rawCause = event?.cause ?? error;
1874
- const statusCode = event?.message?.status_code;
1875
- const statusText = event?.message?.reason_phrase;
1876
- const causeText = rawCause || (statusCode ? `${statusCode}${statusText ? " " + statusText : ""}` : "call failed");
1877
- this.emitError(
1878
- { raw: event, cause: rawCause, statusCode, statusText },
1879
- "SESSION_FAILED",
1880
- causeText
1881
- );
1882
- }
1883
- emitError(raw, code, fallback) {
1884
- const payload = this.errorHandler.format({ raw, code, fallback });
1885
- this.emit("error", payload);
1886
- return payload;
1887
- }
1888
- resolveSessionId(sessionId) {
1889
- if (sessionId)
1890
- return sessionId;
1891
- const sessions = this.stateStore.getState().sessions;
1892
- const active = sessions.find((s) => s.status === CallStatus.Active);
1893
- return active?.id ?? sessions[0]?.id ?? null;
1894
- }
1895
- sessionExists(sessionId) {
1896
- return !!this.sessionManager.getSession(sessionId) || !!this.sessionManager.getRtc(sessionId);
1897
- }
1898
- resolveExistingSessionId(sessionId) {
1899
- const id = this.resolveSessionId(sessionId);
1900
- if (!id)
1901
- return null;
1902
- return this.sessionExists(id) ? id : null;
1903
- }
1904
- ensureMediaConstraints(opts) {
1905
- if (opts.mediaStream || opts.mediaConstraints)
1906
- return opts;
1907
- return { ...opts, mediaConstraints: { audio: true, video: false } };
2091
+ this.sessionModule.handleNewRTCSession(e);
1908
2092
  }
1909
2093
  answerSession(sessionId, options = {}) {
1910
- if (!sessionId || !this.sessionExists(sessionId))
1911
- return false;
1912
- const opts = this.ensureMediaConstraints(options);
1913
- if (opts.mediaStream) {
1914
- this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
2094
+ if (options.mediaStream) {
2095
+ this.sessionModule.setSessionMedia(sessionId, options.mediaStream);
1915
2096
  }
1916
- return this.sessionManager.answer(sessionId, opts);
2097
+ return this.sessionModule.answerSession(sessionId, options);
1917
2098
  }
1918
2099
  hangupSession(sessionId, options) {
1919
- if (!sessionId || !this.sessionExists(sessionId))
1920
- return false;
1921
- return this.sessionManager.hangup(sessionId, options);
2100
+ return this.sessionModule.hangupSession(sessionId, options);
1922
2101
  }
1923
2102
  toggleMuteSession(sessionId) {
1924
- const resolved = this.resolveExistingSessionId(sessionId);
1925
- if (!resolved)
1926
- return false;
1927
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1928
- const muted = sessionState?.muted ?? false;
1929
- if (muted) {
1930
- this.sessionManager.unmute(resolved);
1931
- return true;
1932
- }
1933
- this.sessionManager.mute(resolved);
1934
- return true;
2103
+ return this.sessionModule.toggleMuteSession(sessionId);
1935
2104
  }
1936
2105
  toggleHoldSession(sessionId) {
1937
- const resolved = this.resolveExistingSessionId(sessionId);
1938
- if (!resolved)
1939
- return false;
1940
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1941
- const isOnHold = sessionState?.status === CallStatus.Hold;
1942
- if (isOnHold) {
1943
- this.sessionManager.unhold(resolved);
1944
- return true;
1945
- }
1946
- if (sessionState?.status === CallStatus.Active) {
1947
- this.sessionManager.hold(resolved);
1948
- return true;
1949
- }
1950
- return true;
2106
+ return this.sessionModule.toggleHoldSession(sessionId);
1951
2107
  }
1952
2108
  sendDTMFSession(sessionId, tones, options) {
1953
- const resolved = this.resolveExistingSessionId(sessionId);
1954
- if (!resolved)
1955
- return false;
1956
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1957
- if (sessionState?.status === CallStatus.Active)
1958
- this.sessionManager.sendDTMF(resolved, tones, options);
1959
- return true;
2109
+ return this.sessionModule.sendDTMFSession(sessionId, tones, options);
1960
2110
  }
1961
2111
  transferSession(sessionId, target, options) {
1962
- const resolved = this.resolveExistingSessionId(sessionId);
1963
- if (!resolved)
1964
- return false;
1965
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1966
- if (sessionState?.status === CallStatus.Active)
1967
- this.sessionManager.transfer(resolved, target, options);
1968
- return true;
2112
+ return this.sessionModule.transferSession(sessionId, target, options);
1969
2113
  }
1970
- setSessionMedia(sessionId, stream) {
1971
- this.sessionManager.setSessionMedia(sessionId, stream);
2114
+ sendInfoSession(sessionId, contentType, body, options) {
2115
+ return this.sessionModule.sendInfoSession(
2116
+ sessionId,
2117
+ contentType,
2118
+ body,
2119
+ options
2120
+ );
1972
2121
  }
1973
- switchCameraSession(sessionId, track) {
1974
- if (!this.sessionExists(sessionId))
1975
- return false;
1976
- const rtc = this.sessionManager.getRtc(sessionId);
1977
- return rtc ? rtc.switchCamera(track) : false;
2122
+ updateSession(sessionId, options) {
2123
+ return this.sessionModule.updateSession(sessionId, options);
1978
2124
  }
1979
- enableVideoSession(sessionId) {
1980
- if (!this.sessionExists(sessionId))
1981
- return false;
1982
- const rtc = this.sessionManager.getRtc(sessionId);
1983
- rtc?.enableVideo();
1984
- return !!rtc;
2125
+ reinviteSession(sessionId, options) {
2126
+ return this.sessionModule.updateSession(sessionId, options);
1985
2127
  }
1986
- disableVideoSession(sessionId) {
1987
- if (!this.sessionExists(sessionId))
1988
- return false;
1989
- const rtc = this.sessionManager.getRtc(sessionId);
1990
- rtc?.disableVideo();
1991
- return !!rtc;
2128
+ setSessionMedia(sessionId, stream) {
2129
+ this.sessionModule.setSessionMedia(sessionId, stream);
1992
2130
  }
1993
2131
  getSession(sessionId) {
1994
- return this.sessionManager.getSession(sessionId);
2132
+ return this.sessionModule.getSession(sessionId);
1995
2133
  }
1996
2134
  getSessionIds() {
1997
- return this.sessionManager.getSessionIds();
2135
+ return this.sessionModule.getSessionIds();
1998
2136
  }
1999
2137
  getSessions() {
2000
- return this.sessionManager.getSessions();
2001
- }
2002
- attachBeforeUnload() {
2003
- if (typeof window === "undefined" || this.unloadHandler)
2004
- return;
2005
- const handler = () => {
2006
- this.hangupAll();
2007
- this.disconnect();
2008
- };
2009
- window.addEventListener("beforeunload", handler);
2010
- this.unloadHandler = handler;
2011
- }
2012
- detachBeforeUnload() {
2013
- if (typeof window === "undefined" || !this.unloadHandler)
2014
- return;
2015
- window.removeEventListener("beforeunload", this.unloadHandler);
2016
- this.unloadHandler = void 0;
2017
- }
2018
- syncDebugInspector(debug) {
2019
- if (typeof window === "undefined")
2020
- return;
2021
- const persisted = this.getPersistedDebug();
2022
- const effectiveDebug = debug ?? persisted ?? this.debugPattern;
2023
- this.lifecycle.setDebugEnabled(Boolean(effectiveDebug));
2024
- this.toggleStateLogger(Boolean(effectiveDebug));
2025
- const win = window;
2026
- const disabledInspector = () => {
2027
- console.warn("SIP debug inspector disabled; enable debug to inspect.");
2028
- return null;
2029
- };
2030
- win.sipState = () => effectiveDebug ? this.stateStore.getState() : disabledInspector();
2031
- win.sipSessions = () => effectiveDebug ? this.getSessions() : disabledInspector();
2032
- }
2033
- toggleStateLogger(enabled) {
2034
- if (!enabled) {
2035
- this.stateLogOff?.();
2036
- this.stateLogOff = void 0;
2037
- return;
2038
- }
2039
- if (this.stateLogOff)
2040
- return;
2041
- let prev = this.stateStore.getState();
2042
- console.info("[sip][state]", { initial: true }, prev);
2043
- this.stateLogOff = this.stateStore.onChange((next) => {
2044
- console.info("[sip][state]", next);
2045
- prev = next;
2046
- });
2047
- }
2048
- getPersistedDebug() {
2049
- if (typeof window === "undefined")
2050
- return void 0;
2051
- try {
2052
- const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
2053
- if (!persisted)
2054
- return void 0;
2055
- return persisted;
2056
- } catch {
2057
- return void 0;
2058
- }
2059
- }
2060
- async requestMicrophoneStreamInternal(deviceId) {
2061
- if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
2062
- throw new Error("getUserMedia not available");
2063
- }
2064
- const audio = deviceId && deviceId !== "default" ? { deviceId: { exact: deviceId } } : true;
2065
- try {
2066
- return await navigator.mediaDevices.getUserMedia({ audio });
2067
- } catch (err) {
2068
- const cause = err?.name || "getUserMedia failed";
2069
- this.emitError(
2070
- { raw: err, cause },
2071
- "MICROPHONE_UNAVAILABLE",
2072
- "microphone unavailable"
2073
- );
2074
- throw err;
2075
- }
2138
+ return this.sessionModule.getSessions();
2076
2139
  }
2077
2140
  };
2078
2141
  function createSipClientInstance(options) {
2079
2142
  return new SipClient(options);
2080
2143
  }
2081
- function createSipEventManager(client) {
2144
+
2145
+ // src/core/modules/media/media.module.ts
2146
+ function createMediaModule(deps) {
2147
+ const { client, eventManager } = deps;
2082
2148
  return {
2083
- onUA(event, handler) {
2084
- return client.on(event, handler);
2149
+ getSession(sessionId) {
2150
+ return client.getSession(sessionId);
2085
2151
  },
2086
- onSession(sessionId, event, handler) {
2152
+ observePeerConnection(sessionId, onPeerConnection) {
2087
2153
  const session = client.getSession(sessionId);
2088
- if (!session)
2154
+ if (!session) {
2155
+ onPeerConnection(null);
2089
2156
  return () => {
2090
2157
  };
2091
- session.on(event, handler);
2092
- return () => session.off(event, handler);
2158
+ }
2159
+ const initialPc = session.connection ?? null;
2160
+ onPeerConnection(initialPc);
2161
+ return eventManager.onSession(sessionId, "peerconnection", (payload) => {
2162
+ const pc = payload?.peerconnection ?? null;
2163
+ onPeerConnection(pc);
2164
+ });
2165
+ },
2166
+ buildRemoteStream(peerConnection) {
2167
+ if (!peerConnection || typeof peerConnection.getReceivers !== "function") {
2168
+ return null;
2169
+ }
2170
+ const tracks = peerConnection.getReceivers().map((receiver) => receiver.track).filter((track) => Boolean(track));
2171
+ if (tracks.length === 0)
2172
+ return null;
2173
+ return new MediaStream(tracks);
2093
2174
  }
2094
2175
  };
2095
2176
  }
2177
+
2178
+ // src/core/kernel/createSipKernel.ts
2179
+ function createSipKernel() {
2180
+ const client = createSipClientInstance();
2181
+ const eventManager = createSipEventManager(client);
2182
+ const media = createMediaModule({ client, eventManager });
2183
+ return {
2184
+ client,
2185
+ store: {
2186
+ getState: () => client.state,
2187
+ subscribe: (onStoreChange) => client.onChange(onStoreChange)
2188
+ },
2189
+ commands: {
2190
+ connect: (uri, password, config) => client.connect(uri, password, config),
2191
+ disconnect: () => client.disconnect(),
2192
+ register: () => client.registerUA(),
2193
+ setDebug: (debug) => client.setDebug(debug),
2194
+ call: (target, options) => client.call(target, options),
2195
+ sendMessage: (target, body, options) => client.sendMessage(target, body, options),
2196
+ sendOptions: (target, body, options) => client.sendOptions(target, body, options),
2197
+ answer: (sessionId, options) => client.answerSession(sessionId, options),
2198
+ hangup: (sessionId, options) => client.hangupSession(sessionId, options),
2199
+ hangupAll: (options) => client.hangupAll(options),
2200
+ toggleMute: (sessionId) => client.toggleMuteSession(sessionId),
2201
+ toggleHold: (sessionId) => client.toggleHoldSession(sessionId),
2202
+ sendDTMF: (sessionId, tones, options) => client.sendDTMFSession(sessionId, tones, options),
2203
+ transfer: (sessionId, target, options) => client.transferSession(sessionId, target, options),
2204
+ sendInfo: (sessionId, contentType, body, options) => client.sendInfoSession(sessionId, contentType, body, options),
2205
+ update: (sessionId, options) => client.updateSession(sessionId, options),
2206
+ reinvite: (sessionId, options) => client.reinviteSession(sessionId, options),
2207
+ getSession: (sessionId) => client.getSession(sessionId),
2208
+ getSessionIds: () => client.getSessionIds(),
2209
+ getSessions: () => client.getSessions(),
2210
+ setSessionMedia: (sessionId, stream) => client.setSessionMedia(sessionId, stream)
2211
+ },
2212
+ events: {
2213
+ onUA: (event, handler) => eventManager.onUA(event, handler),
2214
+ onSession: (sessionId, event, handler) => eventManager.onSession(sessionId, event, handler)
2215
+ },
2216
+ eventManager,
2217
+ media
2218
+ };
2219
+ }
2096
2220
  var SipContext = react.createContext(null);
2097
- function useSip() {
2221
+
2222
+ // src/hooks/useSip.ts
2223
+ function useSipKernel() {
2098
2224
  const ctx = react.useContext(SipContext);
2099
2225
  if (!ctx)
2100
2226
  throw new Error("Must be used within SipProvider");
@@ -2103,177 +2229,174 @@ function useSip() {
2103
2229
 
2104
2230
  // src/hooks/useSipState.ts
2105
2231
  function useSipState() {
2106
- const { client } = useSip();
2107
- const subscribe = react.useCallback(
2108
- (onStoreChange) => client.onChange(onStoreChange),
2109
- [client]
2110
- );
2111
- const getSnapshot = react.useCallback(() => client.state, [client]);
2112
- return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
2232
+ const { store } = useSipKernel();
2233
+ return react.useSyncExternalStore(store.subscribe, store.getState, store.getState);
2113
2234
  }
2114
2235
  function useSipActions() {
2115
- const { client } = useSip();
2236
+ const { commands } = useSipKernel();
2116
2237
  return react.useMemo(
2117
2238
  () => ({
2118
- call: (...args) => client.call(...args),
2119
- answer: (...args) => client.answerSession(...args),
2120
- hangup: (...args) => client.hangupSession(...args),
2121
- toggleMute: (...args) => client.toggleMuteSession(...args),
2122
- toggleHold: (...args) => client.toggleHoldSession(...args),
2123
- sendDTMF: (...args) => client.sendDTMFSession(...args),
2124
- transfer: (...args) => client.transferSession(...args),
2125
- getSession: (...args) => client.getSession(...args),
2126
- getSessionIds: () => client.getSessionIds(),
2127
- getSessions: () => client.getSessions(),
2128
- setSessionMedia: (...args) => client.setSessionMedia(...args),
2129
- switchCamera: (...args) => client.switchCameraSession(...args),
2130
- enableVideo: (...args) => client.enableVideoSession(...args),
2131
- disableVideo: (...args) => client.disableVideoSession(...args)
2239
+ connect: commands.connect,
2240
+ disconnect: commands.disconnect,
2241
+ register: commands.register,
2242
+ setDebug: commands.setDebug,
2243
+ call: commands.call,
2244
+ sendMessage: commands.sendMessage,
2245
+ sendOptions: commands.sendOptions,
2246
+ answer: commands.answer,
2247
+ hangup: commands.hangup,
2248
+ hangupAll: commands.hangupAll,
2249
+ toggleMute: commands.toggleMute,
2250
+ toggleHold: commands.toggleHold,
2251
+ sendDTMF: commands.sendDTMF,
2252
+ transfer: commands.transfer,
2253
+ sendInfo: commands.sendInfo,
2254
+ update: commands.update,
2255
+ reinvite: commands.reinvite,
2256
+ getSession: commands.getSession,
2257
+ getSessionIds: commands.getSessionIds,
2258
+ getSessions: commands.getSessions,
2259
+ setSessionMedia: commands.setSessionMedia
2132
2260
  }),
2133
- [client]
2261
+ [commands]
2134
2262
  );
2135
2263
  }
2264
+ function useSipSelector(selector, equalityFn = Object.is) {
2265
+ const { store } = useSipKernel();
2266
+ const selectorRef = react.useRef(selector);
2267
+ const equalityFnRef = react.useRef(equalityFn);
2268
+ const selectedRef = react.useRef(void 0);
2269
+ const hasSelectedRef = react.useRef(false);
2270
+ selectorRef.current = selector;
2271
+ equalityFnRef.current = equalityFn;
2272
+ const getSelection = () => {
2273
+ const nextSelected = selectorRef.current(store.getState());
2274
+ if (hasSelectedRef.current && equalityFnRef.current(selectedRef.current, nextSelected)) {
2275
+ return selectedRef.current;
2276
+ }
2277
+ hasSelectedRef.current = true;
2278
+ selectedRef.current = nextSelected;
2279
+ return nextSelected;
2280
+ };
2281
+ return react.useSyncExternalStore(store.subscribe, getSelection, getSelection);
2282
+ }
2136
2283
 
2137
- // src/hooks/useSipSessions.ts
2284
+ // src/hooks/useSipSession.ts
2285
+ function useSipSession(sessionId) {
2286
+ return useSipSelector((state) => {
2287
+ if (!sessionId)
2288
+ return null;
2289
+ return state.sessions.find((session) => session.id === sessionId) ?? null;
2290
+ });
2291
+ }
2138
2292
  function useSipSessions() {
2139
- const { sessions } = useSipState();
2140
- return { sessions };
2293
+ const sessions = useSipSelector((state) => state.sessions);
2294
+ return react.useMemo(() => ({ sessions }), [sessions]);
2141
2295
  }
2142
2296
  function useSipEvent(event, handler) {
2143
- const { sipEventManager } = useSip();
2297
+ const { events } = useSipKernel();
2144
2298
  react.useEffect(() => {
2145
2299
  if (!handler)
2146
2300
  return;
2147
- return sipEventManager.onUA(event, handler);
2148
- }, [event, handler, sipEventManager]);
2301
+ return events.onUA(event, handler);
2302
+ }, [event, handler, events]);
2149
2303
  }
2150
2304
  function useSipSessionEvent(sessionId, event, handler) {
2151
- const { sipEventManager } = useSip();
2305
+ const { events } = useSipKernel();
2152
2306
  react.useEffect(() => {
2153
2307
  if (!handler)
2154
2308
  return;
2155
- return sipEventManager.onSession(sessionId, event, handler);
2156
- }, [event, handler, sessionId, sipEventManager]);
2309
+ return events.onSession(sessionId, event, handler);
2310
+ }, [event, handler, sessionId, events]);
2157
2311
  }
2158
-
2159
- // src/jssip-lib/adapters/dom/createCallPlayer.ts
2160
- function createCallPlayer(audioEl) {
2161
- let cleanupTrackListener = null;
2162
- let cleanupSessionPeerListener = null;
2163
- let cleanupClientListeners = null;
2164
- const dispose = (fn) => {
2165
- if (fn)
2166
- fn();
2167
- return null;
2168
- };
2169
- function clearAudioStream(stream) {
2170
- if (stream) {
2171
- for (const t of stream.getTracks()) {
2172
- t.stop();
2173
- }
2312
+ function useSessionMedia(sessionId) {
2313
+ const { media } = useSipKernel();
2314
+ const sessions = useSipSelector((state) => state.sessions);
2315
+ const [peerConnection, setPeerConnection] = react.useState(null);
2316
+ const [remoteStream, setRemoteStream] = react.useState(null);
2317
+ const resolvedSessionId = react.useMemo(() => {
2318
+ if (sessionId)
2319
+ return sessionId;
2320
+ const active = sessions.find((s) => s.status === CallStatus.Active);
2321
+ return active?.id ?? sessions[0]?.id;
2322
+ }, [sessionId, sessions]);
2323
+ const session = react.useMemo(
2324
+ () => resolvedSessionId ? media.getSession(resolvedSessionId) : null,
2325
+ [media, resolvedSessionId]
2326
+ );
2327
+ const sessionState = react.useMemo(() => {
2328
+ if (!resolvedSessionId)
2329
+ return null;
2330
+ return sessions.find((s) => s.id === resolvedSessionId) ?? null;
2331
+ }, [sessions, resolvedSessionId]);
2332
+ react.useEffect(() => {
2333
+ if (!resolvedSessionId) {
2334
+ setPeerConnection(null);
2335
+ setRemoteStream(null);
2336
+ return;
2174
2337
  }
2175
- audioEl.srcObject = null;
2176
- }
2177
- const attachTracks = (pc) => {
2178
- const onTrack = (e) => {
2179
- if (e.track.kind !== "audio")
2180
- return;
2181
- const nextStream = e.streams?.[0] ?? new MediaStream([e.track]);
2182
- const prev = audioEl.srcObject;
2183
- if (prev && prev !== nextStream) {
2184
- clearAudioStream(prev);
2185
- }
2186
- audioEl.srcObject = nextStream;
2187
- audioEl.play?.().catch(() => {
2188
- });
2189
- };
2190
- pc.addEventListener("track", onTrack);
2191
- return () => pc.removeEventListener("track", onTrack);
2192
- };
2193
- const listenSessionPeerconnection = (session) => {
2194
- const onPeer = (data) => {
2195
- cleanupTrackListener = dispose(cleanupTrackListener);
2196
- cleanupTrackListener = attachTracks(data.peerconnection);
2197
- };
2198
- session.on("peerconnection", onPeer);
2199
- return () => session.off("peerconnection", onPeer);
2200
- };
2201
- function bindToSession(session) {
2202
- clearAudioStream(audioEl.srcObject);
2203
- if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
2204
- cleanupTrackListener = dispose(cleanupTrackListener);
2205
- cleanupTrackListener = attachTracks(session.connection);
2338
+ const off = media.observePeerConnection(resolvedSessionId, (pc) => {
2339
+ setPeerConnection(pc);
2340
+ setRemoteStream(media.buildRemoteStream(pc));
2341
+ });
2342
+ return off;
2343
+ }, [media, resolvedSessionId]);
2344
+ react.useEffect(() => {
2345
+ if (!peerConnection) {
2346
+ setRemoteStream(null);
2347
+ return;
2206
2348
  }
2207
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2208
- cleanupSessionPeerListener = listenSessionPeerconnection(session);
2349
+ const update = () => {
2350
+ setRemoteStream(media.buildRemoteStream(peerConnection));
2351
+ };
2352
+ peerConnection.addEventListener("track", update);
2353
+ peerConnection.addEventListener("connectionstatechange", update);
2354
+ peerConnection.addEventListener("iceconnectionstatechange", update);
2355
+ update();
2209
2356
  return () => {
2210
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2211
- cleanupTrackListener = dispose(cleanupTrackListener);
2357
+ peerConnection.removeEventListener("track", update);
2358
+ peerConnection.removeEventListener("connectionstatechange", update);
2359
+ peerConnection.removeEventListener("iceconnectionstatechange", update);
2212
2360
  };
2213
- }
2214
- function bindToClient(client) {
2215
- const offNew = client.on("newRTCSession", (payload) => {
2216
- const e = payload?.data;
2217
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2218
- cleanupTrackListener = dispose(cleanupTrackListener);
2219
- clearAudioStream(audioEl.srcObject);
2220
- if (!e?.session)
2221
- return;
2222
- cleanupSessionPeerListener = listenSessionPeerconnection(e.session);
2223
- if (e.session.direction === "outgoing" && e.session.connection instanceof RTCPeerConnection) {
2224
- cleanupTrackListener = attachTracks(e.session.connection);
2225
- }
2226
- });
2227
- const offEnded = client.on("ended", () => detach());
2228
- const offFailed = client.on("failed", () => detach());
2229
- const offDisconnected = client.on("disconnected", () => detach());
2230
- cleanupClientListeners = () => {
2231
- offNew();
2232
- offEnded();
2233
- offFailed();
2234
- offDisconnected();
2361
+ }, [media, peerConnection]);
2362
+ const tracks = remoteStream?.getTracks() ?? [];
2363
+ const audioTracks = tracks.filter((track) => track.kind === "audio");
2364
+ if (!sessionState) {
2365
+ return {
2366
+ sessionId: resolvedSessionId ?? "",
2367
+ session,
2368
+ peerConnection,
2369
+ remoteStream,
2370
+ audioTracks
2235
2371
  };
2236
- return cleanupClientListeners;
2237
- }
2238
- function detach() {
2239
- cleanupClientListeners = dispose(cleanupClientListeners);
2240
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2241
- cleanupTrackListener = dispose(cleanupTrackListener);
2242
- clearAudioStream(audioEl.srcObject);
2243
2372
  }
2244
2373
  return {
2245
- bindToSession,
2246
- bindToClient,
2247
- detach
2374
+ sessionId: sessionState.id,
2375
+ session,
2376
+ peerConnection,
2377
+ remoteStream,
2378
+ audioTracks
2248
2379
  };
2249
2380
  }
2250
2381
  function CallPlayer({ sessionId }) {
2251
- const { client } = useSip();
2382
+ const { remoteStream } = useSessionMedia(sessionId);
2252
2383
  const audioRef = react.useRef(null);
2253
2384
  react.useEffect(() => {
2254
- if (!audioRef.current)
2385
+ const audioEl = audioRef.current;
2386
+ if (!audioEl)
2255
2387
  return;
2256
- const player = createCallPlayer(audioRef.current);
2257
- const session = sessionId ? client.getSession(sessionId) : null;
2258
- const off = session ? player.bindToSession(session) : player.bindToClient(client);
2388
+ audioEl.srcObject = remoteStream;
2389
+ audioEl.play?.().catch(() => {
2390
+ });
2259
2391
  return () => {
2260
- off?.();
2261
- player.detach();
2392
+ audioEl.srcObject = null;
2262
2393
  };
2263
- }, [client, sessionId]);
2394
+ }, [remoteStream]);
2264
2395
  return /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true });
2265
2396
  }
2266
- function SipProvider({
2267
- client,
2268
- children,
2269
- sipEventManager
2270
- }) {
2271
- const manager = react.useMemo(
2272
- () => sipEventManager ?? createSipEventManager(client),
2273
- [client, sipEventManager]
2274
- );
2275
- const contextValue = react.useMemo(() => ({ client, sipEventManager: manager }), [client, manager]);
2276
- return /* @__PURE__ */ jsxRuntime.jsx(SipContext.Provider, { value: contextValue, children });
2397
+ function SipProvider(props) {
2398
+ const contextValue = react.useMemo(() => props.kernel, [props.kernel]);
2399
+ return /* @__PURE__ */ jsxRuntime.jsx(SipContext.Provider, { value: contextValue, children: props.children });
2277
2400
  }
2278
2401
 
2279
2402
  Object.defineProperty(exports, "WebSocketInterface", {
@@ -2282,14 +2405,17 @@ Object.defineProperty(exports, "WebSocketInterface", {
2282
2405
  });
2283
2406
  exports.CallPlayer = CallPlayer;
2284
2407
  exports.CallStatus = CallStatus;
2285
- exports.SipContext = SipContext;
2286
2408
  exports.SipProvider = SipProvider;
2287
2409
  exports.SipStatus = SipStatus;
2288
2410
  exports.createSipClientInstance = createSipClientInstance;
2289
2411
  exports.createSipEventManager = createSipEventManager;
2290
- exports.useSip = useSip;
2412
+ exports.createSipKernel = createSipKernel;
2413
+ exports.useSessionMedia = useSessionMedia;
2291
2414
  exports.useSipActions = useSipActions;
2292
2415
  exports.useSipEvent = useSipEvent;
2416
+ exports.useSipKernel = useSipKernel;
2417
+ exports.useSipSelector = useSipSelector;
2418
+ exports.useSipSession = useSipSession;
2293
2419
  exports.useSipSessionEvent = useSipSessionEvent;
2294
2420
  exports.useSipSessions = useSipSessions;
2295
2421
  exports.useSipState = useSipState;