From 738f1510667bbcd0cc36d64bc7c817c34ade7c6a Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Thu, 11 Aug 2022 14:33:01 +0200 Subject: [PATCH] Feature/sip info dtmf (#44) * initial changes for handling SIP INFO from webrtc clients * SIP INFO DTMF handling: if media has been released, just relay SIP INFO to FS otherwise transcode to RFC 2833 --- lib/call-session.js | 46 +++++++++++++++++++++++++++++++++++++++++++-- lib/utils.js | 10 ++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/call-session.js b/lib/call-session.js index 3bd8450..b5c8157 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -3,7 +3,6 @@ const SrsClient = require('@jambonz/siprec-client-utils'); const {makeRtpEngineOpts, SdpWantsSrtp, makeCallCountKey} = require('./utils'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); const {parseUri, stringifyUri, SipError} = require('drachtio-srf'); -const { v4: uuidv4 } = require('uuid'); const debug = require('debug')('jambonz:sbc-inbound'); const MS_TEAMS_USER_AGENT = 'Microsoft.PSTNHub.SIPProxy'; const MS_TEAMS_SIP_ENDPOINT = 'sip.pstnhub.microsoft.com'; @@ -20,7 +19,7 @@ const createBLegFromHeader = (req) => { }; const createSiprecBody = (headers, sdp, type, content) => { - const sep = uuidv4(); + const sep = 'uniqueBoundary'; headers['Content-Type'] = `multipart/mixed;boundary="${sep}"`; return `--${sep}\r Content-Type: application/sdp\r @@ -51,6 +50,7 @@ class CallSession extends Emitter { this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey; this.callCountKey = makeCallCountKey(req.locals.account_sid); + this._mediaReleased = false; } get isFromMSTeams() { @@ -61,6 +61,10 @@ class CallSession extends Emitter { return this.srf.locals.privateSipAddress; } + get isMediaReleased() { + return this._mediaReleased; + } + async connect() { const {sdp} = this.req.locals; this.logger.info('inbound call accepted for routing'); @@ -80,6 +84,7 @@ class CallSession extends Emitter { unblockMedia, blockDTMF, unblockDTMF, + playDTMF, subscribeDTMF, unsubscribeDTMF, subscribeRequest, @@ -93,6 +98,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; @@ -487,6 +493,7 @@ Duration=${payload.duration} ` this.logger.info({response}, `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); @@ -512,6 +519,7 @@ Duration=${payload.duration} ` async _onInfo(dlg, req, res) { const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag; const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag; + const contentType = req.get('Content-Type'); try { if (dlg.type === 'uac' && req.has('X-Reason')) { const reason = req.get('X-Reason'); @@ -617,6 +625,40 @@ Duration=${payload.duration} ` res.send(succeeded ? 200 : 503); } } + else if (dlg.type === 'uas' && ['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.uas.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 immutableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'max-forwards', 'content-length']; const headers = {}; diff --git a/lib/utils.js b/lib/utils.js index 171a34c..5af342f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -15,7 +15,13 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) { const from = req.getParsedHeader('from'); const srtpOpts = teams ? srtpCharacteristics['teams'] : srtpCharacteristics['default']; const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCharacteristics; - const srctOpts = srcIsUsingSrtp ? 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'] @@ -24,7 +30,7 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, teams = false) { common, uas: { tag: from.params.tag, - mediaOpts: srctOpts + mediaOpts: srcOpts }, uac: { tag: null,