diff --git a/.gitignore b/.gitignore index 9ed71c2..65502e2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ node_modules .DS_Store examples/* +CLAUDE.md \ No newline at end of file diff --git a/lib/call-session.js b/lib/call-session.js index b9bf5f1..e9f82d8 100644 --- a/lib/call-session.js +++ b/lib/call-session.js @@ -9,7 +9,8 @@ const { makeFullMediaReleaseKey, makePartnerFullMediaReleaseKey, isValidDomainOrIP, - removeVideoSdp + removeVideoSdp, + determineAnswerCodec } = require('./utils'); const { MediaPath } = require('./constants.json'); const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar'); @@ -99,8 +100,9 @@ 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-${codec}`); opts.flags.push('codec-accept-telephone-event'); } } catch {} @@ -554,12 +556,16 @@ class CallSession extends Emitter { localSdpB: response.sdp, localSdpA: async(sdp, res) => { 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 = { ...this.rtpEngineOpts.common, ...this.rtpEngineOpts.uas.mediaOpts, 'from-tag': this.rtpEngineOpts.uas.tag, 'to-tag': this.rtpEngineOpts.uac.tag, - flags: ['single codec', 'inject DTMF'], + flags: ['single codec', 'inject DTMF', `codec-accept-${codec}`, 'codec-accept-telephone-event'], sdp }; const response = await this.answer(opts); diff --git a/lib/utils.js b/lib/utils.js index 37887ea..09156a5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -349,6 +349,55 @@ const removeVideoSdp = (sdp) => { parsedSdp.media = parsedSdp.media.filter((media) => media.type !== 'video'); 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 = { makeRtpEngineOpts, selectHostPort, @@ -364,5 +413,6 @@ module.exports = { makeFullMediaReleaseKey, makePartnerFullMediaReleaseKey, isValidDomainOrIP, - removeVideoSdp + removeVideoSdp, + determineAnswerCodec };