diff --git a/lib/call-session.js b/lib/call-session.js index 7a82c5e..56622e7 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -69,6 +69,8 @@ class CallSession extends Emitter { this.performLcr = performLcr; this.lookupCarrierBySid = lookupCarrierBySid; this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier; + + this._mediaReleased = false; } get account_sid() { @@ -79,6 +81,10 @@ class CallSession extends Emitter { return this.srf.locals.privateSipAddress; } + get isMediaReleased() { + return this._mediaReleased; + } + async connect() { const teams = this.teams = this.req.locals.target === 'teams'; const engine = this.srf.locals.getRtpEngine(); @@ -95,6 +101,7 @@ class CallSession extends Emitter { unblockMedia, blockDTMF, unblockDTMF, + playDTMF, subscribeDTMF, unsubscribeDTMF, subscribeRequest, @@ -109,6 +116,7 @@ class CallSession extends Emitter { this.unblockMedia = unblockMedia; this.blockDTMF = blockDTMF; this.unblockDTMF = unblockDTMF; + this.playDTMF = playDTMF; this.subscribeDTMF = subscribeDTMF; this.unsubscribeDTMF = unsubscribeDTMF; this.subscribeRequest = subscribeRequest; @@ -560,6 +568,7 @@ Duration=${payload.duration} ` this.logger.info(`got a reinvite from FS to ${reason}`); sdp = dlg.other.remote.sdp; answerMedia.flags = ['asymmetric', 'port latching']; + this._mediaReleased = 'release-media' === reason; } else { sdp = await dlg.other.modify(response.sdp); @@ -586,6 +595,7 @@ Duration=${payload.duration} ` async _onInfo(dlg, req, res) { try { + const contentType = req.get('Content-Type'); if (dlg.type === 'uas' && req.has('X-Reason')) { const toTag = this.rtpEngineOpts.uac.tag; const reason = req.get('X-Reason'); @@ -688,6 +698,40 @@ Duration=${payload.duration} ` res.send(succeeded ? 200 : 503); } } + else if (dlg.type === 'uac' && ['application/dtmf-relay', 'application/dtmf'].includes(contentType)) { + const arr = /Signal=\s*([1-9#*])/.exec(req.body); + if (!arr) { + this.logger.info({body: req.body}, '_onInfo: invalid INFO dtmf request'); + throw new Error(`_onInfo: no dtmf in body for ${contentType}`); + } + const code = arr[1]; + const arr2 = /Duration=\s*(\d+)/.exec(req.body); + const duration = arr2 ? arr2[1] : 250; + + if (this.isMediaReleased) { + /* just relay on to the feature server */ + this.logger.info({code, duration}, 'got SIP INFO DTMF from caller, relaying to feature server'); + this._onDTMF(dlg.other, {event: code, duration}) + .catch((err) => this.logger.info({err}, 'Error relaying DTMF to feature server')); + res.send(200); + } + else { + /* else convert SIP INFO to RFC 2833 telephony events */ + this.logger.info({code, duration}, 'got SIP INFO DTMF from caller, converting to RFC 2833'); + const opts = { + ...this.rtpEngineOpts.common, + 'from-tag': this.rtpEngineOpts.uac.tag, + code, + duration + }; + const response = await this.playDTMF(opts); + if ('ok' !== response.result) { + this.logger.info({response}, `rtpengine playDTMF failed with ${JSON.stringify(response)}`); + throw new Error('rtpengine failed: answer'); + } + res.send(200); + } + } else { const response = await dlg.other.request({ method: 'INFO', diff --git a/lib/utils.js b/lib/utils.js index 209e97b..e42dbb4 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,6 +7,12 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) { const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default']; const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics; const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCharacteristics; + + /* webrtc clients (e.g. sipjs) send DMTF via SIP INFO */ + if ((srcIsUsingSrtp || dstIsUsingSrtp) && !teams) { + dstOpts.flags.push('inject DTMF'); + srcOpts.flags.push('inject DTMF'); + } const common = { 'call-id': req.get('Call-ID'), 'replace': ['origin', 'session-connection']