mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
121ae75b39 | ||
|
|
57bc073ea3 | ||
|
|
84bce56766 | ||
|
|
d053b94e71 | ||
|
|
a50ed56b36 | ||
|
|
8fdcc8cbc4 | ||
|
|
b882c0de2d | ||
|
|
b3fee43c7f | ||
|
|
5fab8a7515 | ||
|
|
178105acd7 |
@@ -34,5 +34,7 @@ build/Release
|
||||
node_modules
|
||||
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
||||
examples/*
|
||||
CLAUDE.md
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
|
||||
FROM --platform=linux/amd64 node:24-alpine AS base
|
||||
|
||||
RUN apk --update --no-cache add --virtual .builds-deps build-base python3
|
||||
|
||||
WORKDIR /opt/app/
|
||||
|
||||
FROM base as build
|
||||
FROM base AS build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
@@ -18,6 +18,6 @@ COPY --from=build /opt/app /opt/app/
|
||||
|
||||
ARG NODE_ENV
|
||||
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
ENV NODE_ENV=$NODE_ENV
|
||||
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
+24
-25
@@ -1,5 +1,4 @@
|
||||
const Emitter = require('events');
|
||||
const sdpTransform = require('sdp-transform');
|
||||
const SrsClient = require('@jambonz/siprec-client-utils');
|
||||
const {
|
||||
makeRtpEngineOpts,
|
||||
@@ -9,7 +8,7 @@ const {
|
||||
makeFullMediaReleaseKey,
|
||||
makePartnerFullMediaReleaseKey,
|
||||
isValidDomainOrIP,
|
||||
removeVideoSdp
|
||||
removeVideoSdp,
|
||||
} = require('./utils');
|
||||
const { MediaPath } = require('./constants.json');
|
||||
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
|
||||
@@ -95,18 +94,6 @@ 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();
|
||||
@@ -123,6 +110,7 @@ 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,
|
||||
@@ -364,7 +352,8 @@ 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'}`);
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true);
|
||||
// eslint-disable-next-line max-len
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, o.pad_crypto, true, o.remove_ice, o.dtls_off);
|
||||
encryptedMedia = true;
|
||||
}
|
||||
});
|
||||
@@ -396,13 +385,13 @@ class CallSession extends Emitter {
|
||||
const toPublic = uris.some((u) => u.private_network === false);
|
||||
let isOfferUpdatedToPrivate = toPrivate && !toPublic;
|
||||
|
||||
const opts = updateRtpEngineFlags(this.req.body, {
|
||||
const opts = {
|
||||
...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) {
|
||||
@@ -514,7 +503,7 @@ class CallSession extends Emitter {
|
||||
})
|
||||
};
|
||||
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 */
|
||||
@@ -554,12 +543,17 @@ 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'],
|
||||
flags: [
|
||||
'single codec',
|
||||
'inject DTMF',
|
||||
'reuse codecs',
|
||||
],
|
||||
sdp
|
||||
};
|
||||
const response = await this.answer(opts);
|
||||
@@ -731,10 +725,13 @@ class CallSession extends Emitter {
|
||||
if (this.req.locals.cdr) {
|
||||
const now = Date.now();
|
||||
const day = new Date();
|
||||
const recordAllCalls = this.req.locals.record_all_calls;
|
||||
// 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';
|
||||
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')}/${recordAllCalls}`;
|
||||
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${record_format}`;
|
||||
this.writeCdrs({...this.req.locals.cdr,
|
||||
terminated_at: now,
|
||||
termination_reason: dlg.type === 'uas' ? 'caller hungup' : 'called party hungup',
|
||||
@@ -1216,7 +1213,6 @@ 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';
|
||||
@@ -1225,7 +1221,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', () => {
|
||||
uac.on('destroy', async() => {
|
||||
this.logger.info('call ended with normal termination');
|
||||
this.rtpEngineResource.destroy();
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
@@ -1234,10 +1230,13 @@ Duration=${payload.duration} `
|
||||
if (this.req.locals.cdr) {
|
||||
const now = Date.now();
|
||||
const day = new Date();
|
||||
const recordAllCalls = this.req.locals.record_all_calls;
|
||||
// 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';
|
||||
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')}/${recordAllCalls}`;
|
||||
recording_url += `/${day.getDate().toString().padStart(2, '0')}/${record_format}`;
|
||||
this.writeCdrs({...this.req.locals.cdr,
|
||||
terminated_at: now,
|
||||
termination_reason: 'caller hungup',
|
||||
|
||||
+25
-6
@@ -5,7 +5,8 @@ const CIDRMatcher = require('cidr-matcher');
|
||||
const dns = require('dns');
|
||||
const sdpTransform = require('sdp-transform');
|
||||
|
||||
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams) {
|
||||
// eslint-disable-next-line max-len
|
||||
function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams, remove_ice = false, dtls_off = false) {
|
||||
const from = req.getParsedHeader('from');
|
||||
const rtpCopy = JSON.parse(JSON.stringify(rtpCharacteristics));
|
||||
const srtpCopy = JSON.parse(JSON.stringify(srtpCharacteristics));
|
||||
@@ -16,8 +17,16 @@ 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) {
|
||||
@@ -41,6 +50,16 @@ 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: {
|
||||
@@ -51,8 +70,7 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
|
||||
tag: null,
|
||||
mediaOpts: {
|
||||
...dstOpts,
|
||||
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
|
||||
{ codec: { offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' } }),
|
||||
codec,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -349,6 +367,7 @@ const removeVideoSdp = (sdp) => {
|
||||
parsedSdp.media = parsedSdp.media.filter((media) => media.type !== 'video');
|
||||
return sdpTransform.write(parsedSdp);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
makeRtpEngineOpts,
|
||||
selectHostPort,
|
||||
@@ -364,5 +383,5 @@ module.exports = {
|
||||
makeFullMediaReleaseKey,
|
||||
makePartnerFullMediaReleaseKey,
|
||||
isValidDomainOrIP,
|
||||
removeVideoSdp
|
||||
removeVideoSdp,
|
||||
};
|
||||
|
||||
Generated
+596
-512
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.6",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
@@ -37,10 +37,10 @@
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.10",
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"debug": "^4.4.3",
|
||||
"drachtio-fn-b2b-sugar": "^0.2.1",
|
||||
"drachtio-srf": "^5.0.12",
|
||||
"express": "^4.19.2",
|
||||
"drachtio-srf": "^5.0.20",
|
||||
"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": "^15.1.0",
|
||||
"tape": "^5.7.5"
|
||||
"nyc": "^17.1.0",
|
||||
"tape": "^5.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user