mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 16:50:39 +00:00
feat moh (#423)
* feat moh * feat moh * fix typo * fix typo * fix * fix * wip * wip * wip * wip * wip * wip * wip * wip * wip * git commit -a -m wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix * fix * fix * fix * wip * rebase * fix * fix
This commit is contained in:
@@ -357,6 +357,14 @@ class CallSession extends Emitter {
|
||||
return this._globalSttPunctuation;
|
||||
}
|
||||
|
||||
get onHoldMusic() {
|
||||
return this._onHoldMusic;
|
||||
}
|
||||
|
||||
set onHoldMusic(url) {
|
||||
this._onHoldMusic = url;
|
||||
}
|
||||
|
||||
hasGlobalSttPunctuation() {
|
||||
return this._globalSttPunctuation !== undefined;
|
||||
}
|
||||
@@ -1392,6 +1400,8 @@ class CallSession extends Emitter {
|
||||
this.ep = ep;
|
||||
this.logger.debug(`allocated endpoint ${ep.uuid}`);
|
||||
|
||||
this._configMsEndpoint();
|
||||
|
||||
this.ep.on('destroy', () => {
|
||||
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
|
||||
});
|
||||
@@ -1462,6 +1472,7 @@ class CallSession extends Emitter {
|
||||
return;
|
||||
}
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
this._configMsEndpoint();
|
||||
|
||||
await this.dlg.modify(this.ep.local.sdp);
|
||||
this.logger.debug('CallSession:replaceEndpoint completed');
|
||||
@@ -1555,9 +1566,14 @@ class CallSession extends Emitter {
|
||||
res.send(200, {body: this.ep.local.sdp});
|
||||
}
|
||||
else {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHold) {
|
||||
this.logger.info('onholdMusic reINVITE after media has been released');
|
||||
await this.currentTask.handleReinviteAfterMediaReleased(req, res);
|
||||
} else {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.currentTask && this.currentTask.name === TaskName.Dial) {
|
||||
@@ -1604,6 +1620,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
if (!this.ep) {
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
this._configMsEndpoint();
|
||||
}
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
@@ -1758,6 +1775,7 @@ class CallSession extends Emitter {
|
||||
'X-Reason': 'anchor-media'
|
||||
}
|
||||
});
|
||||
this._configMsEndpoint();
|
||||
}
|
||||
|
||||
async handleReinviteAfterMediaReleased(req, res) {
|
||||
@@ -1839,6 +1857,12 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
_configMsEndpoint() {
|
||||
if (this.onHoldMusic) {
|
||||
this.ep.set({hold_music: `shout://${this.onHoldMusic.replace(/^https?:\/\//, '')}`});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* notifyTaskError - only used when websocket connection is used instead of webhooks
|
||||
*/
|
||||
|
||||
@@ -40,6 +40,8 @@ class TaskConfig extends Task {
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
|
||||
TaskPreconditions.Endpoint :
|
||||
TaskPreconditions.None;
|
||||
|
||||
this.onHoldMusic = this.data.onHoldMusic;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Config; }
|
||||
@@ -72,6 +74,7 @@ class TaskConfig extends Task {
|
||||
}
|
||||
if (this.data.amd) phrase.push('enable amd');
|
||||
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
|
||||
return `${this.name}{${phrase.join(',')}}`;
|
||||
}
|
||||
|
||||
@@ -83,6 +86,10 @@ class TaskConfig extends Task {
|
||||
cs.notifyEvents = !!this.data.notifyEvents;
|
||||
}
|
||||
|
||||
if (this.onHoldMusic) {
|
||||
cs.onHoldMusic = this.onHoldMusic;
|
||||
}
|
||||
|
||||
if (this.data.amd) {
|
||||
this.startAmd = cs.startAmd;
|
||||
this.stopAmd = cs.stopAmd;
|
||||
|
||||
@@ -12,10 +12,13 @@ const assert = require('assert');
|
||||
const placeCall = require('../utils/place-outdial');
|
||||
const sessionTracker = require('../session/session-tracker');
|
||||
const DtmfCollector = require('../utils/dtmf-collector');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const dbUtils = require('../utils/db-utils');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
const {ANCHOR_MEDIA_ALWAYS} = require('../config');
|
||||
const { isOnhold } = require('../utils/sdp-utils');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
function parseDtmfOptions(logger, dtmfCapture) {
|
||||
let parentDtmfCollector, childDtmfCollector;
|
||||
@@ -135,6 +138,10 @@ class TaskDial extends Task {
|
||||
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
get isOnHold() {
|
||||
return this.isIncomingLegHold || this.isOutgoingLegHold;
|
||||
}
|
||||
|
||||
get canReleaseMedia() {
|
||||
const keepAnchor = this.data.anchorMedia ||
|
||||
this.cs.isBackGroundListen ||
|
||||
@@ -507,7 +514,8 @@ class TaskDial extends Task {
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
rootSpan: cs.rootSpan,
|
||||
startSpan: this.startSpan.bind(this)
|
||||
startSpan: this.startSpan.bind(this),
|
||||
dialTask: this
|
||||
});
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
@@ -576,11 +584,7 @@ class TaskDial extends Task {
|
||||
}
|
||||
})
|
||||
.on('reinvite', (req, res) => {
|
||||
try {
|
||||
cs.handleReinviteAfterMediaReleased(req, res);
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error in dial einvite from B leg');
|
||||
}
|
||||
this._onReinvite(req, res);
|
||||
})
|
||||
.on('refer', (callInfo, req, res) => {
|
||||
|
||||
@@ -616,6 +620,35 @@ class TaskDial extends Task {
|
||||
this._killOutdials(); // NB: order is important
|
||||
}
|
||||
|
||||
async _onReinvite(req, res) {
|
||||
try {
|
||||
let isHandled = false;
|
||||
if (this.cs.onHoldMusic) {
|
||||
if (isOnhold(req.body) && !this.epOther && !this.ep) {
|
||||
await this.cs.handleReinviteAfterMediaReleased(req, res);
|
||||
// Onhold but media is already released
|
||||
// reconnect A Leg and Response B leg
|
||||
await this.reAnchorMedia(this.cs, this.sd);
|
||||
this.isOutgoingLegHold = true;
|
||||
isHandled = true;
|
||||
this._onHoldHook();
|
||||
} else if (!isOnhold(req.body) && this.epOther && this.ep && this.isOutgoingLegHold && this.canReleaseMedia) {
|
||||
// Offhold, time to release media
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
await res.send(200, {body: newSdp});
|
||||
await this._releaseMedia(this.cs, this.sd);
|
||||
isHandled = true;
|
||||
this.isOutgoingLegHold = false;
|
||||
}
|
||||
}
|
||||
if (!isHandled) {
|
||||
this.cs.handleReinviteAfterMediaReleased(req, res);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error in dial einvite from B leg');
|
||||
}
|
||||
}
|
||||
|
||||
_onMaxCallDuration(cs) {
|
||||
this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
|
||||
this.ep && this.ep.unbridge();
|
||||
@@ -720,9 +753,29 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
async handleReinviteAfterMediaReleased(req, res) {
|
||||
const sdp = await this.dlg.modify(req.body);
|
||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
||||
res.send(200, {body: sdp});
|
||||
let isHandled = false;
|
||||
if (isOnhold(req.body) && !this.epOther && !this.ep) {
|
||||
const sdp = await this.dlg.modify(req.body);
|
||||
res.send(200, {body: sdp});
|
||||
// Onhold but media is already released
|
||||
await this.reAnchorMedia(this.cs, this.sd);
|
||||
isHandled = true;
|
||||
this.isIncomingLegHold = true;
|
||||
this._onHoldHook();
|
||||
} else if (!isOnhold(req.body) && this.epOther && this.ep && this.isIncomingLegHold && this.canReleaseMedia) {
|
||||
// Offhold, time to release media
|
||||
const newSdp = await this.epOther.modify(req.body);
|
||||
await res.send(200, {body: newSdp});
|
||||
await this._releaseMedia(this.cs, this.sd);
|
||||
isHandled = true;
|
||||
this.isIncomingLegHold = false;
|
||||
}
|
||||
|
||||
if (!isHandled) {
|
||||
const sdp = await this.dlg.modify(req.body);
|
||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
||||
res.send(200, {body: sdp});
|
||||
}
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
@@ -733,6 +786,48 @@ class TaskDial extends Task {
|
||||
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
|
||||
});
|
||||
}
|
||||
|
||||
async _onHoldHook(allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||
if (this.data.onHoldHook) {
|
||||
// send silence for keep Voice quality
|
||||
await this.epOther.play('silence_stream://500');
|
||||
let allowedTasks;
|
||||
do {
|
||||
try {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await this.cs.application.requestor.
|
||||
request('verb:hook', this.data.onHoldHook, this.cs.callInfo.toJSON(), httpHeaders);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
if (tasks.length !== allowedTasks.length) {
|
||||
this.logger.debug({tasks, allowedTasks}, 'unsupported task');
|
||||
throw new Error(`unsupported verb in enqueue waitHook: only ${JSON.stringify(allowed)}`);
|
||||
}
|
||||
this.logger.debug(`DialTask:_onHoldHook: executing ${tasks.length} tasks`);
|
||||
if (tasks.length) {
|
||||
this._playSession = new ConfirmCallSession({
|
||||
logger: this.logger,
|
||||
application: this.cs.application,
|
||||
dlg: this.isIncomingLegHold ? this.dlg : this.cs.dlg,
|
||||
ep: this.isIncomingLegHold ? this.ep : this.cs.ep,
|
||||
callInfo: this.cs.callInfo,
|
||||
accountInfo: this.cs.accountInfo,
|
||||
tasks,
|
||||
rootSpan: this.cs.rootSpan
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.info(error, 'DialTask:_onHoldHook: failed retrieving waitHook');
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (allowedTasks && allowedTasks.length > 0 && !this.killed && this.isOnHold);
|
||||
this.logger.info('Finish onHoldHook');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskDial;
|
||||
|
||||
@@ -15,7 +15,7 @@ const RootSpan = require('./call-tracer');
|
||||
const uuidv4 = require('uuid-random');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) {
|
||||
super();
|
||||
assert(target.type);
|
||||
|
||||
@@ -37,6 +37,7 @@ class SingleDialer extends Emitter {
|
||||
this.callGone = false;
|
||||
|
||||
this.callSid = uuidv4();
|
||||
this.dialTask = dialTask;
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
}
|
||||
@@ -247,9 +248,14 @@ class SingleDialer extends Emitter {
|
||||
.on('modify', async(req, res) => {
|
||||
try {
|
||||
if (this.ep) {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||
if (this.dialTask && this.dialTask.isOnHold) {
|
||||
this.logger.info('dial is onhold, emit event');
|
||||
this.emit('reinvite', req, res);
|
||||
} else {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event');
|
||||
@@ -430,11 +436,11 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
function placeOutdial({
|
||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan
|
||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask
|
||||
}) {
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({
|
||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan
|
||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask
|
||||
});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
return sd;
|
||||
|
||||
7
lib/utils/sdp-utils.js
Normal file
7
lib/utils/sdp-utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const isOnhold = (sdp) => {
|
||||
return sdp && (sdp.includes('a=sendonly') || sdp.includes('a=inactive'));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isOnhold
|
||||
};
|
||||
Reference in New Issue
Block a user