mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
15 Commits
feat/playb
...
alpha-1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48e860112b | ||
|
|
5f1eead24f | ||
|
|
592b0fa2aa | ||
|
|
12b6f58a0d | ||
|
|
aa9e781baf | ||
|
|
50011e01dd | ||
|
|
74404c155f | ||
|
|
a3c077586f | ||
|
|
ed838ffa28 | ||
|
|
e23573e833 | ||
|
|
7581eca524 | ||
|
|
ff8b6c6b18 | ||
|
|
52f790836a | ||
|
|
017b3a66a8 | ||
|
|
d82e2254ab |
6
app.js
6
app.js
@@ -29,7 +29,11 @@ const {
|
||||
// HTTP
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
app.locals.logger = logger;
|
||||
Object.assign(app.locals, {
|
||||
logger,
|
||||
srf
|
||||
});
|
||||
|
||||
const httpRoutes = require('./lib/http-routes');
|
||||
|
||||
const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||
|
||||
@@ -7,29 +7,21 @@ const SipError = require('drachtio-srf').SipError;
|
||||
const sysError = require('./error');
|
||||
const Requestor = require('../../utils/requestor');
|
||||
|
||||
/**
|
||||
* Retrieve a connection to a drachtio server, lazily creating when first called
|
||||
*/
|
||||
function getSrfForOutdial(logger) {
|
||||
const {srf} = require('../../../');
|
||||
const {getSrf} = srf.locals;
|
||||
const srfForOutdial = getSrf();
|
||||
if (!srfForOutdial) throw new Error('no available feature servers for outbound call creation');
|
||||
return srfForOutdial;
|
||||
}
|
||||
|
||||
router.post('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const {logger, srf} = req.app.locals;
|
||||
|
||||
logger.debug({body: req.body}, 'got createCall request');
|
||||
try {
|
||||
let uri, cs, to;
|
||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
||||
const srf = getSrfForOutdial(logger);
|
||||
const {getSBC, getFreeswitch} = srf.locals;
|
||||
const sbcAddress = getSBC();
|
||||
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
||||
const target = restDial.to;
|
||||
const opts = { callingNumber: restDial.from };
|
||||
const opts = {
|
||||
callingNumber: restDial.from,
|
||||
headers: req.body.headers || {}
|
||||
};
|
||||
|
||||
switch (target.type) {
|
||||
case 'phone':
|
||||
@@ -90,9 +82,9 @@ router.post('/', async(req, res) => {
|
||||
if (err) {
|
||||
logger.error(err, 'createCall Error creating call');
|
||||
res.status(500).send('Call Failure');
|
||||
ep.destroy();
|
||||
return;
|
||||
}
|
||||
/* ok our outbound NVITE is in flight */
|
||||
/* ok our outbound INVITE is in flight */
|
||||
|
||||
const tasks = [restDial];
|
||||
const callInfo = new CallInfo({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const {CallDirection, CallStatus} = require('../utils/constants');
|
||||
const parseUri = require('drachtio-srf').parseUri;
|
||||
const uuidv4 = require('uuid/v4');
|
||||
|
||||
/**
|
||||
@@ -7,16 +8,22 @@ const uuidv4 = require('uuid/v4');
|
||||
*/
|
||||
class CallInfo {
|
||||
constructor(opts) {
|
||||
let from ;
|
||||
this.direction = opts.direction;
|
||||
if (opts.req) {
|
||||
const u = opts.req.getParsedHeader('from');
|
||||
const uri = parseUri(u.uri);
|
||||
from = uri.user;
|
||||
this.callerName = u.name || '';
|
||||
}
|
||||
if (this.direction === CallDirection.Inbound) {
|
||||
// inbound call
|
||||
const {app, req} = opts;
|
||||
this.callSid = req.locals.callSid,
|
||||
this.accountSid = app.account_sid,
|
||||
this.applicationSid = app.application_sid;
|
||||
this.from = req.callingNumber;
|
||||
this.from = from || req.callingNumber;
|
||||
this.to = req.calledNumber;
|
||||
this.callerName = this.from.name || req.callingNumber;
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.callStatus = CallStatus.Trying;
|
||||
@@ -30,7 +37,7 @@ class CallInfo {
|
||||
this.parentCallSid = parentCallInfo.callSid;
|
||||
this.accountSid = parentCallInfo.accountSid;
|
||||
this.applicationSid = parentCallInfo.applicationSid;
|
||||
this.from = req.callingNumber;
|
||||
this.from = from || req.callingNumber;
|
||||
this.to = to;
|
||||
this.callerId = this.from.name || req.callingNumber;
|
||||
this.callId = req.get('Call-ID');
|
||||
@@ -46,7 +53,7 @@ class CallInfo {
|
||||
this.callStatus = CallStatus.Trying,
|
||||
this.callId = req.get('Call-ID');
|
||||
this.sipStatus = 100;
|
||||
this.from = req.callingNumber;
|
||||
this.from = from || req.callingNumber;
|
||||
this.to = to;
|
||||
if (tag) this._customerData = tag;
|
||||
}
|
||||
|
||||
@@ -421,6 +421,7 @@ class CallSession extends Emitter {
|
||||
uas.callSid = this.callSid;
|
||||
uas.connectTime = moment();
|
||||
this.dlg = uas;
|
||||
this.wrapDialog(this.dlg);
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug('CallSession:_evalEndpointPrecondition - answered call');
|
||||
};
|
||||
@@ -532,6 +533,57 @@ class CallSession extends Emitter {
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this whenever we answer the A leg, creating a dialog
|
||||
* It wraps the 'destroy' method such that if we hang up the A leg
|
||||
* (e.g. via 'hangup' verb) we emit a callStatusChange event
|
||||
* @param {SipDialog} dlg
|
||||
*/
|
||||
wrapDialog(dlg) {
|
||||
dlg.connectTime = moment();
|
||||
const origDestroy = dlg.destroy.bind(dlg);
|
||||
dlg.destroy = () => {
|
||||
if (dlg.connected) {
|
||||
dlg.connected = false;
|
||||
dlg.destroy = origDestroy;
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('CallSession: call terminated by jambones');
|
||||
origDestroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer the call, if it has not already been answered.
|
||||
*
|
||||
* NB: This should be the one and only place we generate 200 OK to incoming INVITEs
|
||||
*/
|
||||
async propagateAnswer() {
|
||||
if (!this.dlg) {
|
||||
assert(this.ep);
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.wrapDialog(this.dlg);
|
||||
this.dlg.callSid = this.callSid;
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
|
||||
this.dlg.on('modify', this._onReinvite.bind(this));
|
||||
|
||||
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
||||
}
|
||||
}
|
||||
|
||||
async _onReinvite(req, res) {
|
||||
try {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error handling reinvite');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called any time call status changes. This method both invokes the
|
||||
* call_status_hook callback as well as updates the realtime database
|
||||
|
||||
@@ -31,26 +31,6 @@ class InboundCallSession extends CallSession {
|
||||
this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite');
|
||||
this.res.send(603);
|
||||
}
|
||||
else if (this.dlg && this.dlg.connected) {
|
||||
assert(this.dlg.connectTime);
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession:_onTasksDone hanging up call since all tasks are done');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Answer the call, if it has not already been answered.
|
||||
*/
|
||||
async propagateAnswer() {
|
||||
if (!this.dlg) {
|
||||
assert(this.ep);
|
||||
this.dlg = await this.srf.createUAS(this.req, this.res, {localSdp: this.ep.local.sdp});
|
||||
this.dlg.connectTime = moment();
|
||||
this.dlg.on('destroy', this._callerHungup.bind(this));
|
||||
this.emit('callStatusChange', {sipStatus: 200, callStatus: CallStatus.InProgress});
|
||||
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ class RestCallSession extends CallSession {
|
||||
setDialog(dlg) {
|
||||
this.dlg = dlg;
|
||||
dlg.on('destroy', this._callerHungup.bind(this));
|
||||
dlg.connectTime = moment();
|
||||
this.wrapDialog(dlg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ class RestCallSession extends CallSession {
|
||||
_callerHungup() {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||
this.logger.debug('InboundCallSession: caller hung up');
|
||||
this.logger.debug('RestCallSession: called party hung up');
|
||||
this._callReleased();
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@ class TaskDial extends Task {
|
||||
}
|
||||
else {
|
||||
this.epOther = cs.ep;
|
||||
if (this.dialMusic && this.epOther && this.epOther.connected) {
|
||||
this.epOther.play(this.dialMusic).catch((err) => {});
|
||||
}
|
||||
}
|
||||
this._installDtmfDetection(cs, this.epOther, this.parentDtmfCollector);
|
||||
await this._attemptCalls(cs);
|
||||
@@ -206,19 +209,21 @@ class TaskDial extends Task {
|
||||
_removeDtmfDetection(cs, ep) {
|
||||
if (ep) {
|
||||
delete ep.dtmfDetector;
|
||||
ep.removeListener('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
ep.removeAllListeners('dtmf');
|
||||
}
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
const match = ep.dtmfDetector.keyPress(evt.dtmf);
|
||||
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
|
||||
cs.requestor :
|
||||
this.sd.requestor;
|
||||
if (match) {
|
||||
this.logger.debug(`parentCall triggered dtmf match: ${match}`);
|
||||
requestor.request(this.dtmfHook, Object.assign({dtmf: match}, cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
if (ep.dtmfDetector) {
|
||||
const match = ep.dtmfDetector.keyPress(evt.dtmf);
|
||||
const requestor = ep.dtmfDetector === this.parentDtmfCollector ?
|
||||
cs.requestor :
|
||||
this.sd.requestor;
|
||||
if (match) {
|
||||
this.logger.debug(`parentCall triggered dtmf match: ${match}`);
|
||||
requestor.request(this.dtmfHook, Object.assign({dtmf: match}, cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'Dial:_onDtmf - error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,18 +245,6 @@ class TaskDial extends Task {
|
||||
const {getSBC} = srf.locals;
|
||||
const sbcAddress = getSBC();
|
||||
|
||||
/*
|
||||
if (CallDirection.Inbound === cs.direction) {
|
||||
const contact = req.getParsedHeader('Contact');
|
||||
const uri = parseUri(contact[0].uri);
|
||||
this.logger.debug({contact}, 'outdialing with contact');
|
||||
sbcAddress = `${uri.host}:${uri.port || 5060}`;
|
||||
//sbcAddress = `${req.source_address}:${req.source_port}`;
|
||||
}
|
||||
else {
|
||||
sbcAddress = getSBC();
|
||||
}
|
||||
*/
|
||||
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
||||
const opts = {
|
||||
headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers,
|
||||
|
||||
@@ -24,9 +24,9 @@ class TaskRestDial extends Task {
|
||||
/**
|
||||
* INVITE has just been sent at this point
|
||||
*/
|
||||
async exec(cs, req) {
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
this.req = req;
|
||||
this.req = cs.req;
|
||||
|
||||
this._setCallTimer();
|
||||
await this.awaitTaskDone();
|
||||
|
||||
@@ -53,8 +53,9 @@ function installSrfLocals(srf, logger) {
|
||||
// retry to connect to any that were initially offline
|
||||
setInterval(async() => {
|
||||
for (const val of mediaservers) {
|
||||
if (val.connect === 0) {
|
||||
if (val.connects === 0) {
|
||||
try {
|
||||
logger.info({mediaserver: val.opts}, 'Retrying initial connection to media server');
|
||||
const ms = await mrf.connect(val.opts);
|
||||
val.ms = ms;
|
||||
} catch (err) {
|
||||
@@ -66,13 +67,15 @@ function installSrfLocals(srf, logger) {
|
||||
|
||||
// if we have a single freeswitch (as is typical) report stats periodically
|
||||
if (mediaservers.length === 1) {
|
||||
const ms = mediaservers[0].ms;
|
||||
setInterval(() => {
|
||||
try {
|
||||
stats.gauge('fs.media.channels.in_use', ms.currentSessions);
|
||||
stats.gauge('fs.media.channels.free', ms.maxSessions - ms.currentSessions);
|
||||
stats.gauge('fs.media.calls_per_second', ms.cps);
|
||||
stats.gauge('fs.media.cpu_idle', ms.cpuIdle);
|
||||
if (mediaservers[0].ms && mediaservers[0].active) {
|
||||
const ms = mediaservers[0].ms;
|
||||
stats.gauge('fs.media.channels.in_use', ms.currentSessions);
|
||||
stats.gauge('fs.media.channels.free', ms.maxSessions - ms.currentSessions);
|
||||
stats.gauge('fs.media.calls_per_second', ms.cps);
|
||||
stats.gauge('fs.media.cpu_idle', ms.cpuIdle);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.info(err, 'Error sending media server metrics');
|
||||
|
||||
Reference in New Issue
Block a user