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