react-jssip-kit 0.7.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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,7 +290,7 @@ 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();
@@ -385,468 +342,90 @@ var SipStateStore = class {
385
342
  }
386
343
  };
387
344
 
388
- // src/jssip-lib/sip/handlers/uaHandlers.ts
389
- function createUAHandlers(deps) {
390
- const { emitter, state, cleanupAllSessions, emitError, onNewRTCSession } = deps;
391
- 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 });
412
- },
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"
429
- });
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)
345
+ // src/core/modules/debug/sip-debug.runtime.ts
346
+ var SESSION_DEBUG_KEY = "sip-debug-enabled";
347
+ var SipDebugRuntime = class {
348
+ constructor(deps) {
349
+ this.deps = deps;
350
+ }
351
+ attachBridge(setDebug) {
352
+ if (typeof window === "undefined")
443
353
  return;
444
- if (s.status === CallStatus.Active) {
445
- holdFn(s.id);
354
+ window.sipDebugBridge = (debug) => setDebug(debug ?? true);
355
+ }
356
+ getPersistedDebug() {
357
+ if (typeof window === "undefined")
358
+ return void 0;
359
+ try {
360
+ const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
361
+ if (!persisted)
362
+ return void 0;
363
+ return persisted;
364
+ } catch {
365
+ return void 0;
446
366
  }
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
- };
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
- }
367
+ }
368
+ syncInspector(effectiveDebug) {
369
+ if (typeof window === "undefined")
370
+ return;
371
+ const enabled = Boolean(effectiveDebug);
372
+ this.deps.setDebugEnabled(enabled);
373
+ this.toggleStateLogger(enabled);
374
+ const win = window;
375
+ const disabledInspector = () => {
376
+ console.warn("SIP debug inspector disabled; enable debug to inspect.");
377
+ return null;
378
+ };
379
+ win.sipState = () => enabled ? this.deps.getState() : disabledInspector();
380
+ win.sipSessions = () => enabled ? this.deps.getSessions() : disabledInspector();
381
+ }
382
+ cleanup() {
383
+ this.toggleStateLogger(false);
384
+ }
385
+ toggleStateLogger(enabled) {
386
+ if (!enabled) {
387
+ this.stateLogOff?.();
388
+ this.stateLogOff = void 0;
389
+ return;
390
+ }
391
+ if (this.stateLogOff)
392
+ return;
393
+ let prev = this.deps.getState();
394
+ console.info("[sip][state]", { initial: true }, prev);
395
+ this.stateLogOff = this.deps.onChange((next) => {
396
+ console.info("[sip][state]", next);
397
+ prev = next;
398
+ });
399
+ }
400
+ };
475
401
 
