mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-22 01:27:55 +00:00
implement release-media and anchor-media operations
This commit is contained in:
@@ -167,7 +167,7 @@ module.exports = function(srf, logger) {
|
|||||||
if (0 === app.tasks.length) throw new Error('no application provided');
|
if (0 === app.tasks.length) throw new Error('no application provided');
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info(`Error retrieving or parsing application: ${err.message}`);
|
logger.info({err}, `Error retrieving or parsing application: ${err.message}`);
|
||||||
res.send(480, {headers: {'X-Reason': err.message}});
|
res.send(480, {headers: {'X-Reason': err.message}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ const CallSession = require('./call-session');
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
class AdultingCallSession extends CallSession {
|
class AdultingCallSession extends CallSession {
|
||||||
constructor({logger, application, singleDialer, tasks, callInfo}) {
|
constructor({logger, application, singleDialer, tasks, callInfo, accountInfo}) {
|
||||||
super({
|
super({
|
||||||
logger,
|
logger,
|
||||||
application,
|
application,
|
||||||
srf: singleDialer.dlg.srf,
|
srf: singleDialer.dlg.srf,
|
||||||
tasks,
|
tasks,
|
||||||
callInfo
|
callInfo,
|
||||||
|
accountInfo
|
||||||
});
|
});
|
||||||
this.sd = singleDialer;
|
this.sd = singleDialer;
|
||||||
|
|
||||||
|
|||||||
@@ -927,14 +927,25 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async releaseMediaToSBC(remoteSdp) {
|
async releaseMediaToSBC(remoteSdp) {
|
||||||
assert(this.dlg);
|
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||||
assert(this.dlg.connected);
|
await this.dlg.modify(remoteSdp, {
|
||||||
assert(this.ep);
|
headers: {
|
||||||
assert(typeof remoteSdp === 'string');
|
'X-Reason': 'release-media'
|
||||||
await this.dlg.modify(remoteSdp);
|
}
|
||||||
|
});
|
||||||
this.ep.destroy()
|
this.ep.destroy()
|
||||||
.then(() => this.ep = null)
|
.then(() => this.ep = null)
|
||||||
.catch((err) => this.logger.error({err}, 'releaseMediaToSBC: Error destroying endpoint'));
|
.catch((err) => this.logger.error({err}, 'CallSession:releaseMediaToSBC: Error destroying endpoint'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async reAnchorMedia() {
|
||||||
|
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||||
|
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||||
|
await this.dlg.modify(this.ep.local.sdp, {
|
||||||
|
headers: {
|
||||||
|
'X-Reason': 'anchor-media'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ class TaskDial extends Task {
|
|||||||
|
|
||||||
get name() { return TaskName.Dial; }
|
get name() { return TaskName.Dial; }
|
||||||
|
|
||||||
get canReleaseMedia() { return !this.dtmfHook && !this.listenTask && !this.transcribeTask; }
|
get canReleaseMedia() {
|
||||||
|
return !process.env.ANCHOR_MEDIA_ALWAYS && !this.dtmfHook && !this.listenTask && !this.transcribeTask;
|
||||||
|
}
|
||||||
|
|
||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
@@ -179,9 +181,14 @@ class TaskDial extends Task {
|
|||||||
* @param {*} tasks - array of play/say tasks to execute
|
* @param {*} tasks - array of play/say tasks to execute
|
||||||
*/
|
*/
|
||||||
async whisper(tasks, callSid) {
|
async whisper(tasks, callSid) {
|
||||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
|
||||||
try {
|
try {
|
||||||
const cs = this.callSession;
|
const cs = this.callSession;
|
||||||
|
if (!this.ep && !this.epOther) {
|
||||||
|
await this.reAnchorMedia(this.callSession, this.sd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.epOther || !this.ep) return this.logger.info('Dial:whisper: no paired endpoint found');
|
||||||
|
|
||||||
this.logger.debug('Dial:whisper unbridging endpoints');
|
this.logger.debug('Dial:whisper unbridging endpoints');
|
||||||
await this.epOther.unbridge();
|
await this.epOther.unbridge();
|
||||||
this.logger.debug('Dial:whisper executing tasks');
|
this.logger.debug('Dial:whisper executing tasks');
|
||||||
@@ -190,7 +197,12 @@ class TaskDial extends Task {
|
|||||||
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
await task.exec(cs, callSid === this.callSid ? this.ep : this.epOther);
|
||||||
}
|
}
|
||||||
this.logger.debug('Dial:whisper tasks complete');
|
this.logger.debug('Dial:whisper tasks complete');
|
||||||
if (!cs.callGone && this.epOther) this.epOther.bridge(this.ep);
|
if (!cs.callGone && this.epOther) {
|
||||||
|
|
||||||
|
/* if we can release the media back to the SBC, do so now */
|
||||||
|
if (this.canReleaseMedia) this._releaseMedia(cs, this.sd);
|
||||||
|
else this.epOther.bridge(this.ep);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err, 'Dial:whisper error');
|
this.logger.error(err, 'Dial:whisper error');
|
||||||
}
|
}
|
||||||
@@ -200,12 +212,20 @@ class TaskDial extends Task {
|
|||||||
* mute or unmute one side of the call
|
* mute or unmute one side of the call
|
||||||
*/
|
*/
|
||||||
async mute(callSid, doMute) {
|
async mute(callSid, doMute) {
|
||||||
|
if (doMute && !this.epOther && !this.ep) {
|
||||||
|
this.logger.info('Dial:mute re-anchoring media so we can mute');
|
||||||
|
await this.reAnchorMedia(this.callSession, this.sd);
|
||||||
|
this.epOther.bridge(this.ep);
|
||||||
|
}
|
||||||
if (!this.epOther || !this.ep) return this.logger.info('Dial:mute: no paired endpoint found');
|
if (!this.epOther || !this.ep) return this.logger.info('Dial:mute: no paired endpoint found');
|
||||||
try {
|
try {
|
||||||
const parentCall = callSid !== this.callSid;
|
const parentCall = callSid !== this.callSid;
|
||||||
const ep = parentCall ? this.epOther : this.ep;
|
const ep = parentCall ? this.epOther : this.ep;
|
||||||
await ep[doMute ? 'mute' : 'unmute']();
|
await ep[doMute ? 'mute' : 'unmute']();
|
||||||
this.logger.debug(`Dial:mute ${doMute ? 'muted' : 'unmuted'} ${parentCall ? 'parentCall' : 'childCall'}`);
|
this.logger.debug(`Dial:mute ${doMute ? 'muted' : 'unmuted'} ${parentCall ? 'parentCall' : 'childCall'}`);
|
||||||
|
|
||||||
|
/* release media once mute is over, if we can */
|
||||||
|
if (!doMute && this.canReleaseMedia) this._releaseMedia(this.callSession, this.sd);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err, 'Dial:mute error');
|
this.logger.error(err, 'Dial:mute error');
|
||||||
}
|
}
|
||||||
@@ -325,7 +345,8 @@ class TaskDial extends Task {
|
|||||||
sbcAddress,
|
sbcAddress,
|
||||||
target: t,
|
target: t,
|
||||||
opts,
|
opts,
|
||||||
callInfo: cs.callInfo
|
callInfo: cs.callInfo,
|
||||||
|
accountInfo: cs.accountInfo
|
||||||
});
|
});
|
||||||
this.dials.set(sd.callSid, sd);
|
this.dials.set(sd.callSid, sd);
|
||||||
|
|
||||||
@@ -369,9 +390,13 @@ class TaskDial extends Task {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('accept', () => {
|
.on('accept', async() => {
|
||||||
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
this.logger.debug(`Dial:_attemptCalls - we have a winner: ${sd.callSid}`);
|
||||||
this._connectSingleDial(cs, sd);
|
try {
|
||||||
|
await this._connectSingleDial(cs, sd);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial ');
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.on('decline', () => {
|
.on('decline', () => {
|
||||||
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
this.logger.debug(`Dial:_attemptCalls - declined: ${sd.callSid}`);
|
||||||
@@ -397,7 +422,7 @@ class TaskDial extends Task {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_connectSingleDial(cs, sd) {
|
async _connectSingleDial(cs, sd) {
|
||||||
if (!this.bridged && !this.canReleaseMedia) {
|
if (!this.bridged && !this.canReleaseMedia) {
|
||||||
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
|
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
|
||||||
if (this.epOther) {
|
if (this.epOther) {
|
||||||
@@ -408,7 +433,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ding! ding! ding! we have a winner
|
// ding! ding! ding! we have a winner
|
||||||
this._selectSingleDial(cs, sd);
|
await this._selectSingleDial(cs, sd);
|
||||||
this._killOutdials(); // NB: order is important
|
this._killOutdials(); // NB: order is important
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +446,7 @@ class TaskDial extends Task {
|
|||||||
* - launch any nested tasks
|
* - launch any nested tasks
|
||||||
* - and establish a handler to clean up if the called party hangs up
|
* - and establish a handler to clean up if the called party hangs up
|
||||||
*/
|
*/
|
||||||
_selectSingleDial(cs, sd) {
|
async _selectSingleDial(cs, sd) {
|
||||||
debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`);
|
debug(`Dial:_selectSingleDial ep for outbound call: ${sd.ep.uuid}`);
|
||||||
this.dials.delete(sd.callSid);
|
this.dials.delete(sd.callSid);
|
||||||
|
|
||||||
@@ -429,7 +454,7 @@ class TaskDial extends Task {
|
|||||||
this.callSid = sd.callSid;
|
this.callSid = sd.callSid;
|
||||||
if (this.earlyMedia) {
|
if (this.earlyMedia) {
|
||||||
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
debug('Dial:_selectSingleDial propagating answer supervision on A leg now that B is connected');
|
||||||
cs.propagateAnswer();
|
await cs.propagateAnswer();
|
||||||
}
|
}
|
||||||
if (this.timeLimit) {
|
if (this.timeLimit) {
|
||||||
this.timerMaxCallDuration = setTimeout(() => {
|
this.timerMaxCallDuration = setTimeout(() => {
|
||||||
@@ -445,7 +470,7 @@ class TaskDial extends Task {
|
|||||||
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
||||||
sessionTracker.remove(this.callSid);
|
sessionTracker.remove(this.callSid);
|
||||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||||
this.ep.unbridge();
|
this.ep && this.ep.unbridge();
|
||||||
this.kill(cs);
|
this.kill(cs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -483,15 +508,24 @@ class TaskDial extends Task {
|
|||||||
assert(cs.ep && sd.ep);
|
assert(cs.ep && sd.ep);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.logger.info('Dial:_releaseMedia - releasing media from freewitch since we can');
|
this.logger.info('Dial:_releaseMedia - releasing media from freewitch');
|
||||||
const aLegSdp = cs.ep.remote.sdp;
|
const aLegSdp = cs.ep.remote.sdp;
|
||||||
const bLegSdp = sd.ep.remote.sdp;
|
const bLegSdp = sd.ep.remote.sdp;
|
||||||
await Promise.all[sd.releaseMediaToSBC(aLegSdp), cs.releaseMediaToSBC(bLegSdp)];
|
await Promise.all[sd.releaseMediaToSBC(aLegSdp), cs.releaseMediaToSBC(bLegSdp)];
|
||||||
this.epOther = null;
|
this.epOther = null;
|
||||||
|
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'Dial:_releaseMedia error');
|
this.logger.info({err}, 'Dial:_releaseMedia error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reAnchorMedia(cs, sd) {
|
||||||
|
if (cs.ep && sd.ep) return;
|
||||||
|
|
||||||
|
this.logger.info('Dial:reAnchorMedia - re-anchoring media to freewitch');
|
||||||
|
await Promise.all([sd.reAnchorMedia(), cs.reAnchorMedia()]);
|
||||||
|
this.epOther = cs.ep;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TaskDial;
|
module.exports = TaskDial;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const moment = require('moment');
|
|||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
class SingleDialer extends Emitter {
|
class SingleDialer extends Emitter {
|
||||||
constructor({logger, sbcAddress, target, opts, application, callInfo}) {
|
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||||
super();
|
super();
|
||||||
assert(target.type);
|
assert(target.type);
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ class SingleDialer extends Emitter {
|
|||||||
this.bindings = logger.bindings();
|
this.bindings = logger.bindings();
|
||||||
|
|
||||||
this.parentCallInfo = callInfo;
|
this.parentCallInfo = callInfo;
|
||||||
|
this.accountInfo = accountInfo;
|
||||||
|
|
||||||
this.callGone = false;
|
this.callGone = false;
|
||||||
|
|
||||||
this.callSid = uuidv4();
|
this.callSid = uuidv4();
|
||||||
@@ -202,7 +204,7 @@ class SingleDialer extends Emitter {
|
|||||||
const duration = moment().diff(connectTime, 'seconds');
|
const duration = moment().diff(connectTime, 'seconds');
|
||||||
this.logger.debug('SingleDialer:exec called party hung up');
|
this.logger.debug('SingleDialer:exec called party hung up');
|
||||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||||
this.ep.destroy();
|
this.ep && this.ep.destroy();
|
||||||
})
|
})
|
||||||
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
|
.on('refresh', () => this.logger.info('SingleDialer:exec - dialog refreshed by uas'))
|
||||||
.on('modify', async(req, res) => {
|
.on('modify', async(req, res) => {
|
||||||
@@ -300,14 +302,20 @@ class SingleDialer extends Emitter {
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.adulting = true;
|
this.adulting = true;
|
||||||
this.emit('adulting');
|
this.emit('adulting');
|
||||||
|
if (this.ep) {
|
||||||
await this.ep.unbridge()
|
await this.ep.unbridge()
|
||||||
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
|
.catch((err) => this.logger.info({err}, 'SingleDialer:doAdulting - failed to unbridge ep'));
|
||||||
this.ep.play('silence_stream://1000');
|
this.ep.play('silence_stream://1000');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await this.reAnchorMedia();
|
||||||
|
}
|
||||||
const cs = new AdultingCallSession({
|
const cs = new AdultingCallSession({
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
singleDialer: this,
|
singleDialer: this,
|
||||||
application,
|
application,
|
||||||
callInfo: this.callInfo,
|
callInfo: this.callInfo,
|
||||||
|
accountInfo: this.accountInfo,
|
||||||
tasks
|
tasks
|
||||||
});
|
});
|
||||||
cs.exec();
|
cs.exec();
|
||||||
@@ -315,14 +323,25 @@ class SingleDialer extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async releaseMediaToSBC(remoteSdp) {
|
async releaseMediaToSBC(remoteSdp) {
|
||||||
assert(this.dlg);
|
assert(this.dlg && this.dlg.connected && this.ep && typeof remoteSdp === 'string');
|
||||||
assert(this.dlg.connected);
|
await this.dlg.modify(remoteSdp, {
|
||||||
assert(this.ep);
|
headers: {
|
||||||
assert(typeof remoteSdp === 'string');
|
'X-Reason': 'release-media'
|
||||||
await this.dlg.modify(remoteSdp);
|
}
|
||||||
|
});
|
||||||
this.ep.destroy()
|
this.ep.destroy()
|
||||||
.then(() => this.ep = null)
|
.then(() => this.ep = null)
|
||||||
.catch((err) => this.logger.error({err}, 'releaseMediaToSBC: Error destroying endpoint'));
|
.catch((err) => this.logger.error({err}, 'SingleDialer:releaseMediaToSBC: Error destroying endpoint'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async reAnchorMedia() {
|
||||||
|
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||||
|
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||||
|
await this.dlg.modify(this.ep.local.sdp, {
|
||||||
|
headers: {
|
||||||
|
'X-Reason': 'anchor-media'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
_notifyCallStatusChange({callStatus, sipStatus, duration}) {
|
||||||
@@ -347,9 +366,9 @@ class SingleDialer extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo}) {
|
function placeOutdial({logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo}) {
|
||||||
const myOpts = deepcopy(opts);
|
const myOpts = deepcopy(opts);
|
||||||
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo});
|
const sd = new SingleDialer({logger, sbcAddress, target, myOpts, application, callInfo, accountInfo});
|
||||||
sd.exec(srf, ms, myOpts);
|
sd.exec(srf, ms, myOpts);
|
||||||
return sd;
|
return sd;
|
||||||
}
|
}
|
||||||
|
|||||||
4803
package-lock.json
generated
4803
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@
|
|||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"drachtio-fsmrf": "^2.0.7",
|
"drachtio-fsmrf": "^2.0.7",
|
||||||
"drachtio-srf": "^4.4.50",
|
"drachtio-srf": "^4.4.55",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"ip": "^1.1.5",
|
"ip": "^1.1.5",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user