include codec-accept on answer to rtpengine during reinvites (#209)

* include codec-accept on answer to rtpengine during reinvites

* attempt to simplify

* fixes from testing
This commit is contained in:
Dave Horton
2025-12-02 07:37:18 -05:00
committed by GitHub
parent 5fab8a7515
commit b3fee43c7f
2 changed files with 21 additions and 75 deletions

View File

@@ -1,5 +1,4 @@
const Emitter = require('events'); const Emitter = require('events');
const sdpTransform = require('sdp-transform');
const SrsClient = require('@jambonz/siprec-client-utils'); const SrsClient = require('@jambonz/siprec-client-utils');
const { const {
makeRtpEngineOpts, makeRtpEngineOpts,
@@ -10,7 +9,6 @@ const {
makePartnerFullMediaReleaseKey, makePartnerFullMediaReleaseKey,
isValidDomainOrIP, isValidDomainOrIP,
removeVideoSdp, removeVideoSdp,
determineAnswerCodec
} = require('./utils'); } = require('./utils');
const { MediaPath } = require('./constants.json'); const { MediaPath } = require('./constants.json');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
@@ -96,19 +94,6 @@ const initCdr = (req, invite) => {
}; };
}; };
const updateRtpEngineFlags = (sdp, opts) => {
try {
const parsed = sdpTransform.parse(sdp);
const codec = parsed.media[0].rtp[0].codec;
// Only add telephone-event support, don't restrict to specific G.711 codec yet
// This allows far end to choose between PCMU/PCMA freely
if (['PCMU', 'PCMA'].includes(codec)) {
opts.flags.push('codec-accept-telephone-event');
}
} catch {}
return opts;
};
class CallSession extends Emitter { class CallSession extends Emitter {
constructor(logger, req, res) { constructor(logger, req, res) {
super(); super();
@@ -398,13 +383,13 @@ class CallSession extends Emitter {
const toPublic = uris.some((u) => u.private_network === false); const toPublic = uris.some((u) => u.private_network === false);
let isOfferUpdatedToPrivate = toPrivate && !toPublic; let isOfferUpdatedToPrivate = toPrivate && !toPublic;
const opts = updateRtpEngineFlags(this.req.body, { const opts = {
...this.rtpEngineOpts.common, ...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts, ...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag, 'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['private', toPublic ? 'public' : 'private'], direction: ['private', toPublic ? 'public' : 'private'],
sdp: this.req.body sdp: this.req.body
}); };
let response = await this.offer(opts); let response = await this.offer(opts);
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine'); this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
if ('ok' !== response.result) { if ('ok' !== response.result) {
@@ -516,7 +501,7 @@ class CallSession extends Emitter {
}) })
}; };
const p = proxy ? ` via ${proxy}` : ''; const p = proxy ? ` via ${proxy}` : '';
this.logger.info({uri, p}, `sending INVITE to ${uri}${p}`); this.logger.info({uri, p}, `sending INVITE${p}`);
} }
/* now launch an outbound call attempt */ /* now launch an outbound call attempt */
@@ -557,15 +542,16 @@ class CallSession extends Emitter {
localSdpA: async(sdp, res) => { localSdpA: async(sdp, res) => {
this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag; this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
// Determine which codec to use based on far end negotiation
const {codec} = determineAnswerCodec(sdp, this.req.body, this.logger);
const opts = { const opts = {
...this.rtpEngineOpts.common, ...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uas.mediaOpts, ...this.rtpEngineOpts.uas.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag, 'from-tag': this.rtpEngineOpts.uas.tag,
'to-tag': this.rtpEngineOpts.uac.tag, 'to-tag': this.rtpEngineOpts.uac.tag,
flags: ['single codec', 'inject DTMF', `codec-accept-${codec}`, 'codec-accept-telephone-event'], flags: [
'single codec',
'inject DTMF',
'reuse codecs',
],
sdp sdp
}; };
const response = await this.answer(opts); const response = await this.answer(opts);

View File

@@ -16,8 +16,8 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
} }
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default']; const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy; const dstOpts = JSON.parse(JSON.stringify(dstIsUsingSrtp ? srtpOpts : rtpCopy));
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy; const srcOpts = JSON.parse(JSON.stringify(srcIsUsingSrtp ? srtpOpts : rtpCopy));
/** Allow feature server to send DTMF to the call excepts call from/to teams */ /** Allow feature server to send DTMF to the call excepts call from/to teams */
if (!teams) { if (!teams) {
@@ -41,6 +41,16 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
'replace': ['origin', 'session-connection'], 'replace': ['origin', 'session-connection'],
'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no' 'record call': process.env.JAMBONES_RECORD_ALL_CALLS ? 'yes' : 'no'
}; };
const codec = {
accept: ['PCMU', 'PCMA', 'telephone-event'],
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
{
offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','),
strip: 'all'
}),
};
return { return {
common, common,
uas: { uas: {
@@ -51,8 +61,7 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
tag: null, tag: null,
mediaOpts: { mediaOpts: {
...dstOpts, ...dstOpts,
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER && codec,
{ codec: { offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' } }),
} }
} }
}; };
@@ -350,54 +359,6 @@ const removeVideoSdp = (sdp) => {
return sdpTransform.write(parsedSdp); return sdpTransform.write(parsedSdp);
}; };
const determineAnswerCodec = (farEndSdp, featureServerSdp, logger) => {
try {
// Parse both SDPs
const farEndParsed = sdpTransform.parse(farEndSdp);
const fsParsed = sdpTransform.parse(featureServerSdp);
// Get negotiated codec from far end (first codec in answer)
const negotiatedCodec = farEndParsed.media[0].rtp[0].codec;
// Get all codecs offered by feature server
const fsCodecs = fsParsed.media[0].rtp.map((r) => r.codec);
logger.debug({negotiatedCodec, fsCodecs}, 'determineAnswerCodec: analyzing codec negotiation');
// If far end negotiated G.711 (PCMU/PCMA) AND it was in the FS offer, pass it through
if (['PCMU', 'PCMA'].includes(negotiatedCodec) && fsCodecs.includes(negotiatedCodec)) {
logger.info({negotiatedCodec}, 'G.711 codec passthrough - no transcoding needed');
return {
codec: negotiatedCodec,
needsTranscoding: false
};
}
// Otherwise, we need to transcode to first G.711 codec in FS offer
const firstG711 = fsCodecs.find((c) => ['PCMU', 'PCMA'].includes(c));
if (firstG711) {
logger.info({negotiatedCodec, transcodeTarget: firstG711}, 'Transcoding required to G.711');
return {
codec: firstG711,
needsTranscoding: true
};
}
// Fallback: use PCMU
logger.info({negotiatedCodec}, 'No G.711 in FS offer, defaulting to PCMU');
return {
codec: 'PCMU',
needsTranscoding: true
};
} catch (err) {
logger.error({err}, 'Error determining answer codec, defaulting to PCMU');
return {
codec: 'PCMU',
needsTranscoding: true
};
}
};
module.exports = { module.exports = {
makeRtpEngineOpts, makeRtpEngineOpts,
selectHostPort, selectHostPort,
@@ -414,5 +375,4 @@ module.exports = {
makePartnerFullMediaReleaseKey, makePartnerFullMediaReleaseKey,
isValidDomainOrIP, isValidDomainOrIP,
removeVideoSdp, removeVideoSdp,
determineAnswerCodec
}; };