diff --git a/lib/session/call-session.js b/lib/session/call-session.js index e32dd4d0..1c2907ca 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -1744,7 +1744,7 @@ Duration=${duration} ` res.send(200, {body: this.ep.local.sdp}); } else { - if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHold) { + if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHoldEnabled) { this.logger.info('onholdMusic reINVITE after media has been released'); await this.currentTask.handleReinviteAfterMediaReleased(req, res); } else { diff --git a/lib/tasks/dial.js b/lib/tasks/dial.js index 96df3a9e..f9975e60 100644 --- a/lib/tasks/dial.js +++ b/lib/tasks/dial.js @@ -138,13 +138,14 @@ class TaskDial extends Task { get name() { return TaskName.Dial; } - get isOnHold() { - return this.isIncomingLegHold || this.isOutgoingLegHold; + get isOnHoldEnabled() { + return !!this.data.onHoldHook; } get canReleaseMedia() { const keepAnchor = this.data.anchorMedia || this.cs.isBackGroundListen || + this.cs.onHoldMusic || ANCHOR_MEDIA_ALWAYS || this.listenTask || this.transcribeTask || @@ -570,7 +571,8 @@ class TaskDial extends Task { accountInfo: cs.accountInfo, rootSpan: cs.rootSpan, startSpan: this.startSpan.bind(this), - dialTask: this + dialTask: this, + onHoldMusic: this.cs.onHoldMusic }); this.dials.set(sd.callSid, sd); @@ -679,22 +681,43 @@ class TaskDial extends Task { 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; + if (this.isOnHoldEnabled) { + if (isOnhold(req.body)) { + this.logger.debug('Dial: _onReinvite receive hold Request'); + if (!this.epOther && !this.ep) { + this.logger.debug(`Dial: _onReinvite receive hold Request, + media already released, reconnect media server`); + // update caller leg for new SDP from callee. + await this.cs.handleReinviteAfterMediaReleased(req, res); + // Freeswitch media is released, reconnect + await this.reAnchorMedia(this.cs, this.sd); + this.isOutgoingLegHold = true; + } else { + this.logger.debug('Dial: _onReinvite receive hold Request, update SDP'); + const newSdp = await this.ep.modify(req.body); + res.send(200, {body: newSdp}); + } 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); + // Media already connected, ask for onHoldHook + this._onHoldHook(req); + } else if (!isOnhold(req.body)) { + this.logger.debug('Dial: _onReinvite receive unhold Request'); + if (this.epOther && this.ep && this.isOutgoingLegHold && this.canReleaseMedia) { + this.logger.debug('Dial: _onReinvite receive unhold Request, release media'); + // 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); + this.isOutgoingLegHold = false; + } else { + this.logger.debug('Dial: _onReinvite receive unhold Request, update media server'); + const newSdp = await this.ep.modify(req.body); + res.send(200, {body: newSdp}); + } + if (this._onHoldSession) { + this._onHoldSession.kill(); + } isHandled = true; - this.isOutgoingLegHold = false; } } if (!isHandled) { @@ -811,23 +834,34 @@ class TaskDial extends Task { this.epOther = cs.ep; } + // Handle RE-INVITE hold from caller leg. async handleReinviteAfterMediaReleased(req, res) { 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 (this.isOnHoldEnabled) { + if (isOnhold(req.body)) { + if (!this.epOther && !this.ep) { + // update callee leg for new SDP from caller. + const sdp = await this.dlg.modify(req.body); + res.send(200, {body: sdp}); + // Onhold but media is already released, reconnect + await this.reAnchorMedia(this.cs, this.sd); + isHandled = true; + this.isIncomingLegHold = true; + } + this._onHoldHook(req); + } else if (!isOnhold(req.body)) { + if (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 (this._onHoldSession) { + this._onHoldSession.kill(); + } + } } if (!isHandled) { @@ -846,7 +880,7 @@ class TaskDial extends Task { }); } - async _onHoldHook(allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) { + async _onHoldHook(req, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) { if (this.data.onHoldHook) { // send silence for keep Voice quality await this.epOther.play('silence_stream://500'); @@ -856,7 +890,13 @@ class TaskDial extends Task { 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); + request('verb:hook', this.data.onHoldHook, { + ...this.cs.callInfo.toJSON(), + hold_detail: { + from: req.get('From'), + to: req.get('To') + } + }, 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) { @@ -865,7 +905,7 @@ class TaskDial extends Task { } this.logger.debug(`DialTask:_onHoldHook: executing ${tasks.length} tasks`); if (tasks.length) { - this._playSession = new ConfirmCallSession({ + this._onHoldSession = new ConfirmCallSession({ logger: this.logger, application: this.cs.application, dlg: this.isIncomingLegHold ? this.dlg : this.cs.dlg, @@ -875,12 +915,12 @@ class TaskDial extends Task { tasks, rootSpan: this.cs.rootSpan }); - await this._playSession.exec(); - this._playSession = null; + await this._onHoldSession.exec(); + this._onHoldSession = null; } } catch (error) { this.logger.info(error, 'DialTask:_onHoldHook: failed retrieving waitHook'); - this._playSession = null; + this._onHoldSession = null; break; } } while (allowedTasks && allowedTasks.length > 0 && !this.killed && this.isOnHold); diff --git a/lib/utils/place-outdial.js b/lib/utils/place-outdial.js index 823704e5..c45e281c 100644 --- a/lib/utils/place-outdial.js +++ b/lib/utils/place-outdial.js @@ -18,7 +18,8 @@ const WsRequestor = require('./ws-requestor'); const {makeOpusFirst} = require('./sdp-utils'); class SingleDialer extends Emitter { - constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) { + constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask, + onHoldMusic}) { super(); assert(target.type); @@ -41,6 +42,7 @@ class SingleDialer extends Emitter { this.callSid = uuidv4(); this.dialTask = dialTask; + this.onHoldMusic = onHoldMusic; this.on('callStatusChange', this._notifyCallStatusChange.bind(this)); } @@ -131,6 +133,7 @@ class SingleDialer extends Emitter { this.serviceUrl = srf.locals.serviceUrl; this.ep = await ms.createEndpoint(); + this._configMsEndpoint(); this.logger.debug(`SingleDialer:exec - created endpoint ${this.ep.uuid}`); /** @@ -253,7 +256,7 @@ class SingleDialer extends Emitter { .on('modify', async(req, res) => { try { if (this.ep) { - if (this.dialTask && this.dialTask.isOnHold) { + if (this.dialTask && this.dialTask.isOnHoldEnabled) { this.logger.info('dial is onhold, emit event'); this.emit('reinvite', req, res); } else { @@ -320,6 +323,12 @@ class SingleDialer extends Emitter { } } + _configMsEndpoint() { + if (this.onHoldMusic) { + this.ep.set({hold_music: `shout://${this.onHoldMusic.replace(/^https?:\/\//, '')}`}); + } + } + /** * Run an application on the call after answer, e.g. call screening. * Once the application completes in some fashion, emit an 'accepted' event @@ -436,6 +445,7 @@ class SingleDialer extends Emitter { async reAnchorMedia() { assert(this.dlg && this.dlg.connected && !this.ep); this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp}); + this._configMsEndpoint(); await this.dlg.modify(this.ep.local.sdp, { headers: { 'X-Reason': 'anchor-media' @@ -466,11 +476,12 @@ class SingleDialer extends Emitter { } function placeOutdial({ - logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask + logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask, + onHoldMusic }) { const myOpts = deepcopy(opts); const sd = new SingleDialer({ - logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask + logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask, onHoldMusic }); sd.exec(srf, ms, myOpts); return sd;