diff --git a/app.js b/app.js index a5aec70..7b046b3 100644 --- a/app.js +++ b/app.js @@ -68,6 +68,8 @@ const { decrKey, retrieveSet, isMemberOfSet, + addKey, + retrieveKey } = require('@jambonz/realtimedb-helpers')({}, logger); const activeCallIds = new Map(); @@ -101,6 +103,8 @@ srf.locals = {...srf.locals, }, realtimeDbHelpers: { client: redisClient, + addKey, + retrieveKey, createHash, retrieveHash, incrKey, diff --git a/lib/call-session.js b/lib/call-session.js index d0a90ce..7613772 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -1,7 +1,15 @@ const Emitter = require('events'); const sdpTransform = require('sdp-transform'); const SrsClient = require('@jambonz/siprec-client-utils'); -const {makeRtpEngineOpts, nudgeCallCounts, isPrivateVoipNetwork, isBlackListedSipGateway} = require('./utils'); +const { + makeRtpEngineOpts, + nudgeCallCounts, + isPrivateVoipNetwork, + isBlackListedSipGateway, + makeFullMediaReleaseKey, + makePartnerFullMediaReleaseKey +} = require('./utils'); +const { MediaPath } = require('./constants.json'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); const {SipError, stringifyUri, parseUri} = require('drachtio-srf'); const debug = require('debug')('jambonz:sbc-outbound'); @@ -111,6 +119,8 @@ class CallSession extends Emitter { this.writeCdrs = this.srf.locals.writeCdrs; this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey; + this.addKey = req.srf.locals.realtimeDbHelpers.addKey; + this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey; const { lookupOutboundCarrierForAccount, @@ -123,7 +133,8 @@ class CallSession extends Emitter { this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier; this.lookupCarrierByAccountLcr = lookupCarrierByAccountLcr; - this._mediaReleased = false; + this._mediaPath = MediaPath.FullMedia; + this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000; } @@ -144,7 +155,7 @@ class CallSession extends Emitter { } get isMediaReleased() { - return this._mediaReleased; + return this._mediaPath !== MediaPath.FullMedia; } subscribeForDTMF(dlg) { @@ -651,6 +662,15 @@ class CallSession extends Emitter { answered_at: callStart }; } + + /* save far end SDP for later use if we do a full media release */ + if (process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE) { + const key = makeFullMediaReleaseKey(this.req.get('X-CID')); + const sdp = uac.remote.sdp; + this.logger.info({key, sdp}, 'saving far end sdp for full media release feature'); + this.addKey(key, sdp, 3600).catch((err) => this.logger.error(err, 'Error saving far end sdp')); + } + this.uas = uas; this.uac = uac; [uas, uac].forEach((dlg) => { @@ -807,11 +827,32 @@ Duration=${payload.duration} ` try { const reason = req.get('X-Reason'); const isReleasingMedia = reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason); + const isFullMediaRelease = reason === 'release-media-entirely' && process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE; 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 offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts; const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts; const direction = dlg.type === 'uas' ? ['private', 'public'] : ['public', 'private']; + + if (isFullMediaRelease) { + const a_sdp = await this.retrieveKey(makePartnerFullMediaReleaseKey(this.req.get('X-CID'))); + this.logger.info({a_sdp}, 'reinvite ourselves out of the media path with this reinvite offer'); + const answerSdp = await dlg.other.modify(a_sdp); + this.logger.info({answerSdp}, 'far end response to full media release'); + res.send(200, { + body: dlg.local.sdp, + headers: { + 'Contact': this.contactHeader + } + }); + /* no media going through us now we can destroy the rtpengine resource */ + this.rtpEngineResource.destroy().catch((err) => { + this.logger.info({err}, 'Error destroying rtpengine resource after full media release'); + }); + this._mediaPath = MediaPath.NoMedia; + return; + } + if (isReleasingMedia) { if (!offerMedia.flags.includes('port latching')) offerMedia.flags.push('port latching'); if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric'); @@ -839,17 +880,18 @@ Duration=${payload.duration} ` let sdp; //HL 2024-11-13: previously forwarded re-invites to webrtc clients but further testing has shown to be unnecessary //if (isReleasingMedia && !this.calleeIsUsingSrtp) { - if (isReleasingMedia) { + + //DH 2024-11- 18: if we are going from no-media to either partial or full media, we need reinvite the far end + if (isReleasingMedia && this._mediaPath !== MediaPath.NoMedia) { this.logger.info(`got a reinvite from FS to ${reason}`); sdp = dlg.other.remote.sdp; if (!answerMedia.flags.includes('port latching')) answerMedia.flags.push('port latching'); if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric'); answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover'); - this._mediaReleased = 'release-media' === reason; + this._mediaPath = 'release-media' === reason ? MediaPath.PartialMedia : MediaPath.FullMedia; } else { sdp = await dlg.other.modify(response.sdp); - this.logger.info({sdp}, 'CallSession:_onReinvite: got sdp from 200 OK to invite we sent'); } opts = { ...this.rtpEngineOpts.common, diff --git a/lib/constants.json b/lib/constants.json new file mode 100644 index 0000000..c781292 --- /dev/null +++ b/lib/constants.json @@ -0,0 +1,7 @@ +{ + "MediaPath": { + "NoMedia": "no-media", + "PartialMedia": "partial-media", + "FullMedia": "full-media" + } +} diff --git a/lib/utils.js b/lib/utils.js index 44df2a8..5e44757 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -233,6 +233,13 @@ async function isBlackListedSipGateway(client, logger, sip_gateway_sid) { } } +const makeFullMediaReleaseKey = (callId) => { + return `b_sdp:${callId}`; +}; +const makePartnerFullMediaReleaseKey = (callId) => { + return `a_sdp:${callId}`; +}; + module.exports = { makeRtpEngineOpts, selectHostPort, @@ -244,5 +251,7 @@ module.exports = { createHealthCheckApp, nudgeCallCounts, isPrivateVoipNetwork, - isBlackListedSipGateway + isBlackListedSipGateway, + makeFullMediaReleaseKey, + makePartnerFullMediaReleaseKey };