Compare commits

..
5 changed files with 537 additions and 641 deletions
-2
View File
@@ -34,7 +34,5 @@ build/Release
node_modules
.DS_Store
.vscode
examples/*
CLAUDE.md
+25 -24
View File
@@ -1,4 +1,5 @@
const Emitter = require('events');
const sdpTransform = require('sdp-transform');
const SrsClient = require('@jambonz/siprec-client-utils');
const {
makeRtpEngineOpts,
@@ -8,7 +9,7 @@ const {
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey,
isValidDomainOrIP,
removeVideoSdp,
removeVideoSdp
} = require('./utils');
const { MediaPath } = require('./constants.json');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
@@ -94,6 +95,18 @@ const initCdr = (req, invite) => {
};
};
const updateRtpEngineFlags = (sdp, opts) => {
try {
const parsed = sdpTransform.parse(sdp);
const codec = parsed.media[0].rtp[0].codec;
if (['PCMU', 'PCMA'].includes(codec)) {
opts.flags.push(`codec-accept-${codec}`);
opts.flags.push('codec-accept-telephone-event');
}
} catch {}
return opts;
};
class CallSession extends Emitter {
constructor(logger, req, res) {
super();
@@ -110,7 +123,6 @@ class CallSession extends Emitter {
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
this.retrieveHash = req.srf.locals.realtimeDbHelpers.retrieveHash;
const {
lookupOutboundCarrierForAccount,
@@ -352,8 +364,7 @@ class CallSession extends Emitter {
* like a rare use case -- encryption is usually an all or nothing requirement.
*/
this.logger.info({u}, `using SRTP for outbound call, pad crypto: ${o.pad_crypto ? 'yes' : 'no'}`);
// eslint-disable-next-line max-len
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true, o.remove_ice, o.dtls_off);
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true);
encryptedMedia = true;
}
});
@@ -385,13 +396,13 @@ class CallSession extends Emitter {
const toPublic = uris.some((u) => u.private_network === false);
let isOfferUpdatedToPrivate = toPrivate && !toPublic;
const opts = {
const opts = updateRtpEngineFlags(this.req.body, {
...this.rtpEngineOpts.common,
...this.rtpEngineOpts.uac.mediaOpts,
'from-tag': this.rtpEngineOpts.uas.tag,
direction: ['private', toPublic ? 'public' : 'private'],
sdp: this.req.body
};
});
let response = await this.offer(opts);
this.logger.debug({offer: opts, response}, 'initial offer to rtpengine');
if ('ok' !== response.result) {
@@ -503,7 +514,7 @@ class CallSession extends Emitter {
})
};
const p = proxy ? ` via ${proxy}` : '';
this.logger.info({uri, p}, `sending INVITE${p}`);
this.logger.info({uri, p}, `sending INVITE to ${uri}${p}`);
}
/* now launch an outbound call attempt */
@@ -543,17 +554,12 @@ class CallSession extends Emitter {
localSdpB: response.sdp,
localSdpA: async(sdp, res) => {
this.rtpEngineOpts.uac.tag = res.getParsedHeader('To').params.tag;
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',
'reuse codecs',
],
flags: ['single codec'],
sdp
};
const response = await this.answer(opts);
@@ -725,13 +731,10 @@ class CallSession extends Emitter {
if (this.req.locals.cdr) {
const now = Date.now();
const day = new Date();
// eslint-disable-next-line max-len
const {hasRecording = false} = await this.retrieveHash(`call:${this.account_sid}:${this.req.locals.cdr.call_sid}`) ?? {};
const recordAllCalls = this.req.locals.record_all_calls || hasRecording;
const record_format = this.req.locals.account.record_format || 'mp3';
const recordAllCalls = this.req.locals.record_all_calls;
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${this.req.locals.cdr.call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${record_format}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${recordAllCalls}`;
this.writeCdrs({...this.req.locals.cdr,
terminated_at: now,
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
@@ -1213,6 +1216,7 @@ Duration=${payload.duration} `
...(req.has('X-Retain-Call-Sid') && {'X-Retain-Call-Sid': req.get('X-Retain-Call-Sid')}),
...(req.has('X-Account-Sid') && {'X-Account-Sid': req.get('X-Account-Sid')})
};
const uac = await this.srf.createUAC(referTo.uri, {localSdp: dlg.local.sdp, headers});
this.uas = uac;
uac.type = 'uas';
@@ -1221,7 +1225,7 @@ Duration=${payload.duration} `
uac.on('info', this._onInfo.bind(this, uac));
uac.on('modify', this._onReinvite.bind(this, uac));
uac.on('refer', this._onFeatureServerTransfer.bind(this, uac));
uac.on('destroy', async() => {
uac.on('destroy', () => {
this.logger.info('call ended with normal termination');
this.rtpEngineResource.destroy();
this.activeCallIds.delete(this.req.get('Call-ID'));
@@ -1230,13 +1234,10 @@ Duration=${payload.duration} `
if (this.req.locals.cdr) {
const now = Date.now();
const day = new Date();
// eslint-disable-next-line max-len
const {hasRecording = false} = await this.retrieveHash(`call:${this.account_sid}:${this.req.locals.cdr.call_sid}`) ?? {} ;
const recordAllCalls = this.req.locals.record_all_calls || hasRecording;
const record_format = this.req.locals.account.record_format || 'mp3';
const recordAllCalls = this.req.locals.record_all_calls;
let recording_url = `/Accounts/${this.account_sid}/RecentCalls/${this.req.locals.cdr.call_sid}/record`;
recording_url += `/${day.getFullYear()}/${(day.getMonth() + 1).toString().padStart(2, '0')}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${record_format}`;
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${recordAllCalls}`;
this.writeCdrs({...this.req.locals.cdr,
terminated_at: now,
termination_reason: 'caller hungup',
+8 -27
View File
@@ -5,8 +5,7 @@ const CIDRMatcher = require('cidr-matcher');
const dns = require('dns');
const sdpTransform = require('sdp-transform');
// eslint-disable-next-line max-len
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams, remove_ice = false, dtls_off = false) {
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams) {
const from = req.getParsedHeader('from');
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
@@ -17,19 +16,11 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
}
const srtpOpts = teams ? srtpCopy['teams'] : srtpCopy['default'];
const dstOpts = dstIsUsingSrtp ? srtpOpts : rtpCopy;
const srcOpts = srcIsUsingSrtp ? srtpOpts : rtpCopy;
if (remove_ice) {
srtpOpts.ICE = 'remove';
}
if (dtls_off) {
srtpOpts.DTLS = 'off';
}
const dstOpts = JSON.parse(JSON.stringify(dstIsUsingSrtp ? 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 */
if (!teams) {
/** Allow rtpengine to inject DTMF on calls from webrtc only (this disables kernel forwarding) */
if (!teams && (dstIsUsingSrtp || process.env.JAMBONES_RTPENGINE_INJECT_DTMF_ALWAYS)) {
if (!dstOpts.flags.includes('inject DTMF')) {
dstOpts.flags.push('inject DTMF');
}
@@ -50,16 +41,6 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
'replace': ['origin', 'session-connection'],
'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 {
common,
uas: {
@@ -70,7 +51,8 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
tag: null,
mediaOpts: {
...dstOpts,
codec,
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
{ codec: { offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' } }),
}
}
};
@@ -367,7 +349,6 @@ const removeVideoSdp = (sdp) => {
parsedSdp.media = parsedSdp.media.filter((media) => media.type !== 'video');
return sdpTransform.write(parsedSdp);
};
module.exports = {
makeRtpEngineOpts,
selectHostPort,
@@ -383,5 +364,5 @@ module.exports = {
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey,
isValidDomainOrIP,
removeVideoSdp,
removeVideoSdp
};
+501 -585
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -39,7 +39,7 @@
"cidr-matcher": "^2.1.1",
"debug": "^4.4.3",
"drachtio-fn-b2b-sugar": "^0.2.1",
"drachtio-srf": "^5.0.18",
"drachtio-srf": "^5.0.12",
"express": "^4.21.2",
"pino": "^10.1.0",
"sdp-transform": "^2.15.0"
@@ -48,7 +48,7 @@
"bent": "^7.3.12",
"eslint": "^9.17.0",
"eslint-plugin-promise": "^7.2.1",
"nyc": "^17.1.0",
"tape": "^5.9.0"
"nyc": "^15.1.0",
"tape": "^5.7.5"
}
}