476
- // src/jssip-lib/sip/handlers/sessionHandlers.ts
477
- function createSessionHandlers(deps) {
478
- const {
479
- emitter,
480
- state,
481
- rtc,
482
- detachSessionHandlers,
483
- onSessionFailed,
484
- sessionId
485
- } = deps;
402
+ // src/core/modules/event/sip-event-manager.adapter.ts
403
+ function createSipEventManager(client) {
486
404
  return {
487
- progress: (e) => {
488
- emitter.emit("progress", e);
489
- },
490
- accepted: (e) => {
491
- emitter.emit("accepted", e);
492
- state.batchSet({
493
- sessions: state.getState().sessions.map(
494
- (s) => s.id === sessionId ? {
495
- ...s,
496
- status: CallStatus.Active,
497
- acceptedAt: s.acceptedAt ?? Date.now()
498
- } : s
499
- )
500
- });
501
- },
502
- confirmed: (e) => {
503
- emitter.emit("confirmed", e);
504
- deps.enableMicrophoneRecovery?.(sessionId);
505
- },
506
- ended: (e) => {
507
- emitter.emit("ended", e);
508
- detachSessionHandlers();
509
- rtc.cleanup();
510
- const nextSessions = state.getState().sessions.filter((s) => s.id !== sessionId);
511
- state.batchSet({
512
- sessions: nextSessions
513
- });
514
- },
515
- failed: (e) => {
516
- emitter.emit("failed", e);
517
- detachSessionHandlers();
518
- rtc.cleanup();
519
- const cause = e?.cause || "call failed";
520
- onSessionFailed(cause, e);
521
- const nextSessions = state.getState().sessions.filter((s) => s.id !== sessionId);
522
- state.batchSet({
523
- sessions: nextSessions
524
- });
525
- },
526
- muted: () => {
527
- emitter.emit("muted", void 0);
528
- upsertSessionState(state, sessionId, { muted: true });
529
- },
530
- unmuted: () => {
531
- emitter.emit("unmuted", void 0);
532
- upsertSessionState(state, sessionId, { muted: false });
533
- },
534
- hold: () => {
535
- emitter.emit("hold", void 0);
536
- upsertSessionState(state, sessionId, { status: CallStatus.Hold });
537
- },
538
- unhold: () => {
539
- emitter.emit("unhold", void 0);
540
- upsertSessionState(state, sessionId, { status: CallStatus.Active });
541
- },
542
- reinvite: (e) => emitter.emit("reinvite", e),
543
- update: (e) => emitter.emit("update", e),
544
- sdp: (e) => emitter.emit("sdp", e),
545
- icecandidate: (e) => emitter.emit("icecandidate", e),
546
- refer: (e) => emitter.emit("refer", e),
547
- replaces: (e) => emitter.emit("replaces", e),
548
- newDTMF: (e) => emitter.emit("newDTMF", e),
549
- newInfo: (e) => emitter.emit("newInfo", e),
550
- getusermediafailed: (e) => {
551
- emitter.emit("getusermediafailed", e);
552
- detachSessionHandlers();
553
- rtc.cleanup();
554
- onSessionFailed("getUserMedia failed", e);
555
- state.batchSet({
556
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
557
- });
558
- },
559
- "peerconnection:createofferfailed": (e) => {
560
- emitter.emit("peerconnection:createofferfailed", e);
561
- detachSessionHandlers();
562
- rtc.cleanup();
563
- onSessionFailed("peer connection createOffer failed", e);
564
- state.batchSet({
565
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
566
- });
567
- },
568
- "peerconnection:createanswerfailed": (e) => {
569
- emitter.emit("peerconnection:createanswerfailed", e);
570
- detachSessionHandlers();
571
- rtc.cleanup();
572
- onSessionFailed("peer connection createAnswer failed", e);
573
- state.batchSet({
574
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
575
- });
576
- },
577
- "peerconnection:setlocaldescriptionfailed": (e) => {
578
- emitter.emit("peerconnection:setlocaldescriptionfailed", e);
579
- detachSessionHandlers();
580
- rtc.cleanup();
581
- onSessionFailed("peer connection setLocalDescription failed", e);
582
- state.batchSet({
583
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
584
- });
585
- },
586
- "peerconnection:setremotedescriptionfailed": (e) => {
587
- emitter.emit("peerconnection:setremotedescriptionfailed", e);
588
- detachSessionHandlers();
589
- rtc.cleanup();
590
- onSessionFailed("peer connection setRemoteDescription failed", e);
591
- state.batchSet({
592
- sessions: state.getState().sessions.filter((s) => s.id !== sessionId)
593
- });
405
+ onUA(event, handler) {
406
+ return client.on(event, handler);
594
407
  },
595
- peerconnection: (e) => emitter.emit("peerconnection", e)
408
+ onSession(sessionId, event, handler) {
409
+ const session = client.getSession(sessionId);
410
+ if (!session)
411
+ return () => {
412
+ };
413
+ session.on(event, handler);
414
+ return () => session.off(event, handler);
415
+ }
596
416
  };
597
417
  }
598
418
 
599
- // src/jssip-lib/sip/sessionController.ts
600
- var WebRTCSessionController = class {
419
+ // src/core/modules/debug/sip-debug.logger.ts
420
+ var describePc = (pc) => ({
421
+ connectionState: pc?.connectionState,
422
+ signalingState: pc?.signalingState,
423
+ iceConnectionState: pc?.iceConnectionState
424
+ });
425
+ var SipDebugLogger = class {
601
426
  constructor() {
602
- this.currentSession = null;
603
- this.mediaStream = null;
604
- }
605
- setSession(session) {
606
- this.currentSession = session;
607
- }
608
- setMediaStream(stream) {
609
- this.mediaStream = stream;
610
- }
611
- getPC() {
612
- return this.currentSession?.connection ?? null;
613
- }
614
- cleanup(stopTracks = true) {
615
- const pc = this.getPC();
616
- const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
617
- if (pc && typeof pc.getSenders === "function") {
618
- if (!isClosed) {
619
- for (const s of pc.getSenders()) {
620
- try {
621
- s.replaceTrack(null);
622
- } catch {
623
- }
624
- }
625
- }
626
- }
627
- if (stopTracks && this.mediaStream) {
628
- const senderTracks = pc && !isClosed ? new Set(
629
- pc.getSenders().map((s) => s.track).filter((t) => Boolean(t))
630
- ) : null;
631
- for (const t of this.mediaStream.getTracks()) {
632
- if (senderTracks?.has(t))
633
- continue;
634
- t.stop();
635
- }
636
- }
637
- this.mediaStream = null;
638
- this.currentSession = null;
639
- }
640
- answer(options = {}) {
641
- return this.currentSession ? (this.currentSession.answer(options), true) : false;
642
- }
643
- hangup(options) {
644
- return this.currentSession ? (this.currentSession.terminate(
645
- options ?? { status_code: 486, reason_phrase: "Busy Here" }
646
- ), true) : false;
647
- }
648
- mute() {
649
- this.mediaStream?.getAudioTracks().forEach((t) => t.enabled = false);
650
- return this.currentSession ? (this.currentSession.mute({ audio: true }), true) : false;
651
- }
652
- unmute() {
653
- this.mediaStream?.getAudioTracks().forEach((t) => t.enabled = true);
654
- return this.currentSession ? (this.currentSession.unmute({ audio: true }), true) : false;
655
- }
656
- hold() {
657
- return this.currentSession ? (this.currentSession.hold(), true) : false;
658
- }
659
- unhold() {
660
- return this.currentSession ? (this.currentSession.unhold(), true) : false;
661
- }
662
- sendDTMF(tones, options) {
663
- return this.currentSession ? (this.currentSession.sendDTMF(tones, options), true) : false;
664
- }
665
- transfer(target, options) {
666
- return this.currentSession ? (this.currentSession.refer(target, options), true) : false;
667
- }
668
- enableVideo() {
669
- this.mediaStream?.getVideoTracks().forEach((t) => t.enabled = true);
670
- }
671
- disableVideo() {
672
- this.mediaStream?.getVideoTracks().forEach((t) => t.enabled = false);
673
- }
674
- async switchCamera(nextVideoTrack) {
675
- const pc = this.getPC();
676
- if (!pc)
677
- return false;
678
- if (!this.mediaStream)
679
- this.mediaStream = new MediaStream();
680
- const old = this.mediaStream.getVideoTracks()[0];
681
- this.mediaStream.addTrack(nextVideoTrack);
682
- if (old)
683
- this.mediaStream.removeTrack(old);
684
- const sender = pc.getSenders?.().find((s) => s.track?.kind === "video");
685
- if (sender)
686
- await sender.replaceTrack(nextVideoTrack);
687
- if (old && old !== nextVideoTrack)
688
- old.stop();
689
- return true;
690
- }
691
- async replaceAudioTrack(nextAudioTrack) {
692
- const pc = this.getPC();
693
- if (!pc)
694
- return false;
695
- if (!this.mediaStream)
696
- this.mediaStream = new MediaStream();
697
- const old = this.mediaStream.getAudioTracks()[0];
698
- this.mediaStream.addTrack(nextAudioTrack);
699
- if (old)
700
- this.mediaStream.removeTrack(old);
701
- const sender = pc.getSenders?.().find((s) => s.track?.kind === "audio");
702
- if (sender)
703
- await sender.replaceTrack(nextAudioTrack);
704
- if (old && old !== nextAudioTrack)
705
- old.stop();
706
- return true;
707
- }
708
- };
709
-
710
- // src/jssip-lib/sip/sessionManager.ts
711
- var SessionManager = class {
712
- constructor() {
713
- this.entries = /* @__PURE__ */ new Map();
714
- }
715
- stopMediaStream(stream) {
716
- if (!stream)
717
- return;
718
- for (const t of stream.getTracks()) {
719
- if (t.readyState !== "ended")
720
- t.stop();
721
- }
722
- }
723
- getOrCreateRtc(sessionId, session) {
724
- let entry = this.entries.get(sessionId);
725
- if (!entry) {
726
- entry = {
727
- rtc: new WebRTCSessionController(),
728
- session: null,
729
- media: null
730
- };
731
- this.entries.set(sessionId, entry);
732
- }
733
- if (session) {
734
- entry.session = session;
735
- entry.rtc.setSession(session);
736
- }
737
- if (entry.media)
738
- entry.rtc.setMediaStream(entry.media);
739
- return entry.rtc;
740
- }
741
- getRtc(sessionId) {
742
- return this.entries.get(sessionId)?.rtc ?? null;
743
- }
744
- setSession(sessionId, session) {
745
- const entry = this.entries.get(sessionId);
746
- if (entry) {
747
- entry.session = session;
748
- entry.rtc.setSession(session);
749
- } else {
750
- this.entries.set(sessionId, {
751
- rtc: new WebRTCSessionController(),
752
- session,
753
- media: null
754
- });
755
- }
756
- }
757
- setSessionMedia(sessionId, stream) {
758
- const entry = this.entries.get(sessionId) ?? {
759
- rtc: new WebRTCSessionController(),
760
- session: null,
761
- media: null
762
- };
763
- if (entry.media && entry.media !== stream) {
764
- this.stopMediaStream(entry.media);
765
- }
766
- entry.media = stream;
767
- entry.rtc.setMediaStream(stream);
768
- this.entries.set(sessionId, entry);
769
- }
770
- getSession(sessionId) {
771
- return this.entries.get(sessionId)?.session ?? null;
772
- }
773
- getSessionIds() {
774
- return Array.from(this.entries.keys());
775
- }
776
- getSessions() {
777
- return Array.from(this.entries.entries()).map(([id, entry]) => ({
778
- id,
779
- session: entry.session
780
- }));
781
- }
782
- getActiveSessionId(activeStatuses = ["active"]) {
783
- for (const [id, entry] of Array.from(this.entries.entries()).reverse()) {
784
- const status = entry.session?.status;
785
- if (status && activeStatuses.includes(String(status).toLowerCase())) {
786
- return id;
787
- }
788
- }
789
- return null;
790
- }
791
- cleanupSession(sessionId) {
792
- const entry = this.entries.get(sessionId);
793
- if (entry) {
794
- entry.rtc.cleanup();
795
- this.stopMediaStream(entry.media);
796
- this.entries.delete(sessionId);
797
- }
798
- }
799
- cleanupAllSessions() {
800
- for (const [, entry] of this.entries.entries()) {
801
- entry.rtc.cleanup();
802
- this.stopMediaStream(entry.media);
803
- }
804
- this.entries.clear();
805
- }
806
- answer(sessionId, options) {
807
- const rtc = this.getRtc(sessionId);
808
- return rtc ? rtc.answer(options) : false;
809
- }
810
- hangup(sessionId, options) {
811
- const rtc = this.getRtc(sessionId);
812
- return rtc ? rtc.hangup(options) : false;
813
- }
814
- mute(sessionId) {
815
- const rtc = this.getRtc(sessionId);
816
- return rtc ? rtc.mute() : false;
817
- }
818
- unmute(sessionId) {
819
- const rtc = this.getRtc(sessionId);
820
- return rtc ? rtc.unmute() : false;
821
- }
822
- hold(sessionId) {
823
- const rtc = this.getRtc(sessionId);
824
- return rtc ? rtc.hold() : false;
825
- }
826
- unhold(sessionId) {
827
- const rtc = this.getRtc(sessionId);
828
- return rtc ? rtc.unhold() : false;
829
- }
830
- sendDTMF(sessionId, tones, options) {
831
- const rtc = this.getRtc(sessionId);
832
- return rtc ? rtc.sendDTMF(tones, options) : false;
833
- }
834
- transfer(sessionId, target, options) {
835
- const rtc = this.getRtc(sessionId);
836
- return rtc ? rtc.transfer(target, options) : false;
837
- }
838
- };
839
-
840
- // src/jssip-lib/sip/debugLogging.ts
841
- var describePc = (pc) => ({
842
- connectionState: pc?.connectionState,
843
- signalingState: pc?.signalingState,
844
- iceConnectionState: pc?.iceConnectionState
845
- });
846
- var SipDebugLogger = class {
847
- constructor() {
848
- this.enabled = false;
849
- this.statsStops = /* @__PURE__ */ new Map();
427
+ this.enabled = false;
428
+ this.statsStops = /* @__PURE__ */ new Map();
850
429
  }
851
430
  setEnabled(enabled) {
852
431
  this.enabled = enabled;
@@ -883,6 +462,16 @@ var SipDebugLogger = class {
883
462
  return;
884
463
  console.error("[sip] microphone dropped", payload);
885
464
  }
465
+ logIceReady(sessionId, payload) {
466
+ if (!this.enabled)
467
+ return;
468
+ console.info("[sip] ice ready", { sessionId, ...payload });
469
+ }
470
+ logIceReadyConfig(sessionId, delayMs) {
471
+ if (!this.enabled)
472
+ return;
473
+ console.info("[sip] ice ready config", { sessionId, delayMs });
474
+ }
886
475
  startCallStatsLogging(sessionId, session) {
887
476
  if (!this.enabled || this.statsStops.has(sessionId))
888
477
  return;
@@ -996,43 +585,582 @@ function collectAudioStats(report) {
996
585
  return { outboundAudio, inboundAudio };
997
586
  }
998
587
 
999
- // src/jssip-lib/sip/sessionLifecycle.ts
1000
- var SessionLifecycle = class {
588
+ // src/core/modules/media/mic-recovery.manager.ts
589
+ var MicRecoveryManager = class {
1001
590
  constructor(deps) {
1002
- this.state = deps.state;
1003
- this.sessionManager = deps.sessionManager;
1004
- this.emit = deps.emit;
1005
- this.emitError = deps.emitError;
1006
- this.attachSessionHandlers = deps.attachSessionHandlers;
1007
- this.getMaxSessionCount = deps.getMaxSessionCount;
591
+ this.enabled = false;
592
+ this.defaults = {
593
+ intervalMs: 2e3,
594
+ maxRetries: Infinity
595
+ };
596
+ this.active = /* @__PURE__ */ new Map();
597
+ this.syncedSenderTrackId = /* @__PURE__ */ new Map();
598
+ this.deps = deps;
1008
599
  }
1009
- setDebugEnabled(enabled) {
1010
- sipDebugLogger.setEnabled(enabled);
600
+ configure(config) {
601
+ if (typeof config.enabled === "boolean") {
602
+ this.enabled = config.enabled;
603
+ }
604
+ if (typeof config.intervalMs === "number") {
605
+ this.defaults.intervalMs = config.intervalMs;
606
+ }
607
+ if (typeof config.maxRetries === "number") {
608
+ this.defaults.maxRetries = config.maxRetries;
609
+ }
1011
610
  }
1012
- handleNewRTCSession(e) {
1013
- const session = e.session;
1014
- const sessionId = String(
1015
- session?.id ?? crypto.randomUUID?.() ?? Date.now()
1016
- );
1017
- const currentSessions = this.state.getState().sessions;
1018
- if (currentSessions.length >= this.getMaxSessionCount()) {
1019
- try {
1020
- session.terminate?.({
611
+ enable(sessionId, options = {}) {
612
+ if (!this.enabled)
613
+ return () => {
614
+ };
615
+ this.disable(sessionId);
616
+ const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
617
+ const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
618
+ let retries = 0;
619
+ let stopped = false;
620
+ const startedAt = Date.now();
621
+ const warmupMs = Math.max(intervalMs * 2, 2e3);
622
+ const tick = async () => {
623
+ if (stopped || retries >= maxRetries)
624
+ return;
625
+ const rtc = this.deps.getRtc(sessionId);
626
+ const session2 = this.deps.getSession(sessionId);
627
+ if (!rtc || !session2)
628
+ return;
629
+ const sessionState = this.deps.getSessionState(sessionId);
630
+ if (sessionState?.muted)
631
+ return;
632
+ const stream = rtc.mediaStream;
633
+ const track = stream?.getAudioTracks?.()[0];
634
+ const pc2 = session2?.connection;
635
+ const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
636
+ if (!track && !sender)
637
+ return;
638
+ if (!track && sender?.track?.readyState === "live") {
639
+ const nextId = sender.track.id;
640
+ const prevId = this.syncedSenderTrackId.get(sessionId);
641
+ if (prevId === nextId)
642
+ return;
643
+ this.syncedSenderTrackId.set(sessionId, nextId);
644
+ this.deps.setSessionMedia(sessionId, new MediaStream([sender.track]));
645
+ return;
646
+ }
647
+ if (Date.now() - startedAt < warmupMs)
648
+ return;
649
+ if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
650
+ return;
651
+ }
652
+ const trackLive = track?.readyState === "live";
653
+ const senderLive = sender?.track?.readyState === "live";
654
+ if (trackLive && senderLive)
655
+ return;
656
+ sipDebugLogger.logMicRecoveryDrop({
657
+ sessionId,
658
+ trackLive,
659
+ senderLive
660
+ });
661
+ retries += 1;
662
+ if (trackLive && !senderLive && track) {
663
+ await rtc.replaceAudioTrack(track);
664
+ return;
665
+ }
666
+ };
667
+ const timer = setInterval(() => {
668
+ void tick();
669
+ }, intervalMs);
670
+ void tick();
671
+ const session = this.deps.getSession(sessionId);
672
+ const pc = session?.connection;
673
+ const onIceChange = () => {
674
+ const state = pc?.iceConnectionState;
675
+ if (state === "failed" || state === "disconnected")
676
+ void tick();
677
+ };
678
+ pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
679
+ const stop = () => {
680
+ stopped = true;
681
+ clearInterval(timer);
682
+ pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
683
+ };
684
+ this.active.set(sessionId, { stop });
685
+ return stop;
686
+ }
687
+ disable(sessionId) {
688
+ const entry = this.active.get(sessionId);
689
+ if (!entry)
690
+ return false;
691
+ entry.stop();
692
+ this.active.delete(sessionId);
693
+ this.syncedSenderTrackId.delete(sessionId);
694
+ return true;
695
+ }
696
+ cleanupAll() {
697
+ this.active.forEach((entry) => entry.stop());
698
+ this.active.clear();
699
+ this.syncedSenderTrackId.clear();
700
+ }
701
+ };
702
+
703
+ // src/core/modules/runtime/browser-unload.runtime.ts
704
+ var BrowserUnloadRuntime = class {
705
+ attach(onBeforeUnload) {
706
+ if (typeof window === "undefined" || this.handler)
707
+ return;
708
+ this.handler = () => onBeforeUnload();
709
+ window.addEventListener("beforeunload", this.handler);
710
+ }
711
+ detach() {
712
+ if (typeof window === "undefined" || !this.handler)
713
+ return;
714
+ window.removeEventListener("beforeunload", this.handler);
715
+ this.handler = void 0;
716
+ }
717
+ };
718
+
719
+ // src/core/modules/media/webrtc-session.controller.ts
720
+ var WebRTCSessionController = class {
721
+ constructor() {
722
+ this.currentSession = null;
723
+ this.mediaStream = null;
724
+ }
725
+ setSession(session) {
726
+ this.currentSession = session;
727
+ }
728
+ setMediaStream(stream) {
729
+ this.mediaStream = stream;
730
+ }
731
+ getPC() {
732
+ return this.currentSession?.connection ?? null;
733
+ }
734
+ cleanup(stopTracks = true) {
735
+ const pc = this.getPC();
736
+ const isClosed = pc?.connectionState === "closed" || pc?.signalingState === "closed";
737
+ if (pc && typeof pc.getSenders === "function") {
738
+ if (!isClosed) {
739
+ for (const sender of pc.getSenders()) {
740
+ try {
741
+ sender.replaceTrack(null);
742
+ } catch {
743
+ }
744
+ }
745
+ }
746
+ }
747
+ if (stopTracks && this.mediaStream) {
748
+ const senderTracks = pc && !isClosed ? new Set(
749
+ pc.getSenders().map((sender) => sender.track).filter((track) => Boolean(track))
750
+ ) : null;
751
+ for (const track of this.mediaStream.getTracks()) {
752
+ if (senderTracks?.has(track))
753
+ continue;
754
+ track.stop();
755
+ }
756
+ }
757
+ this.mediaStream = null;
758
+ this.currentSession = null;
759
+ }
760
+ answer(options = {}) {
761
+ return this.currentSession ? (this.currentSession.answer(options), true) : false;
762
+ }
763
+ hangup(options) {
764
+ return this.currentSession ? (this.currentSession.terminate(
765
+ options ?? { status_code: 486, reason_phrase: "Busy Here" }
766
+ ), true) : false;
767
+ }
768
+ mute() {
769
+ this.mediaStream?.getAudioTracks().forEach((track) => track.enabled = false);
770
+ return this.currentSession ? (this.currentSession.mute({ audio: true }), true) : false;
771
+ }
772
+ unmute() {
773
+ this.mediaStream?.getAudioTracks().forEach((track) => track.enabled = true);
774
+ return this.currentSession ? (this.currentSession.unmute({ audio: true }), true) : false;
775
+ }
776
+ hold() {
777
+ return this.currentSession ? (this.currentSession.hold(), true) : false;
778
+ }
779
+ unhold() {
780
+ return this.currentSession ? (this.currentSession.unhold(), true) : false;
781
+ }
782
+ sendDTMF(tones, options) {
783
+ return this.currentSession ? (this.currentSession.sendDTMF(tones, options), true) : false;
784
+ }
785
+ transfer(target, options) {
786
+ return this.currentSession ? (this.currentSession.refer(target, options), true) : false;
787
+ }
788
+ async replaceAudioTrack(nextAudioTrack) {
789
+ const pc = this.getPC();
790
+ if (!pc)
791
+ return false;
792
+ if (!this.mediaStream)
793
+ this.mediaStream = new MediaStream();
794
+ const old = this.mediaStream.getAudioTracks()[0];
795
+ this.mediaStream.addTrack(nextAudioTrack);
796
+ if (old)
797
+ this.mediaStream.removeTrack(old);
798
+ const sender = pc.getSenders?.().find((entry) => entry.track?.kind === "audio");
799
+ if (sender)
800
+ await sender.replaceTrack(nextAudioTrack);
801
+ if (old && old !== nextAudioTrack)
802
+ old.stop();
803
+ return true;
804
+ }
805
+ };
806
+
807
+ // src/core/modules/session/session.manager.ts
808
+ var SessionManager = class {
809
+ constructor() {
810
+ this.entries = /* @__PURE__ */ new Map();
811
+ }
812
+ stopMediaStream(stream) {
813
+ if (!stream)
814
+ return;
815
+ for (const track of stream.getTracks()) {
816
+ if (track.readyState !== "ended")
817
+ track.stop();
818
+ }
819
+ }
820
+ getOrCreateRtc(sessionId, session) {
821
+ let entry = this.entries.get(sessionId);
822
+ if (!entry) {
823
+ entry = {
824
+ rtc: new WebRTCSessionController(),
825
+ session: null,
826
+ media: null
827
+ };
828
+ this.entries.set(sessionId, entry);
829
+ }
830
+ if (session) {
831
+ entry.session = session;
832
+ entry.rtc.setSession(session);
833
+ }
834
+ if (entry.media)
835
+ entry.rtc.setMediaStream(entry.media);
836
+ return entry.rtc;
837
+ }
838
+ getRtc(sessionId) {
839
+ return this.entries.get(sessionId)?.rtc ?? null;
840
+ }
841
+ setSession(sessionId, session) {
842
+ const entry = this.entries.get(sessionId);
843
+ if (entry) {
844
+ entry.session = session;
845
+ entry.rtc.setSession(session);
846
+ } else {
847
+ this.entries.set(sessionId, {
848
+ rtc: new WebRTCSessionController(),
849
+ session,
850
+ media: null
851
+ });
852
+ }
853
+ }
854
+ setSessionMedia(sessionId, stream) {
855
+ const entry = this.entries.get(sessionId) ?? {
856
+ rtc: new WebRTCSessionController(),
857
+ session: null,
858
+ media: null
859
+ };
860
+ if (entry.media && entry.media !== stream) {
861
+ this.stopMediaStream(entry.media);
862
+ }
863
+ entry.media = stream;
864
+ entry.rtc.setMediaStream(stream);
865
+ this.entries.set(sessionId, entry);
866
+ }
867
+ getSession(sessionId) {
868
+ return this.entries.get(sessionId)?.session ?? null;
869
+ }
870
+ getSessionIds() {
871
+ return Array.from(this.entries.keys());
872
+ }
873
+ getSessions() {
874
+ return Array.from(this.entries.entries()).map(([id, entry]) => ({
875
+ id,
876
+ session: entry.session
877
+ }));
878
+ }
879
+ cleanupSession(sessionId) {
880
+ const entry = this.entries.get(sessionId);
881
+ if (entry) {
882
+ entry.rtc.cleanup();
883
+ this.stopMediaStream(entry.media);
884
+ this.entries.delete(sessionId);
885
+ }
886
+ }
887
+ cleanupAllSessions() {
888
+ for (const [, entry] of this.entries.entries()) {
889
+ entry.rtc.cleanup();
890
+ this.stopMediaStream(entry.media);
891
+ }
892
+ this.entries.clear();
893
+ }
894
+ answer(sessionId, options) {
895
+ const rtc = this.getRtc(sessionId);
896
+ return rtc ? rtc.answer(options) : false;
897
+ }
898
+ hangup(sessionId, options) {
899
+ const rtc = this.getRtc(sessionId);
900
+ return rtc ? rtc.hangup(options) : false;
901
+ }
902
+ mute(sessionId) {
903
+ const rtc = this.getRtc(sessionId);
904
+ return rtc ? rtc.mute() : false;
905
+ }
906
+ unmute(sessionId) {
907
+ const rtc = this.getRtc(sessionId);
908
+ return rtc ? rtc.unmute() : false;
909
+ }
910
+ hold(sessionId) {
911
+ const rtc = this.getRtc(sessionId);
912
+ return rtc ? rtc.hold() : false;
913
+ }
914
+ unhold(sessionId) {
915
+ const rtc = this.getRtc(sessionId);
916
+ return rtc ? rtc.unhold() : false;
917
+ }
918
+ sendDTMF(sessionId, tones, options) {
919
+ const rtc = this.getRtc(sessionId);
920
+ return rtc ? rtc.sendDTMF(tones, options) : false;
921
+ }
922
+ transfer(sessionId, target, options) {
923
+ const rtc = this.getRtc(sessionId);
924
+ return rtc ? rtc.transfer(target, options) : false;
925
+ }
926
+ };
927
+
928
+ // src/core/modules/session/session.state.projector.ts
929
+ function toSessionMaps(sessions) {
930
+ const sessionsById = {};
931
+ const sessionIds = [];
932
+ for (const session of sessions) {
933
+ sessionsById[session.id] = session;
934
+ sessionIds.push(session.id);
935
+ }
936
+ return { sessionsById, sessionIds };
937
+ }
938
+ function holdOtherSessions(state, sessionId, holdFn) {
939
+ const current = state.getState();
940
+ current.sessionIds.forEach((id) => {
941
+ if (id === sessionId)
942
+ return;
943
+ const session = current.sessionsById[id];
944
+ if (session?.status === CallStatus.Active) {
945
+ holdFn(id);
946
+ }
947
+ });
948
+ }
949
+ function upsertSessionState(state, sessionId, partial) {
950
+ const current = state.getState();
951
+ const existing = current.sessionsById[sessionId];
952
+ const base = existing ?? {
953
+ id: sessionId,
954
+ status: CallStatus.Idle,
955
+ direction: null,
956
+ from: null,
957
+ to: null,
958
+ muted: false,
959
+ acceptedAt: null
960
+ };
961
+ const nextSession = { ...base, ...partial };
962
+ const sessions = current.sessionIds.map(
963
+ (id) => id === sessionId ? nextSession : current.sessionsById[id]
964
+ );
965
+ if (!existing) {
966
+ sessions.push(nextSession);
967
+ }
968
+ const { sessionsById, sessionIds } = toSessionMaps(sessions);
969
+ state.setState({ sessions, sessionsById, sessionIds });
970
+ }
971
+ function removeSessionState(state, sessionId) {
972
+ const current = state.getState();
973
+ const sessions = current.sessionIds.filter((id) => id !== sessionId).map((id) => current.sessionsById[id]);
974
+ const { sessionsById, sessionIds } = toSessionMaps(sessions);
975
+ state.setState({
976
+ sessions,
977
+ sessionsById,
978
+ sessionIds,
979
+ error: null
980
+ });
981
+ }
982
+
983
+ // src/core/modules/session/session.handlers.ts
984
+ function createSessionHandlers(deps) {
985
+ const {
986
+ emitter,
987
+ state,
988
+ rtc,
989
+ detachSessionHandlers,
990
+ sessionId,
991
+ iceCandidateReadyDelayMs
992
+ } = deps;
993
+ let iceReadyCalled = false;
994
+ let iceReadyTimer = null;
995
+ const clearIceReadyTimer = () => {
996
+ if (!iceReadyTimer)
997
+ return;
998
+ clearTimeout(iceReadyTimer);
999
+ iceReadyTimer = null;
1000
+ };
1001
+ if (typeof iceCandidateReadyDelayMs === "number") {
1002
+ sipDebugLogger.logIceReadyConfig(sessionId, iceCandidateReadyDelayMs);
1003
+ }
1004
+ return {
1005
+ progress: (e) => {
1006
+ emitter.emit("progress", e);
1007
+ },
1008
+ accepted: (e) => {
1009
+ emitter.emit("accepted", e);
1010
+ const existing = state.getState().sessionsById[sessionId];
1011
+ upsertSessionState(state, sessionId, {
1012
+ status: CallStatus.Active,
1013
+ acceptedAt: existing?.acceptedAt ?? Date.now()
1014
+ });
1015
+ },
1016
+ confirmed: (e) => {
1017
+ emitter.emit("confirmed", e);
1018
+ deps.enableMicrophoneRecovery?.(sessionId);
1019
+ },
1020
+ ended: (e) => {
1021
+ emitter.emit("ended", e);
1022
+ clearIceReadyTimer();
1023
+ detachSessionHandlers();
1024
+ rtc.cleanup();
1025
+ removeSessionState(state, sessionId);
1026
+ },
1027
+ failed: (e) => {
1028
+ emitter.emit("failed", e);
1029
+ clearIceReadyTimer();
1030
+ detachSessionHandlers();
1031
+ rtc.cleanup();
1032
+ removeSessionState(state, sessionId);
1033
+ },
1034
+ muted: (e) => {
1035
+ emitter.emit("muted", e);
1036
+ upsertSessionState(state, sessionId, { muted: true });
1037
+ },
1038
+ unmuted: (e) => {
1039
+ emitter.emit("unmuted", e);
1040
+ upsertSessionState(state, sessionId, { muted: false });
1041
+ },
1042
+ hold: (e) => {
1043
+ emitter.emit("hold", e);
1044
+ upsertSessionState(state, sessionId, { status: CallStatus.Hold });
1045
+ },
1046
+ unhold: (e) => {
1047
+ emitter.emit("unhold", e);
1048
+ upsertSessionState(state, sessionId, { status: CallStatus.Active });
1049
+ },
1050
+ reinvite: (e) => emitter.emit("reinvite", e),
1051
+ update: (e) => emitter.emit("update", e),
1052
+ sdp: (e) => emitter.emit("sdp", e),
1053
+ icecandidate: (e) => {
1054
+ const candidate = e?.candidate;
1055
+ const ready = typeof e?.ready === "function" ? e.ready : null;
1056
+ const delayMs = typeof iceCandidateReadyDelayMs === "number" ? iceCandidateReadyDelayMs : null;
1057
+ if (!iceReadyCalled && ready && delayMs != null) {
1058
+ if (candidate?.type === "srflx" && candidate?.relatedAddress != null && candidate?.relatedPort != null) {
1059
+ iceReadyCalled = true;
1060
+ if (iceReadyTimer) {
1061
+ clearTimeout(iceReadyTimer);
1062
+ iceReadyTimer = null;
1063
+ }
1064
+ sipDebugLogger.logIceReady(sessionId, {
1065
+ source: "srflx",
1066
+ delayMs,
1067
+ candidateType: candidate?.type
1068
+ });
1069
+ ready();
1070
+ } else if (!iceReadyTimer && delayMs > 0) {
1071
+ iceReadyTimer = setTimeout(() => {
1072
+ iceReadyTimer = null;
1073
+ if (iceReadyCalled)
1074
+ return;
1075
+ iceReadyCalled = true;
1076
+ sipDebugLogger.logIceReady(sessionId, {
1077
+ source: "timer",
1078
+ delayMs,
1079
+ candidateType: candidate?.type
1080
+ });
1081
+ ready();
1082
+ }, delayMs);
1083
+ } else if (delayMs === 0) {
1084
+ iceReadyCalled = true;
1085
+ sipDebugLogger.logIceReady(sessionId, {
1086
+ source: "immediate",
1087
+ delayMs,
1088
+ candidateType: candidate?.type
1089
+ });
1090
+ ready();
1091
+ }
1092
+ }
1093
+ emitter.emit("icecandidate", e);
1094
+ },
1095
+ refer: (e) => emitter.emit("refer", e),
1096
+ replaces: (e) => emitter.emit("replaces", e),
1097
+ newDTMF: (e) => emitter.emit("newDTMF", e),
1098
+ newInfo: (e) => emitter.emit("newInfo", e),
1099
+ getusermediafailed: (e) => {
1100
+ emitter.emit("getusermediafailed", e);
1101
+ clearIceReadyTimer();
1102
+ detachSessionHandlers();
1103
+ rtc.cleanup();
1104
+ removeSessionState(state, sessionId);
1105
+ },
1106
+ "peerconnection:createofferfailed": (e) => {
1107
+ emitter.emit("peerconnection:createofferfailed", e);
1108
+ clearIceReadyTimer();
1109
+ detachSessionHandlers();
1110
+ rtc.cleanup();
1111
+ removeSessionState(state, sessionId);
1112
+ },
1113
+ "peerconnection:createanswerfailed": (e) => {
1114
+ emitter.emit("peerconnection:createanswerfailed", e);
1115
+ clearIceReadyTimer();
1116
+ detachSessionHandlers();
1117
+ rtc.cleanup();
1118
+ removeSessionState(state, sessionId);
1119
+ },
1120
+ "peerconnection:setlocaldescriptionfailed": (e) => {
1121
+ emitter.emit("peerconnection:setlocaldescriptionfailed", e);
1122
+ clearIceReadyTimer();
1123
+ detachSessionHandlers();
1124
+ rtc.cleanup();
1125
+ removeSessionState(state, sessionId);
1126
+ },
1127
+ "peerconnection:setremotedescriptionfailed": (e) => {
1128
+ emitter.emit("peerconnection:setremotedescriptionfailed", e);
1129
+ clearIceReadyTimer();
1130
+ detachSessionHandlers();
1131
+ rtc.cleanup();
1132
+ removeSessionState(state, sessionId);
1133
+ },
1134
+ peerconnection: (e) => emitter.emit("peerconnection", e)
1135
+ };
1136
+ }
1137
+
1138
+ // src/core/modules/session/session.lifecycle.ts
1139
+ var SessionLifecycle = class {
1140
+ constructor(deps) {
1141
+ this.state = deps.state;
1142
+ this.sessionManager = deps.sessionManager;
1143
+ this.emit = deps.emit;
1144
+ this.attachSessionHandlers = deps.attachSessionHandlers;
1145
+ this.getMaxSessionCount = deps.getMaxSessionCount;
1146
+ }
1147
+ setDebugEnabled(enabled) {
1148
+ sipDebugLogger.setEnabled(enabled);
1149
+ }
1150
+ handleNewRTCSession(e) {
1151
+ const session = e.session;
1152
+ const sessionId = String(
1153
+ session?.id ?? crypto.randomUUID?.() ?? Date.now()
1154
+ );
1155
+ const currentSessions = this.state.getState().sessions;
1156
+ if (currentSessions.length >= this.getMaxSessionCount()) {
1157
+ try {
1158
+ session.terminate?.({
1021
1159
  status_code: 486,
1022
1160
  reason_phrase: "Busy Here"
1023
1161
  });
1024
1162
  } catch {
1025
1163
  }
1026
- if (e.originator === "remote") {
1027
- this.emit("missed", e);
1028
- } else {
1029
- this.emitError(
1030
- "max session count reached",
1031
- "MAX_SESSIONS_REACHED",
1032
- "max session count reached"
1033
- );
1034
- }
1035
- return;
1036
1164
  }
1037
1165
  const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
1038
1166
  this.sessionManager.setSession(sessionId, session);
@@ -1048,14 +1176,11 @@ var SessionLifecycle = class {
1048
1176
  const otherRtc = this.sessionManager.getRtc(id);
1049
1177
  otherRtc?.hold();
1050
1178
  });
1051
- const sdpHasVideo = e.request?.body && e.request.body.toString().includes("m=video") || session?.connection?.getReceivers?.()?.some((r) => r.track?.kind === "video");
1052
1179
  upsertSessionState(this.state, sessionId, {
1053
1180
  direction: e.originator,
1054
1181
  from: e.originator === "remote" ? e.request.from.uri.user : null,
1055
1182
  to: e.request.to.uri.user,
1056
- status: e.originator === "remote" ? CallStatus.Ringing : CallStatus.Dialing,
1057
- mediaKind: sdpHasVideo ? "video" : "audio",
1058
- remoteVideoEnabled: sdpHasVideo
1183
+ status: e.originator === "remote" ? CallStatus.Ringing : CallStatus.Dialing
1059
1184
  });
1060
1185
  this.emit("newRTCSession", e);
1061
1186
  }
@@ -1123,7 +1248,10 @@ var SessionLifecycle = class {
1123
1248
  attachedPc = pc;
1124
1249
  attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1125
1250
  attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1126
- attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1251
+ attachedPc.addEventListener?.(
1252
+ "iceconnectionstatechange",
1253
+ onPcStateChange
1254
+ );
1127
1255
  };
1128
1256
  const clearRetryTimer = () => {
1129
1257
  if (!retryTimer)
@@ -1131,7 +1259,7 @@ var SessionLifecycle = class {
1131
1259
  clearTimeout(retryTimer);
1132
1260
  retryTimer = null;
1133
1261
  };
1134
- const stopRetry = () => {
1262
+ const stopRetry = (opts = {}) => {
1135
1263
  if (stopped)
1136
1264
  return;
1137
1265
  stopped = true;
@@ -1232,8 +1360,8 @@ var SessionLifecycle = class {
1232
1360
  session.on?.("peerconnection", onPeer);
1233
1361
  }
1234
1362
  session.on?.("confirmed", onConfirmed);
1235
- session.on?.("ended", stopRetry);
1236
- session.on?.("failed", stopRetry);
1363
+ session.on?.("ended", (e) => stopRetry());
1364
+ session.on?.("failed", (e) => stopRetry());
1237
1365
  }
1238
1366
  bindRemoteIncomingAudio(sessionId, session) {
1239
1367
  const maxAttempts = 50;
@@ -1299,11 +1427,11 @@ var SessionLifecycle = class {
1299
1427
  return;
1300
1428
  exhaustedCheckUsed = true;
1301
1429
  if (checkRemoteTrack(attachedPc))
1302
- stopRetry();
1430
+ stopRetry({ keepTrack: true });
1303
1431
  return;
1304
1432
  }
1305
1433
  if (checkRemoteTrack(attachedPc))
1306
- stopRetry();
1434
+ stopRetry({ keepTrack: true });
1307
1435
  };
1308
1436
  const attachPcListeners = (pc) => {
1309
1437
  if (!pc || pc === attachedPc)
@@ -1326,7 +1454,10 @@ var SessionLifecycle = class {
1326
1454
  attachedPc = pc;
1327
1455
  attachedPc.addEventListener?.("signalingstatechange", onPcStateChange);
1328
1456
  attachedPc.addEventListener?.("connectionstatechange", onPcStateChange);
1329
- attachedPc.addEventListener?.("iceconnectionstatechange", onPcStateChange);
1457
+ attachedPc.addEventListener?.(
1458
+ "iceconnectionstatechange",
1459
+ onPcStateChange
1460
+ );
1330
1461
  attachedPc.addEventListener?.("track", onTrack);
1331
1462
  };
1332
1463
  const clearRetryTimer = () => {
@@ -1335,7 +1466,7 @@ var SessionLifecycle = class {
1335
1466
  clearTimeout(retryTimer);
1336
1467
  retryTimer = null;
1337
1468
  };
1338
- const stopRetry = () => {
1469
+ const stopRetry = (opts = {}) => {
1339
1470
  if (stopped)
1340
1471
  return;
1341
1472
  stopped = true;
@@ -1356,7 +1487,7 @@ var SessionLifecycle = class {
1356
1487
  attachedPc.removeEventListener?.("track", onTrack);
1357
1488
  attachedPc = null;
1358
1489
  }
1359
- if (attachedTrack) {
1490
+ if (attachedTrack && !opts.keepTrack) {
1360
1491
  attachedTrack.removeEventListener?.("ended", onRemoteEnded);
1361
1492
  attachedTrack.removeEventListener?.("mute", onRemoteMuted);
1362
1493
  attachedTrack = null;
@@ -1385,7 +1516,7 @@ var SessionLifecycle = class {
1385
1516
  retryScheduled = false;
1386
1517
  retryTimer = null;
1387
1518
  if (checkRemoteTrack(pc)) {
1388
- stopRetry();
1519
+ stopRetry({ keepTrack: true });
1389
1520
  return;
1390
1521
  }
1391
1522
  if (!pc)
@@ -1401,11 +1532,11 @@ var SessionLifecycle = class {
1401
1532
  return;
1402
1533
  exhaustedCheckUsed = true;
1403
1534
  if (checkRemoteTrack(attachedPc))
1404
- stopRetry();
1535
+ stopRetry({ keepTrack: true });
1405
1536
  return;
1406
1537
  }
1407
1538
  if (checkRemoteTrack(attachedPc))
1408
- stopRetry();
1539
+ stopRetry({ keepTrack: true });
1409
1540
  };
1410
1541
  const onPeer = (data) => {
1411
1542
  if (stopped)
@@ -1416,11 +1547,11 @@ var SessionLifecycle = class {
1416
1547
  return;
1417
1548
  exhaustedCheckUsed = true;
1418
1549
  if (checkRemoteTrack(data.peerconnection))
1419
- stopRetry();
1550
+ stopRetry({ keepTrack: true });
1420
1551
  return;
1421
1552
  }
1422
1553
  if (checkRemoteTrack(data.peerconnection)) {
1423
- stopRetry();
1554
+ stopRetry({ keepTrack: true });
1424
1555
  return;
1425
1556
  }
1426
1557
  scheduleRetry(data.peerconnection);
@@ -1428,205 +1559,341 @@ var SessionLifecycle = class {
1428
1559
  const onConfirmed = () => {
1429
1560
  if (stopped)
1430
1561
  return;
1431
- const currentPc = session?.connection ?? attachedPc;
1432
- if (exhausted) {
1433
- if (exhaustedCheckUsed)
1434
- return;
1435
- exhaustedCheckUsed = true;
1436
- if (checkRemoteTrack(currentPc))
1437
- stopRetry();
1438
- return;
1439
- }
1440
- if (checkRemoteTrack(currentPc)) {
1441
- stopRetry();
1442
- return;
1443
- }
1444
- logMissingReceiver(currentPc, "confirmed without remote track");
1445
- scheduleRetry(currentPc);
1446
- };
1447
- const existingPc = session?.connection;
1448
- if (!checkRemoteTrack(existingPc)) {
1449
- if (existingPc) {
1450
- attachPcListeners(existingPc);
1451
- scheduleRetry(existingPc);
1452
- }
1453
- session.on?.("peerconnection", onPeer);
1454
- }
1455
- session.on?.("confirmed", onConfirmed);
1456
- session.on?.("ended", stopRetry);
1457
- session.on?.("failed", stopRetry);
1458
- }
1459
- attachCallStatsLogging(sessionId, session) {
1460
- const onConfirmed = () => {
1461
- sipDebugLogger.startCallStatsLogging(sessionId, session);
1462
- };
1463
- const onEnd = () => {
1464
- sipDebugLogger.stopCallStatsLogging(sessionId);
1465
- };
1466
- session.on?.("confirmed", onConfirmed);
1467
- session.on?.("ended", onEnd);
1468
- session.on?.("failed", onEnd);
1469
- }
1470
- };
1471
-
1472
- // src/jssip-lib/sip/micRecovery.ts
1473
- var MicRecoveryManager = class {
1474
- constructor(deps) {
1475
- this.enabled = false;
1476
- this.defaults = {
1477
- intervalMs: 2e3,
1478
- maxRetries: Infinity
1479
- };
1480
- this.active = /* @__PURE__ */ new Map();
1481
- this.deps = deps;
1482
- }
1483
- configure(config) {
1484
- if (typeof config.enabled === "boolean") {
1485
- this.enabled = config.enabled;
1486
- }
1487
- if (typeof config.intervalMs === "number") {
1488
- this.defaults.intervalMs = config.intervalMs;
1489
- }
1490
- if (typeof config.maxRetries === "number") {
1491
- this.defaults.maxRetries = config.maxRetries;
1492
- }
1493
- }
1494
- enable(sessionId, options = {}) {
1495
- if (!this.enabled)
1496
- return () => {
1497
- };
1498
- this.disable(sessionId);
1499
- const intervalMs = options.intervalMs ?? this.defaults.intervalMs;
1500
- const maxRetries = options.maxRetries ?? this.defaults.maxRetries;
1501
- let retries = 0;
1502
- let stopped = false;
1503
- const startedAt = Date.now();
1504
- const warmupMs = Math.max(intervalMs * 2, 2e3);
1505
- const tick = async () => {
1506
- if (stopped || retries >= maxRetries)
1507
- return;
1508
- const rtc = this.deps.getRtc(sessionId);
1509
- const session2 = this.deps.getSession(sessionId);
1510
- if (!rtc || !session2)
1511
- return;
1512
- const sessionState = this.deps.getSessionState(sessionId);
1513
- if (sessionState?.muted)
1514
- return;
1515
- const stream = rtc.mediaStream;
1516
- const track = stream?.getAudioTracks?.()[0];
1517
- const pc2 = session2?.connection;
1518
- const sender = pc2?.getSenders?.()?.find((s) => s.track?.kind === "audio");
1519
- if (!track && !sender)
1520
- return;
1521
- if (Date.now() - startedAt < warmupMs)
1522
- return;
1523
- if (pc2?.connectionState === "new" || pc2?.connectionState === "connecting" || pc2?.iceConnectionState === "new" || pc2?.iceConnectionState === "checking") {
1524
- return;
1525
- }
1526
- const trackLive = track?.readyState === "live";
1527
- const senderLive = sender?.track?.readyState === "live";
1528
- if (trackLive && senderLive)
1529
- return;
1530
- sipDebugLogger.logMicRecoveryDrop({
1531
- sessionId,
1532
- trackLive,
1533
- senderLive
1534
- });
1535
- retries += 1;
1536
- if (trackLive && !senderLive && track) {
1537
- await rtc.replaceAudioTrack(track);
1538
- return;
1539
- }
1540
- let nextStream;
1541
- try {
1542
- const deviceId = track?.getSettings?.().deviceId ?? sender?.track?.getSettings?.().deviceId;
1543
- nextStream = await this.deps.requestMicrophoneStream(deviceId);
1544
- } catch (err) {
1545
- console.warn("[sip] mic recovery failed to get stream", err);
1562
+ const currentPc = session?.connection ?? attachedPc;
1563
+ if (exhausted) {
1564
+ if (exhaustedCheckUsed)
1565
+ return;
1566
+ exhaustedCheckUsed = true;
1567
+ if (checkRemoteTrack(currentPc))
1568
+ stopRetry({ keepTrack: true });
1546
1569
  return;
1547
1570
  }
1548
- const nextTrack = nextStream.getAudioTracks()[0];
1549
- if (!nextTrack)
1571
+ if (checkRemoteTrack(currentPc)) {
1572
+ stopRetry({ keepTrack: true });
1550
1573
  return;
1551
- await rtc.replaceAudioTrack(nextTrack);
1552
- this.deps.setSessionMedia(sessionId, nextStream);
1574
+ }
1575
+ logMissingReceiver(currentPc, "confirmed without remote track");
1576
+ scheduleRetry(currentPc);
1553
1577
  };
1554
- const timer = setInterval(() => {
1555
- void tick();
1556
- }, intervalMs);
1557
- void tick();
1558
- const session = this.deps.getSession(sessionId);
1559
- const pc = session?.connection;
1560
- const onIceChange = () => {
1561
- const state = pc?.iceConnectionState;
1562
- if (state === "failed" || state === "disconnected")
1563
- void tick();
1578
+ const existingPc = session?.connection;
1579
+ if (!checkRemoteTrack(existingPc)) {
1580
+ if (existingPc) {
1581
+ attachPcListeners(existingPc);
1582
+ scheduleRetry(existingPc);
1583
+ }
1584
+ session.on?.("peerconnection", onPeer);
1585
+ }
1586
+ session.on?.("confirmed", onConfirmed);
1587
+ session.on?.("ended", () => stopRetry());
1588
+ session.on?.("failed", () => stopRetry());
1589
+ }
1590
+ attachCallStatsLogging(sessionId, session) {
1591
+ const onConfirmed = () => {
1592
+ sipDebugLogger.startCallStatsLogging(sessionId, session);
1564
1593
  };
1565
- pc?.addEventListener?.("iceconnectionstatechange", onIceChange);
1566
- const stop = () => {
1567
- stopped = true;
1568
- clearInterval(timer);
1569
- pc?.removeEventListener?.("iceconnectionstatechange", onIceChange);
1594
+ const onEnd = () => {
1595
+ sipDebugLogger.stopCallStatsLogging(sessionId);
1570
1596
  };
1571
- this.active.set(sessionId, { stop });
1572
- return stop;
1597
+ session.on?.("confirmed", onConfirmed);
1598
+ session.on?.("ended", onEnd);
1599
+ session.on?.("failed", onEnd);
1573
1600
  }
1574
- disable(sessionId) {
1575
- const entry = this.active.get(sessionId);
1576
- if (!entry)
1601
+ };
1602
+
1603
+ // src/core/modules/session/session.module.ts
1604
+ var SessionModule = class {
1605
+ constructor(deps) {
1606
+ this.deps = deps;
1607
+ this.sessionHandlers = /* @__PURE__ */ new Map();
1608
+ this.lifecycle = new SessionLifecycle({
1609
+ state: deps.state,
1610
+ sessionManager: deps.sessionManager,
1611
+ emit: (event, payload) => deps.emitter.emit(event, payload),
1612
+ attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
1613
+ getMaxSessionCount: deps.getMaxSessionCount
1614
+ });
1615
+ }
1616
+ setDebugEnabled(enabled) {
1617
+ this.lifecycle.setDebugEnabled(enabled);
1618
+ }
1619
+ handleNewRTCSession(e) {
1620
+ this.lifecycle.handleNewRTCSession(e);
1621
+ }
1622
+ setSessionMedia(sessionId, stream) {
1623
+ this.deps.sessionManager.setSessionMedia(sessionId, stream);
1624
+ }
1625
+ setSession(sessionId, session) {
1626
+ this.deps.sessionManager.setSession(sessionId, session);
1627
+ }
1628
+ answerSession(sessionId, options = {}) {
1629
+ if (!sessionId || !this.sessionExists(sessionId))
1577
1630
  return false;
1578
- entry.stop();
1579
- this.active.delete(sessionId);
1631
+ return this.deps.sessionManager.answer(sessionId, options);
1632
+ }
1633
+ hangupSession(sessionId, options) {
1634
+ if (!sessionId || !this.sessionExists(sessionId))
1635
+ return false;
1636
+ return this.deps.sessionManager.hangup(sessionId, options);
1637
+ }
1638
+ hangupAll(options) {
1639
+ const ids = this.getSessionIds();
1640
+ ids.forEach((id) => this.hangupSession(id, options));
1641
+ return ids.length > 0;
1642
+ }
1643
+ toggleMuteSession(sessionId) {
1644
+ const resolved = this.resolveExistingSessionId(sessionId);
1645
+ if (!resolved)
1646
+ return false;
1647
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1648
+ const muted = sessionState?.muted ?? false;
1649
+ if (muted) {
1650
+ this.deps.sessionManager.unmute(resolved);
1651
+ return true;
1652
+ }
1653
+ this.deps.sessionManager.mute(resolved);
1580
1654
  return true;
1581
1655
  }
1582
- cleanupAll() {
1583
- this.active.forEach((entry) => entry.stop());
1584
- this.active.clear();
1656
+ toggleHoldSession(sessionId) {
1657
+ const resolved = this.resolveExistingSessionId(sessionId);
1658
+ if (!resolved)
1659
+ return false;
1660
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1661
+ const isOnHold = sessionState?.status === CallStatus.Hold;
1662
+ if (isOnHold) {
1663
+ this.deps.sessionManager.unhold(resolved);
1664
+ return true;
1665
+ }
1666
+ if (sessionState?.status === CallStatus.Active) {
1667
+ this.deps.sessionManager.hold(resolved);
1668
+ return true;
1669
+ }
1670
+ return true;
1671
+ }
1672
+ sendDTMFSession(sessionId, tones, options) {
1673
+ const resolved = this.resolveExistingSessionId(sessionId);
1674
+ if (!resolved)
1675
+ return false;
1676
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1677
+ if (sessionState?.status === CallStatus.Active) {
1678
+ this.deps.sessionManager.sendDTMF(resolved, tones, options);
1679
+ }
1680
+ return true;
1681
+ }
1682
+ transferSession(sessionId, target, options) {
1683
+ const resolved = this.resolveExistingSessionId(sessionId);
1684
+ if (!resolved)
1685
+ return false;
1686
+ const sessionState = this.deps.state.getState().sessionsById[resolved];
1687
+ if (sessionState?.status === CallStatus.Active) {
1688
+ this.deps.sessionManager.transfer(resolved, target, options);
1689
+ }
1690
+ return true;
1691
+ }
1692
+ getSession(sessionId) {
1693
+ return this.deps.sessionManager.getSession(sessionId);
1694
+ }
1695
+ getSessionIds() {
1696
+ return this.deps.sessionManager.getSessionIds();
1697
+ }
1698
+ getSessions() {
1699
+ return this.deps.sessionManager.getSessions();
1700
+ }
1701
+ cleanupAllSessions() {
1702
+ this.deps.sessionManager.cleanupAllSessions();
1703
+ this.deps.micRecovery.cleanupAll();
1704
+ this.sessionHandlers.clear();
1705
+ this.deps.state.setState({
1706
+ sessions: [],
1707
+ sessionsById: {},
1708
+ sessionIds: [],
1709
+ error: null
1710
+ });
1711
+ }
1712
+ attachSessionHandlers(sessionId, session) {
1713
+ const handlers = this.createSessionHandlersFor(sessionId, session);
1714
+ this.sessionHandlers.set(sessionId, handlers);
1715
+ Object.keys(handlers).forEach((ev) => {
1716
+ const h = handlers[ev];
1717
+ if (h)
1718
+ session.on(ev, h);
1719
+ });
1720
+ }
1721
+ detachSessionHandlers(sessionId, session) {
1722
+ const handlers = this.sessionHandlers.get(sessionId);
1723
+ if (!handlers || !session)
1724
+ return;
1725
+ Object.keys(handlers).forEach((ev) => {
1726
+ const h = handlers[ev];
1727
+ if (h)
1728
+ session.off(ev, h);
1729
+ });
1730
+ this.sessionHandlers.delete(sessionId);
1731
+ }
1732
+ cleanupSession(sessionId, session) {
1733
+ const targetSession = session ?? this.deps.sessionManager.getSession(sessionId) ?? this.deps.sessionManager.getRtc(sessionId)?.currentSession;
1734
+ this.detachSessionHandlers(sessionId, targetSession);
1735
+ this.deps.micRecovery.disable(sessionId);
1736
+ this.deps.sessionManager.cleanupSession(sessionId);
1737
+ removeSessionState(this.deps.state, sessionId);
1738
+ }
1739
+ createSessionHandlersFor(sessionId, session) {
1740
+ const rtc = this.deps.sessionManager.getOrCreateRtc(sessionId, session);
1741
+ return createSessionHandlers({
1742
+ emitter: this.deps.emitter,
1743
+ state: this.deps.state,
1744
+ rtc,
1745
+ detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1746
+ enableMicrophoneRecovery: (confirmedSessionId) => this.deps.micRecovery.enable(confirmedSessionId),
1747
+ iceCandidateReadyDelayMs: this.deps.getIceCandidateReadyDelayMs(),
1748
+ sessionId
1749
+ });
1750
+ }
1751
+ resolveSessionId(sessionId) {
1752
+ if (sessionId)
1753
+ return sessionId;
1754
+ const state = this.deps.state.getState();
1755
+ const activeId = state.sessionIds.find(
1756
+ (id) => state.sessionsById[id]?.status === CallStatus.Active
1757
+ );
1758
+ return activeId ?? state.sessionIds[0] ?? null;
1759
+ }
1760
+ sessionExists(sessionId) {
1761
+ return !!this.deps.sessionManager.getSession(sessionId) || !!this.deps.sessionManager.getRtc(sessionId);
1762
+ }
1763
+ resolveExistingSessionId(sessionId) {
1764
+ const id = this.resolveSessionId(sessionId);
1765
+ if (!id)
1766
+ return null;
1767
+ return this.sessionExists(id) ? id : null;
1585
1768
  }
1586
1769
  };
1587
1770
 
1588
- // src/jssip-lib/sip/client.ts
1589
- var SESSION_DEBUG_KEY = "sip-debug-enabled";
1590
- var SipClient = class extends EventTargetEmitter {
1591
- constructor(options = {}) {
1592
- super();
1593
- this.userAgent = new SipUserAgent();
1594
- this.stateStore = new SipStateStore();
1595
- this.sessionHandlers = /* @__PURE__ */ new Map();
1596
- this.maxSessionCount = Infinity;
1597
- this.sessionManager = new SessionManager();
1598
- this.errorHandler = options.errorHandler ?? new SipErrorHandler({
1599
- formatter: options.formatError,
1600
- messages: options.errorMessages
1771
+ // src/core/modules/ua/ua.handlers.ts
1772
+ function createUAHandlers(deps) {
1773
+ const { emitter, state, cleanupAllSessions, onNewRTCSession } = deps;
1774
+ return {
1775
+ connecting: (e) => {
1776
+ emitter.emit("connecting", e);
1777
+ state.batchSet({ sipStatus: SipStatus.Connecting });
1778
+ },
1779
+ connected: (e) => {
1780
+ emitter.emit("connected", e);
1781
+ state.batchSet({ sipStatus: SipStatus.Connected });
1782
+ },
1783
+ disconnected: (e) => {
1784
+ emitter.emit("disconnected", e);
1785
+ cleanupAllSessions();
1786
+ state.reset();
1787
+ },
1788
+ registered: (e) => {
1789
+ emitter.emit("registered", e);
1790
+ state.batchSet({ sipStatus: SipStatus.Registered, error: null });
1791
+ },
1792
+ unregistered: (e) => {
1793
+ emitter.emit("unregistered", e);
1794
+ state.batchSet({ sipStatus: SipStatus.Unregistered });
1795
+ },
1796
+ registrationFailed: (e) => {
1797
+ emitter.emit("registrationFailed", e);
1798
+ cleanupAllSessions();
1799
+ state.batchSet({
1800
+ sipStatus: SipStatus.RegistrationFailed,
1801
+ error: e?.cause || "registration failed"
1802
+ });
1803
+ },
1804
+ newRTCSession: onNewRTCSession,
1805
+ newMessage: (e) => emitter.emit("newMessage", e),
1806
+ sipEvent: (e) => emitter.emit("sipEvent", e),
1807
+ newOptions: (e) => emitter.emit("newOptions", e)
1808
+ };
1809
+ }
1810
+
1811
+ // src/core/modules/ua/ua.module.ts
1812
+ var UaModule = class {
1813
+ constructor(deps) {
1814
+ this.userAgent = deps.userAgent;
1815
+ this.uaHandlers = deps.createHandlers();
1816
+ this.uaHandlerKeys = Object.keys(this.uaHandlers);
1817
+ }
1818
+ start(uri, password, config, debug) {
1819
+ this.userAgent.start(uri, password, config, { debug });
1820
+ this.attachHandlers();
1821
+ }
1822
+ stop() {
1823
+ this.detachHandlers();
1824
+ this.userAgent.stop();
1825
+ }
1826
+ register() {
1827
+ this.userAgent.register();
1828
+ }
1829
+ setDebug(debug) {
1830
+ this.userAgent.setDebug(debug);
1831
+ }
1832
+ attachHandlers() {
1833
+ const ua = this.userAgent.ua;
1834
+ if (!ua)
1835
+ return;
1836
+ this.detachHandlers();
1837
+ this.uaHandlerKeys.forEach((event) => {
1838
+ const handler = this.uaHandlers[event];
1839
+ if (handler)
1840
+ ua.on(event, handler);
1601
1841
  });
1602
- this.debugPattern = options.debug;
1603
- this.uaHandlers = createUAHandlers({
1604
- emitter: this,
1605
- state: this.stateStore,
1606
- cleanupAllSessions: () => this.cleanupAllSessions(),
1607
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1608
- onNewRTCSession: (e) => this.onNewRTCSession(e)
1842
+ }
1843
+ detachHandlers() {
1844
+ const ua = this.userAgent.ua;
1845
+ if (!ua)
1846
+ return;
1847
+ this.uaHandlerKeys.forEach((event) => {
1848
+ const handler = this.uaHandlers[event];
1849
+ if (handler)
1850
+ ua.off(event, handler);
1609
1851
  });
1610
- this.uaHandlerKeys = Object.keys(this.uaHandlers);
1611
- this.lifecycle = new SessionLifecycle({
1612
- state: this.stateStore,
1613
- sessionManager: this.sessionManager,
1614
- emit: (event, payload) => this.emit(event, payload),
1615
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1616
- attachSessionHandlers: (sessionId, session) => this.attachSessionHandlers(sessionId, session),
1617
- getMaxSessionCount: () => this.maxSessionCount
1852
+ }
1853
+ };
1854
+
1855
+ // src/core/client/sip.client.ts
1856
+ var SipClient = class extends EventTargetEmitter {
1857
+ constructor(options = {}) {
1858
+ super();
1859
+ this.userAgent = new SipUserAgent();
1860
+ this.stateStore = new SipStateStore();
1861
+ this.maxSessionCount = Infinity;
1862
+ this.sessionManager = new SessionManager();
1863
+ this.unloadRuntime = new BrowserUnloadRuntime();
1864
+ this.debugPattern = options.debug;
1865
+ this.uaModule = new UaModule({
1866
+ userAgent: this.userAgent,
1867
+ createHandlers: () => createUAHandlers({
1868
+ emitter: this,
1869
+ state: this.stateStore,
1870
+ cleanupAllSessions: () => this.cleanupAllSessions(),
1871
+ onNewRTCSession: (e) => this.onNewRTCSession(e)
1872
+ })
1618
1873
  });
1619
1874
  this.micRecovery = new MicRecoveryManager({
1620
1875
  getRtc: (sessionId) => this.sessionManager.getRtc(sessionId),
1621
1876
  getSession: (sessionId) => this.sessionManager.getSession(sessionId),
1622
- getSessionState: (sessionId) => this.stateStore.getState().sessions.find((s) => s.id === sessionId),
1623
- setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream),
1624
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1625
- requestMicrophoneStream: (deviceId) => this.requestMicrophoneStreamInternal(deviceId)
1877
+ getSessionState: (sessionId) => this.stateStore.getState().sessionsById[sessionId],
1878
+ setSessionMedia: (sessionId, stream) => this.sessionManager.setSessionMedia(sessionId, stream)
1626
1879
  });
1627
- if (typeof window !== "undefined") {
1628
- window.sipDebugBridge = (debug) => this.setDebug(debug ?? true);
1629
- }
1880
+ this.sessionModule = new SessionModule({
1881
+ state: this.stateStore,
1882
+ emitter: this,
1883
+ sessionManager: this.sessionManager,
1884
+ micRecovery: this.micRecovery,
1885
+ getMaxSessionCount: () => this.maxSessionCount,
1886
+ getIceCandidateReadyDelayMs: () => this.iceCandidateReadyDelayMs
1887
+ });
1888
+ this.debugRuntime = new SipDebugRuntime({
1889
+ getState: () => this.stateStore.getState(),
1890
+ onChange: (listener) => this.stateStore.onChange(listener),
1891
+ getSessions: () => this.getSessions(),
1892
+ setDebugEnabled: (enabled) => this.sessionModule.setDebugEnabled(enabled)
1893
+ });
1894
+ this.debugRuntime.attachBridge(
1895
+ (debug) => this.setDebug(debug)
1896
+ );
1630
1897
  }
1631
1898
  get state() {
1632
1899
  return this.stateStore.getState();
@@ -1640,388 +1907,184 @@ var SipClient = class extends EventTargetEmitter {
1640
1907
  micRecoveryIntervalMs,
1641
1908
  micRecoveryMaxRetries,
1642
1909
  maxSessionCount,
1910
+ iceCandidateReadyDelayMs,
1643
1911
  ...uaCfg
1644
1912
  } = config;
1645
1913
  this.maxSessionCount = typeof maxSessionCount === "number" ? maxSessionCount : Infinity;
1914
+ this.iceCandidateReadyDelayMs = typeof iceCandidateReadyDelayMs === "number" ? iceCandidateReadyDelayMs : void 0;
1646
1915
  this.micRecovery.configure({
1647
1916
  enabled: Boolean(enableMicRecovery),
1648
1917
  intervalMs: micRecoveryIntervalMs,
1649
1918
  maxRetries: micRecoveryMaxRetries
1650
1919
  });
1651
- const debug = cfgDebug ?? this.getPersistedDebug() ?? this.debugPattern;
1652
- this.userAgent.start(uri, password, uaCfg, { debug });
1653
- this.lifecycle.setDebugEnabled(Boolean(debug));
1654
- this.attachUAHandlers();
1655
- this.attachBeforeUnload();
1656
- this.syncDebugInspector(debug);
1920
+ const debug = cfgDebug ?? this.debugRuntime.getPersistedDebug() ?? this.debugPattern;
1921
+ this.uaModule.start(uri, password, uaCfg, debug);
1922
+ this.sessionModule.setDebugEnabled(Boolean(debug));
1923
+ this.unloadRuntime.attach(() => {
1924
+ this.hangupAll();
1925
+ this.disconnect();
1926
+ });
1927
+ this.debugRuntime.syncInspector(debug);
1657
1928
  }
1658
1929
  registerUA() {
1659
- this.userAgent.register();
1930
+ this.uaModule.register();
1660
1931
  }
1661
1932
  disconnect() {
1662
- this.detachBeforeUnload();
1663
- this.detachUAHandlers();
1664
- this.userAgent.stop();
1933
+ this.unloadRuntime.detach();
1934
+ this.uaModule.stop();
1665
1935
  this.cleanupAllSessions();
1666
1936
  this.stateStore.reset();
1937
+ this.debugRuntime.cleanup();
1667
1938
  }
1668
1939
  call(target, callOptions = {}) {
1669
1940
  try {
1670
- const opts = this.ensureMediaConstraints(callOptions);
1671
1941
  const ua = this.userAgent.getUA();
1672
- const session = ua?.call(target, opts);
1673
- if (session && opts.mediaStream) {
1942
+ const session = ua?.call(target, callOptions);
1943
+ if (session && callOptions.mediaStream) {
1674
1944
  const sessionId = String(session?.id ?? "");
1675
1945
  if (sessionId) {
1676
- this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
1677
- this.sessionManager.setSession(sessionId, session);
1946
+ this.sessionModule.setSessionMedia(sessionId, callOptions.mediaStream);
1947
+ this.sessionModule.setSession(sessionId, session);
1678
1948
  }
1679
1949
  }
1680
1950
  } catch (e) {
1681
- const err = this.emitError(e, "CALL_FAILED", "call failed");
1951
+ console.error(e);
1682
1952
  this.cleanupAllSessions();
1683
- this.stateStore.batchSet({
1684
- error: err.cause
1685
- });
1686
1953
  }
1687
1954
  }
1688
- answer(sessionId, options = {}) {
1689
- const resolved = this.resolveExistingSessionId(sessionId);
1690
- if (!resolved)
1691
- return false;
1692
- return this.answerSession(resolved, options);
1693
- }
1694
- hangup(sessionId, options) {
1695
- const resolved = this.resolveExistingSessionId(sessionId);
1696
- if (!resolved)
1697
- return false;
1698
- return this.hangupSession(resolved, options);
1699
- }
1700
1955
  hangupAll(options) {
1701
1956
  const ids = this.getSessionIds();
1702
1957
  ids.forEach((id) => this.hangupSession(id, options));
1703
1958
  return ids.length > 0;
1704
1959
  }
1705
- toggleMute(sessionId) {
1706
- return this.toggleMuteSession(sessionId);
1707
- }
1708
- toggleHold(sessionId) {
1709
- return this.toggleHoldSession(sessionId);
1710
- }
1711
- sendDTMF(sessionId, tones, options) {
1712
- return this.sendDTMFSession(sessionId, tones, options);
1713
- }
1714
- transfer(sessionId, target, options) {
1715
- return this.transferSession(sessionId, target, options);
1716
- }
1717
1960
  onChange(fn) {
1718
1961
  return this.stateStore.onChange(fn);
1719
1962
  }
1720
- attachUAHandlers() {
1721
- const ua = this.userAgent.ua;
1722
- if (!ua)
1723
- return;
1724
- this.detachUAHandlers();
1725
- this.uaHandlerKeys.forEach((ev) => {
1726
- const h = this.uaHandlers[ev];
1727
- if (h)
1728
- ua.on(ev, h);
1729
- });
1730
- }
1731
1963
  setDebug(debug) {
1732
1964
  this.debugPattern = debug;
1733
- this.userAgent.setDebug(debug);
1734
- this.lifecycle.setDebugEnabled(Boolean(debug));
1735
- this.syncDebugInspector(debug);
1736
- }
1737
- attachSessionHandlers(sessionId, session) {
1738
- const handlers = this.createSessionHandlersFor(sessionId, session);
1739
- this.sessionHandlers.set(sessionId, handlers);
1740
- Object.keys(handlers).forEach((ev) => {
1741
- const h = handlers[ev];
1742
- if (h)
1743
- session.on(ev, h);
1744
- });
1745
- }
1746
- detachSessionHandlers(sessionId, session) {
1747
- const handlers = this.sessionHandlers.get(sessionId);
1748
- if (!handlers || !session)
1749
- return;
1750
- Object.keys(handlers).forEach((ev) => {
1751
- const h = handlers[ev];
1752
- if (h)
1753
- session.off(ev, h);
1754
- });
1755
- this.sessionHandlers.delete(sessionId);
1756
- }
1757
- detachUAHandlers() {
1758
- const ua = this.userAgent.ua;
1759
- if (!ua)
1760
- return;
1761
- this.uaHandlerKeys.forEach((ev) => {
1762
- const h = this.uaHandlers[ev];
1763
- if (h)
1764
- ua.off(ev, h);
1765
- });
1766
- }
1767
- cleanupSession(sessionId, session) {
1768
- const targetSession = session ?? this.sessionManager.getSession(sessionId) ?? this.sessionManager.getRtc(sessionId)?.currentSession;
1769
- this.detachSessionHandlers(sessionId, targetSession);
1770
- this.micRecovery.disable(sessionId);
1771
- this.sessionManager.cleanupSession(sessionId);
1772
- removeSessionState(this.stateStore, sessionId);
1965
+ this.uaModule.setDebug(debug);
1966
+ this.sessionModule.setDebugEnabled(Boolean(debug));
1967
+ const effectiveDebug = debug ?? this.debugRuntime.getPersistedDebug() ?? this.debugPattern;
1968
+ this.debugRuntime.syncInspector(effectiveDebug);
1773
1969
  }
1774
1970
  cleanupAllSessions() {
1775
- this.sessionManager.cleanupAllSessions();
1776
- this.micRecovery.cleanupAll();
1777
- this.sessionHandlers.clear();
1778
- this.stateStore.setState({
1779
- sessions: [],
1780
- error: null
1781
- });
1782
- }
1783
- createSessionHandlersFor(sessionId, session) {
1784
- const rtc = this.sessionManager.getOrCreateRtc(sessionId, session);
1785
- return createSessionHandlers({
1786
- emitter: this,
1787
- state: this.stateStore,
1788
- rtc,
1789
- detachSessionHandlers: () => this.cleanupSession(sessionId, session),
1790
- emitError: (raw, code, fallback) => this.emitError(raw, code, fallback),
1791
- onSessionFailed: (err, event) => this.onSessionFailed(err, event),
1792
- enableMicrophoneRecovery: (confirmedSessionId) => this.micRecovery.enable(confirmedSessionId),
1793
- sessionId
1794
- });
1971
+ this.sessionModule.cleanupAllSessions();
1795
1972
  }
1796
1973
  onNewRTCSession(e) {
1797
- this.lifecycle.handleNewRTCSession(e);
1798
- }
1799
- onSessionFailed(error, event) {
1800
- const rawCause = event?.cause ?? error;
1801
- const statusCode = event?.message?.status_code;
1802
- const statusText = event?.message?.reason_phrase;
1803
- const causeText = rawCause || (statusCode ? `${statusCode}${statusText ? " " + statusText : ""}` : "call failed");
1804
- this.emitError(
1805
- { raw: event, cause: rawCause, statusCode, statusText },
1806
- "SESSION_FAILED",
1807
- causeText
1808
- );
1809
- }
1810
- emitError(raw, code, fallback) {
1811
- const payload = this.errorHandler.format({ raw, code, fallback });
1812
- this.emit("error", payload);
1813
- return payload;
1814
- }
1815
- resolveSessionId(sessionId) {
1816
- if (sessionId)
1817
- return sessionId;
1818
- const sessions = this.stateStore.getState().sessions;
1819
- const active = sessions.find((s) => s.status === CallStatus.Active);
1820
- return active?.id ?? sessions[0]?.id ?? null;
1821
- }
1822
- sessionExists(sessionId) {
1823
- return !!this.sessionManager.getSession(sessionId) || !!this.sessionManager.getRtc(sessionId);
1824
- }
1825
- resolveExistingSessionId(sessionId) {
1826
- const id = this.resolveSessionId(sessionId);
1827
- if (!id)
1828
- return null;
1829
- return this.sessionExists(id) ? id : null;
1830
- }
1831
- ensureMediaConstraints(opts) {
1832
- if (opts.mediaStream || opts.mediaConstraints)
1833
- return opts;
1834
- return { ...opts, mediaConstraints: { audio: true, video: false } };
1974
+ this.sessionModule.handleNewRTCSession(e);
1835
1975
  }
1836
1976
  answerSession(sessionId, options = {}) {
1837
- if (!sessionId || !this.sessionExists(sessionId))
1838
- return false;
1839
- const opts = this.ensureMediaConstraints(options);
1840
- if (opts.mediaStream) {
1841
- this.sessionManager.setSessionMedia(sessionId, opts.mediaStream);
1977
+ if (options.mediaStream) {
1978
+ this.sessionModule.setSessionMedia(sessionId, options.mediaStream);
1842
1979
  }
1843
- return this.sessionManager.answer(sessionId, opts);
1980
+ return this.sessionModule.answerSession(sessionId, options);
1844
1981
  }
1845
1982
  hangupSession(sessionId, options) {
1846
- if (!sessionId || !this.sessionExists(sessionId))
1847
- return false;
1848
- return this.sessionManager.hangup(sessionId, options);
1983
+ return this.sessionModule.hangupSession(sessionId, options);
1849
1984
  }
1850
1985
  toggleMuteSession(sessionId) {
1851
- const resolved = this.resolveExistingSessionId(sessionId);
1852
- if (!resolved)
1853
- return false;
1854
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1855
- const muted = sessionState?.muted ?? false;
1856
- if (muted) {
1857
- this.sessionManager.unmute(resolved);
1858
- return true;
1859
- }
1860
- this.sessionManager.mute(resolved);
1861
- return true;
1986
+ return this.sessionModule.toggleMuteSession(sessionId);
1862
1987
  }
1863
1988
  toggleHoldSession(sessionId) {
1864
- const resolved = this.resolveExistingSessionId(sessionId);
1865
- if (!resolved)
1866
- return false;
1867
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1868
- const isOnHold = sessionState?.status === CallStatus.Hold;
1869
- if (isOnHold) {
1870
- this.sessionManager.unhold(resolved);
1871
- return true;
1872
- }
1873
- if (sessionState?.status === CallStatus.Active) {
1874
- this.sessionManager.hold(resolved);
1875
- return true;
1876
- }
1877
- return true;
1989
+ return this.sessionModule.toggleHoldSession(sessionId);
1878
1990
  }
1879
1991
  sendDTMFSession(sessionId, tones, options) {
1880
- const resolved = this.resolveExistingSessionId(sessionId);
1881
- if (!resolved)
1882
- return false;
1883
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1884
- if (sessionState?.status === CallStatus.Active)
1885
- this.sessionManager.sendDTMF(resolved, tones, options);
1886
- return true;
1992
+ return this.sessionModule.sendDTMFSession(sessionId, tones, options);
1887
1993
  }
1888
1994
  transferSession(sessionId, target, options) {
1889
- const resolved = this.resolveExistingSessionId(sessionId);
1890
- if (!resolved)
1891
- return false;
1892
- const sessionState = this.stateStore.getState().sessions.find((s) => s.id === resolved);
1893
- if (sessionState?.status === CallStatus.Active)
1894
- this.sessionManager.transfer(resolved, target, options);
1895
- return true;
1995
+ return this.sessionModule.transferSession(sessionId, target, options);
1896
1996
  }
1897
1997
  setSessionMedia(sessionId, stream) {
1898
- this.sessionManager.setSessionMedia(sessionId, stream);
1899
- }
1900
- switchCameraSession(sessionId, track) {
1901
- if (!this.sessionExists(sessionId))
1902
- return false;
1903
- const rtc = this.sessionManager.getRtc(sessionId);
1904
- return rtc ? rtc.switchCamera(track) : false;
1905
- }
1906
- enableVideoSession(sessionId) {
1907
- if (!this.sessionExists(sessionId))
1908
- return false;
1909
- const rtc = this.sessionManager.getRtc(sessionId);
1910
- rtc?.enableVideo();
1911
- return !!rtc;
1912
- }
1913
- disableVideoSession(sessionId) {
1914
- if (!this.sessionExists(sessionId))
1915
- return false;
1916
- const rtc = this.sessionManager.getRtc(sessionId);
1917
- rtc?.disableVideo();
1918
- return !!rtc;
1998
+ this.sessionModule.setSessionMedia(sessionId, stream);
1919
1999
  }
1920
2000
  getSession(sessionId) {
1921
- return this.sessionManager.getSession(sessionId);
2001
+ return this.sessionModule.getSession(sessionId);
1922
2002
  }
1923
2003
  getSessionIds() {
1924
- return this.sessionManager.getSessionIds();
2004
+ return this.sessionModule.getSessionIds();
1925
2005
  }
1926
2006
  getSessions() {
1927
- return this.sessionManager.getSessions();
1928
- }
1929
- attachBeforeUnload() {
1930
- if (typeof window === "undefined" || this.unloadHandler)
1931
- return;
1932
- const handler = () => {
1933
- this.hangupAll();
1934
- this.disconnect();
1935
- };
1936
- window.addEventListener("beforeunload", handler);
1937
- this.unloadHandler = handler;
1938
- }
1939
- detachBeforeUnload() {
1940
- if (typeof window === "undefined" || !this.unloadHandler)
1941
- return;
1942
- window.removeEventListener("beforeunload", this.unloadHandler);
1943
- this.unloadHandler = void 0;
1944
- }
1945
- syncDebugInspector(debug) {
1946
- if (typeof window === "undefined")
1947
- return;
1948
- const persisted = this.getPersistedDebug();
1949
- const effectiveDebug = debug ?? persisted ?? this.debugPattern;
1950
- this.lifecycle.setDebugEnabled(Boolean(effectiveDebug));
1951
- this.toggleStateLogger(Boolean(effectiveDebug));
1952
- const win = window;
1953
- const disabledInspector = () => {
1954
- console.warn("SIP debug inspector disabled; enable debug to inspect.");
1955
- return null;
1956
- };
1957
- win.sipState = () => effectiveDebug ? this.stateStore.getState() : disabledInspector();
1958
- win.sipSessions = () => effectiveDebug ? this.getSessions() : disabledInspector();
1959
- }
1960
- toggleStateLogger(enabled) {
1961
- if (!enabled) {
1962
- this.stateLogOff?.();
1963
- this.stateLogOff = void 0;
1964
- return;
1965
- }
1966
- if (this.stateLogOff)
1967
- return;
1968
- let prev = this.stateStore.getState();
1969
- console.info("[sip][state]", { initial: true }, prev);
1970
- this.stateLogOff = this.stateStore.onChange((next) => {
1971
- console.info("[sip][state]", next);
1972
- prev = next;
1973
- });
1974
- }
1975
- getPersistedDebug() {
1976
- if (typeof window === "undefined")
1977
- return void 0;
1978
- try {
1979
- const persisted = window.sessionStorage.getItem(SESSION_DEBUG_KEY);
1980
- if (!persisted)
1981
- return void 0;
1982
- return persisted;
1983
- } catch {
1984
- return void 0;
1985
- }
1986
- }
1987
- async requestMicrophoneStreamInternal(deviceId) {
1988
- if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
1989
- throw new Error("getUserMedia not available");
1990
- }
1991
- const audio = deviceId && deviceId !== "default" ? { deviceId: { exact: deviceId } } : true;
1992
- try {
1993
- return await navigator.mediaDevices.getUserMedia({ audio });
1994
- } catch (err) {
1995
- const cause = err?.name || "getUserMedia failed";
1996
- this.emitError(
1997
- { raw: err, cause },
1998
- "MICROPHONE_UNAVAILABLE",
1999
- "microphone unavailable"
2000
- );
2001
- throw err;
2002
- }
2007
+ return this.sessionModule.getSessions();
2003
2008
  }
2004
2009
  };
2005
2010
  function createSipClientInstance(options) {
2006
2011
  return new SipClient(options);
2007
2012
  }
2008
- function createSipEventManager(client) {
2013
+
2014
+ // src/core/modules/media/media.module.ts
2015
+ function createMediaModule(deps) {
2016
+ const { client, eventManager } = deps;
2009
2017
  return {
2010
- onUA(event, handler) {
2011
- return client.on(event, handler);
2018
+ getSession(sessionId) {
2019
+ return client.getSession(sessionId);
2012
2020
  },
2013
- onSession(sessionId, event, handler) {
2021
+ observePeerConnection(sessionId, onPeerConnection) {
2014
2022
  const session = client.getSession(sessionId);
2015
- if (!session)
2023
+ if (!session) {
2024
+ onPeerConnection(null);
2016
2025
  return () => {
2017
2026
  };
2018
- session.on(event, handler);
2019
- return () => session.off(event, handler);
2027
+ }
2028
+ const initialPc = session.connection ?? null;
2029
+ onPeerConnection(initialPc);
2030
+ return eventManager.onSession(sessionId, "peerconnection", (payload) => {
2031
+ const pc = payload?.peerconnection ?? null;
2032
+ onPeerConnection(pc);
2033
+ });
2034
+ },
2035
+ buildRemoteStream(peerConnection) {
2036
+ if (!peerConnection || typeof peerConnection.getReceivers !== "function") {
2037
+ return null;
2038
+ }
2039
+ const tracks = peerConnection.getReceivers().map((receiver) => receiver.track).filter((track) => Boolean(track));
2040
+ if (tracks.length === 0)
2041
+ return null;
2042
+ return new MediaStream(tracks);
2020
2043
  }
2021
2044
  };
2022
2045
  }
2046
+
2047
+ // src/core/kernel/createSipKernel.ts
2048
+ function createSipKernel() {
2049
+ const client = createSipClientInstance();
2050
+ const eventManager = createSipEventManager(client);
2051
+ const media = createMediaModule({ client, eventManager });
2052
+ return {
2053
+ client,
2054
+ store: {
2055
+ getState: () => client.state,
2056
+ subscribe: (onStoreChange) => client.onChange(onStoreChange)
2057
+ },
2058
+ commands: {
2059
+ connect: (uri, password, config) => client.connect(uri, password, config),
2060
+ disconnect: () => client.disconnect(),
2061
+ register: () => client.registerUA(),
2062
+ setDebug: (debug) => client.setDebug(debug),
2063
+ call: (target, options) => client.call(target, options),
2064
+ answer: (sessionId, options) => client.answerSession(sessionId, options),
2065
+ hangup: (sessionId, options) => client.hangupSession(sessionId, options),
2066
+ hangupAll: (options) => client.hangupAll(options),
2067
+ toggleMute: (sessionId) => client.toggleMuteSession(sessionId),
2068
+ toggleHold: (sessionId) => client.toggleHoldSession(sessionId),
2069
+ sendDTMF: (sessionId, tones, options) => client.sendDTMFSession(sessionId, tones, options),
2070
+ transfer: (sessionId, target, options) => client.transferSession(sessionId, target, options),
2071
+ getSession: (sessionId) => client.getSession(sessionId),
2072
+ getSessionIds: () => client.getSessionIds(),
2073
+ getSessions: () => client.getSessions(),
2074
+ setSessionMedia: (sessionId, stream) => client.setSessionMedia(sessionId, stream)
2075
+ },
2076
+ events: {
2077
+ onUA: (event, handler) => eventManager.onUA(event, handler),
2078
+ onSession: (sessionId, event, handler) => eventManager.onSession(sessionId, event, handler)
2079
+ },
2080
+ eventManager,
2081
+ media
2082
+ };
2083
+ }
2023
2084
  var SipContext = react.createContext(null);
2024
- function useSip() {
2085
+
2086
+ // src/hooks/useSip.ts
2087
+ function useSipKernel() {
2025
2088
  const ctx = react.useContext(SipContext);
2026
2089
  if (!ctx)
2027
2090
  throw new Error("Must be used within SipProvider");
@@ -2030,177 +2093,190 @@ function useSip() {
2030
2093
 
2031
2094
  // src/hooks/useSipState.ts
2032
2095
  function useSipState() {
2033
- const { client } = useSip();
2034
- const subscribe = react.useCallback(
2035
- (onStoreChange) => client.onChange(onStoreChange),
2036
- [client]
2037
- );
2038
- const getSnapshot = react.useCallback(() => client.state, [client]);
2039
- return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
2096
+ const { store } = useSipKernel();
2097
+ return react.useSyncExternalStore(store.subscribe, store.getState, store.getState);
2040
2098
  }
2041
2099
  function useSipActions() {
2042
- const { client } = useSip();
2100
+ const { commands } = useSipKernel();
2043
2101
  return react.useMemo(
2044
2102
  () => ({
2045
- call: (...args) => client.call(...args),
2046
- answer: (...args) => client.answerSession(...args),
2047
- hangup: (...args) => client.hangupSession(...args),
2048
- toggleMute: (...args) => client.toggleMuteSession(...args),
2049
- toggleHold: (...args) => client.toggleHoldSession(...args),
2050
- sendDTMF: (...args) => client.sendDTMFSession(...args),
2051
- transfer: (...args) => client.transferSession(...args),
2052
- getSession: (...args) => client.getSession(...args),
2053
- getSessionIds: () => client.getSessionIds(),
2054
- getSessions: () => client.getSessions(),
2055
- setSessionMedia: (...args) => client.setSessionMedia(...args),
2056
- switchCamera: (...args) => client.switchCameraSession(...args),
2057
- enableVideo: (...args) => client.enableVideoSession(...args),
2058
- disableVideo: (...args) => client.disableVideoSession(...args)
2103
+ connect: commands.connect,
2104
+ disconnect: commands.disconnect,
2105
+ register: commands.register,
2106
+ setDebug: commands.setDebug,
2107
+ call: commands.call,
2108
+ answer: commands.answer,
2109
+ hangup: commands.hangup,
2110
+ hangupAll: commands.hangupAll,
2111
+ toggleMute: commands.toggleMute,
2112
+ toggleHold: commands.toggleHold,
2113
+ sendDTMF: commands.sendDTMF,
2114
+ transfer: commands.transfer,
2115
+ getSession: commands.getSession,
2116
+ getSessionIds: commands.getSessionIds,
2117
+ getSessions: commands.getSessions,
2118
+ setSessionMedia: commands.setSessionMedia
2059
2119
  }),
2060
- [client]
2120
+ [commands]
2061
2121
  );
2062
2122
  }
2123
+ function useSipSelector(selector, equalityFn = Object.is) {
2124
+ const { store } = useSipKernel();
2125
+ const selectorRef = react.useRef(selector);
2126
+ const equalityFnRef = react.useRef(equalityFn);
2127
+ const selectedRef = react.useRef(void 0);
2128
+ const hasSelectedRef = react.useRef(false);
2129
+ selectorRef.current = selector;
2130
+ equalityFnRef.current = equalityFn;
2131
+ const getSelection = () => {
2132
+ const nextSelected = selectorRef.current(store.getState());
2133
+ if (hasSelectedRef.current && equalityFnRef.current(selectedRef.current, nextSelected)) {
2134
+ return selectedRef.current;
2135
+ }
2136
+ hasSelectedRef.current = true;
2137
+ selectedRef.current = nextSelected;
2138
+ return nextSelected;
2139
+ };
2140
+ return react.useSyncExternalStore(store.subscribe, getSelection, getSelection);
2141
+ }
2142
+
2143
+ // src/hooks/useActiveSipSession.ts
2144
+ function useActiveSipSession() {
2145
+ return useSipSelector((state) => {
2146
+ const activeId = state.sessionIds.find(
2147
+ (id) => state.sessionsById[id]?.status === CallStatus.Active
2148
+ );
2149
+ return activeId ? state.sessionsById[activeId] : null;
2150
+ });
2151
+ }
2063
2152
 
2064
- // src/hooks/useSipSessions.ts
2153
+ // src/hooks/useSipSession.ts
2154
+ function useSipSession(sessionId) {
2155
+ return useSipSelector((state) => {
2156
+ if (!sessionId)
2157
+ return null;
2158
+ return state.sessionsById[sessionId] ?? null;
2159
+ });
2160
+ }
2065
2161
  function useSipSessions() {
2066
- const { sessions } = useSipState();
2067
- return { sessions };
2162
+ const sessions = useSipSelector((state) => state.sessions);
2163
+ return react.useMemo(() => ({ sessions }), [sessions]);
2068
2164
  }
2069
2165
  function useSipEvent(event, handler) {
2070
- const { sipEventManager } = useSip();
2166
+ const { events } = useSipKernel();
2071
2167
  react.useEffect(() => {
2072
2168
  if (!handler)
2073
2169
  return;
2074
- return sipEventManager.onUA(event, handler);
2075
- }, [event, handler, sipEventManager]);
2170
+ return events.onUA(event, handler);
2171
+ }, [event, handler, events]);
2076
2172
  }
2077
2173
  function useSipSessionEvent(sessionId, event, handler) {
2078
- const { sipEventManager } = useSip();
2174
+ const { events } = useSipKernel();
2079
2175
  react.useEffect(() => {
2080
2176
  if (!handler)
2081
2177
  return;
2082
- return sipEventManager.onSession(sessionId, event, handler);
2083
- }, [event, handler, sessionId, sipEventManager]);
2178
+ return events.onSession(sessionId, event, handler);
2179
+ }, [event, handler, sessionId, events]);
2084
2180
  }
2085
-
2086
- // src/jssip-lib/adapters/dom/createCallPlayer.ts
2087
- function createCallPlayer(audioEl) {
2088
- let cleanupTrackListener = null;
2089
- let cleanupSessionPeerListener = null;
2090
- let cleanupClientListeners = null;
2091
- const dispose = (fn) => {
2092
- if (fn)
2093
- fn();
2094
- return null;
2095
- };
2096
- function clearAudioStream(stream) {
2097
- if (stream) {
2098
- for (const t of stream.getTracks()) {
2099
- t.stop();
2100
- }
2181
+ function useSessionMedia(sessionId) {
2182
+ const { media } = useSipKernel();
2183
+ const { sessionIds, sessionsById } = useSipSelector(
2184
+ (state) => ({
2185
+ sessionIds: state.sessionIds,
2186
+ sessionsById: state.sessionsById
2187
+ }),
2188
+ (prev, next) => prev.sessionIds === next.sessionIds && prev.sessionsById === next.sessionsById
2189
+ );
2190
+ const [peerConnection, setPeerConnection] = react.useState(
2191
+ null
2192
+ );
2193
+ const [remoteStream, setRemoteStream] = react.useState(null);
2194
+ const resolvedSessionId = react.useMemo(() => {
2195
+ if (sessionId)
2196
+ return sessionId;
2197
+ const activeId = sessionIds.find(
2198
+ (id) => sessionsById[id]?.status === CallStatus.Active
2199
+ );
2200
+ return activeId ?? sessionIds[0];
2201
+ }, [sessionId, sessionIds, sessionsById]);
2202
+ const session = react.useMemo(
2203
+ () => resolvedSessionId ? media.getSession(resolvedSessionId) : null,
2204
+ [media, resolvedSessionId]
2205
+ );
2206
+ const sessionState = react.useMemo(() => {
2207
+ if (!resolvedSessionId)
2208
+ return null;
2209
+ return sessionsById[resolvedSessionId] ?? null;
2210
+ }, [sessionsById, resolvedSessionId]);
2211
+ react.useEffect(() => {
2212
+ if (!resolvedSessionId) {
2213
+ setPeerConnection(null);
2214
+ setRemoteStream(null);
2215
+ return;
2101
2216
  }
2102
- audioEl.srcObject = null;
2103
- }
2104
- const attachTracks = (pc) => {
2105
- const onTrack = (e) => {
2106
- if (e.track.kind !== "audio")
2107
- return;
2108
- const nextStream = e.streams?.[0] ?? new MediaStream([e.track]);
2109
- const prev = audioEl.srcObject;
2110
- if (prev && prev !== nextStream) {
2111
- clearAudioStream(prev);
2112
- }
2113
- audioEl.srcObject = nextStream;
2114
- audioEl.play?.().catch(() => {
2115
- });
2116
- };
2117
- pc.addEventListener("track", onTrack);
2118
- return () => pc.removeEventListener("track", onTrack);
2119
- };
2120
- const listenSessionPeerconnection = (session) => {
2121
- const onPeer = (data) => {
2122
- cleanupTrackListener = dispose(cleanupTrackListener);
2123
- cleanupTrackListener = attachTracks(data.peerconnection);
2124
- };
2125
- session.on("peerconnection", onPeer);
2126
- return () => session.off("peerconnection", onPeer);
2127
- };
2128
- function bindToSession(session) {
2129
- clearAudioStream(audioEl.srcObject);
2130
- if (session?.direction === "outgoing" && session.connection instanceof RTCPeerConnection) {
2131
- cleanupTrackListener = dispose(cleanupTrackListener);
2132
- cleanupTrackListener = attachTracks(session.connection);
2217
+ const off = media.observePeerConnection(resolvedSessionId, (pc) => {
2218
+ setPeerConnection(pc);
2219
+ setRemoteStream(media.buildRemoteStream(pc));
2220
+ });
2221
+ return off;
2222
+ }, [media, resolvedSessionId]);
2223
+ react.useEffect(() => {
2224
+ if (!peerConnection) {
2225
+ setRemoteStream(null);
2226
+ return;
2133
2227
  }
2134
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2135
- cleanupSessionPeerListener = listenSessionPeerconnection(session);
2228
+ const update = () => {
2229
+ setRemoteStream(media.buildRemoteStream(peerConnection));
2230
+ };
2231
+ peerConnection.addEventListener("track", update);
2232
+ peerConnection.addEventListener("connectionstatechange", update);
2233
+ peerConnection.addEventListener("iceconnectionstatechange", update);
2234
+ update();
2136
2235
  return () => {
2137
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2138
- cleanupTrackListener = dispose(cleanupTrackListener);
2236
+ peerConnection.removeEventListener("track", update);
2237
+ peerConnection.removeEventListener("connectionstatechange", update);
2238
+ peerConnection.removeEventListener("iceconnectionstatechange", update);
2139
2239
  };
2140
- }
2141
- function bindToClient(client) {
2142
- const offNew = client.on("newRTCSession", (payload) => {
2143
- const e = payload?.data;
2144
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2145
- cleanupTrackListener = dispose(cleanupTrackListener);
2146
- clearAudioStream(audioEl.srcObject);
2147
- if (!e?.session)
2148
- return;
2149
- cleanupSessionPeerListener = listenSessionPeerconnection(e.session);
2150
- if (e.session.direction === "outgoing" && e.session.connection instanceof RTCPeerConnection) {
2151
- cleanupTrackListener = attachTracks(e.session.connection);
2152
- }
2153
- });
2154
- const offEnded = client.on("ended", () => detach());
2155
- const offFailed = client.on("failed", () => detach());
2156
- const offDisconnected = client.on("disconnected", () => detach());
2157
- cleanupClientListeners = () => {
2158
- offNew();
2159
- offEnded();
2160
- offFailed();
2161
- offDisconnected();
2240
+ }, [media, peerConnection]);
2241
+ const tracks = remoteStream?.getTracks() ?? [];
2242
+ const audioTracks = tracks.filter((track) => track.kind === "audio");
2243
+ if (!sessionState) {
2244
+ return {
2245
+ sessionId: resolvedSessionId ?? "",
2246
+ session,
2247
+ peerConnection,
2248
+ remoteStream,
2249
+ audioTracks
2162
2250
  };
2163
- return cleanupClientListeners;
2164
- }
2165
- function detach() {
2166
- cleanupClientListeners = dispose(cleanupClientListeners);
2167
- cleanupSessionPeerListener = dispose(cleanupSessionPeerListener);
2168
- cleanupTrackListener = dispose(cleanupTrackListener);
2169
- clearAudioStream(audioEl.srcObject);
2170
2251
  }
2171
2252
  return {
2172
- bindToSession,
2173
- bindToClient,
2174
- detach
2253
+ sessionId: sessionState.id,
2254
+ session,
2255
+ peerConnection,
2256
+ remoteStream,
2257
+ audioTracks
2175
2258
  };
2176
2259
  }
2177
2260
  function CallPlayer({ sessionId }) {
2178
- const { client } = useSip();
2261
+ const { remoteStream } = useSessionMedia(sessionId);
2179
2262
  const audioRef = react.useRef(null);
2180
2263
  react.useEffect(() => {
2181
2264
  if (!audioRef.current)
2182
2265
  return;
2183
- const player = createCallPlayer(audioRef.current);
2184
- const session = sessionId ? client.getSession(sessionId) : null;
2185
- const off = session ? player.bindToSession(session) : player.bindToClient(client);
2266
+ audioRef.current.srcObject = remoteStream;
2267
+ audioRef.current.play?.().catch(() => {
2268
+ });
2186
2269
  return () => {
2187
- off?.();
2188
- player.detach();
2270
+ if (audioRef.current) {
2271
+ audioRef.current.srcObject = null;
2272
+ }
2189
2273
  };
2190
- }, [client, sessionId]);
2274
+ }, [remoteStream]);
2191
2275
  return /* @__PURE__ */ jsxRuntime.jsx("audio", { ref: audioRef, autoPlay: true, playsInline: true });
2192
2276
  }
2193
- function SipProvider({
2194
- client,
2195
- children,
2196
- sipEventManager
2197
- }) {
2198
- const manager = react.useMemo(
2199
- () => sipEventManager ?? createSipEventManager(client),
2200
- [client, sipEventManager]
2201
- );
2202
- const contextValue = react.useMemo(() => ({ client, sipEventManager: manager }), [client, manager]);
2203
- return /* @__PURE__ */ jsxRuntime.jsx(SipContext.Provider, { value: contextValue, children });
2277
+ function SipProvider(props) {
2278
+ const contextValue = react.useMemo(() => props.kernel, [props.kernel]);
2279
+ return /* @__PURE__ */ jsxRuntime.jsx(SipContext.Provider, { value: contextValue, children: props.children });
2204
2280
  }
2205
2281
 
2206
2282
  Object.defineProperty(exports, "WebSocketInterface", {
@@ -2209,14 +2285,18 @@ Object.defineProperty(exports, "WebSocketInterface", {
2209
2285
  });
2210
2286
  exports.CallPlayer = CallPlayer;
2211
2287
  exports.CallStatus = CallStatus;
2212
- exports.SipContext = SipContext;
2213
2288
  exports.SipProvider = SipProvider;
2214
2289
  exports.SipStatus = SipStatus;
2215
2290
  exports.createSipClientInstance = createSipClientInstance;
2216
2291
  exports.createSipEventManager = createSipEventManager;
2217
- exports.useSip = useSip;
2292
+ exports.createSipKernel = createSipKernel;
2293
+ exports.useActiveSipSession = useActiveSipSession;
2294
+ exports.useSessionMedia = useSessionMedia;
2218
2295
  exports.useSipActions = useSipActions;
2219
2296
  exports.useSipEvent = useSipEvent;
2297
+ exports.useSipKernel = useSipKernel;
2298
+ exports.useSipSelector = useSipSelector;
2299
+ exports.useSipSession = useSipSession;
2220
2300
  exports.useSipSessionEvent = useSipSessionEvent;
2221
2301
  exports.useSipSessions = useSipSessions;
2222
2302
  exports.useSipState = useSipState;