mirror of
https://github.com/jambonz/sbc-outbound.git
synced 2026-10-04 02:04:19 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
560c18ef68 | ||
|
|
fec25d5d15 | ||
|
|
5c470b9630 | ||
|
|
606792825e | ||
|
|
ffec713d62 | ||
|
|
6921c1cf31 | ||
|
|
37e4dea705 | ||
|
|
4fb5acaf40 | ||
|
|
865068f158 |
@@ -29,9 +29,19 @@ Configuration is provided via environment variables:
|
||||
|JAMBONES_RTPENGINES| commans-separated list of ip:ng-port for rtpengines (e.g. '172.31.32.10:22222')|yes|
|
||||
|JAMBONES_TIME_SERIES_HOST| influxdb host |yes|
|
||||
|JAMBONES_RECORD_ALL_CALLS| enable auto record calls |no|
|
||||
|JAMBONES_CODEC_OFFER_WITH_ORDER| comma-separated codec list to use as the outbound offer toward the carrier; the original codecs from the feature server are stripped first (e.g. `'opus,PCMU,PCMA,telephone-event'`) |no|
|
||||
|JAMBONES_CODEC_TRANSCODE| comma-separated codec list that rtpengine should add to the outbound offer and transcode on the fly when the carrier selects them — used to support codecs the feature server does not speak (e.g. `'AMR-WB/16000'`). Requires an rtpengine build that includes the corresponding codec module. |no|
|
||||
|JAMBONES_ACCEPT_AND_TRANSCODE| comma-separated codec list to accept on the outbound leg and transcode to PCMU/PCMA toward the feature server |no|
|
||||
|JAMBONES_ACCEPT_G729| if set, accept G.729 on the outbound leg and transcode to PCMU/PCMA (shorthand for `JAMBONES_ACCEPT_AND_TRANSCODE=g729`) |no|
|
||||
|K8S| service running as kubernetes service |no|
|
||||
|K8S_RTPENGINE_SERVICE_NAME| rtpengine service name(required for K8S) |no|
|
||||
|
||||
#### Codec transcoding notes
|
||||
|
||||
`JAMBONES_CODEC_OFFER_WITH_ORDER` and `JAMBONES_CODEC_TRANSCODE` can be combined. When both are set, rtpengine first strips the original codecs from the SDP, then adds the codecs listed in `JAMBONES_CODEC_OFFER_WITH_ORDER` to the offer, and finally adds the codecs from `JAMBONES_CODEC_TRANSCODE` with transcoding enabled. When only `JAMBONES_CODEC_TRANSCODE` is set, the original codecs from the feature server are kept in the offer and the transcode codecs are appended.
|
||||
|
||||
Codecs that require licensed or optional rtpengine modules (AMR, AMR-WB, G.729, etc.) only work if rtpengine was compiled with support for them. Carrier-specific fmtp parameters (for example, `octet-align=1` for AMR-WB) can be appended to the codec name as needed (e.g. `AMR-WB/16000;octet-align=1`).
|
||||
|
||||
### running under pm2
|
||||
Typically, this application runs under [pm2](https://pm2.io) using an [ecosystem.config.js](https://pm2.keymetrics.io/docs/usage/application-declaration/) file similar to this:
|
||||
```js
|
||||
|
||||
@@ -70,7 +70,8 @@ const {
|
||||
isMemberOfSet,
|
||||
addKey,
|
||||
deleteKey,
|
||||
retrieveKey
|
||||
retrieveKey,
|
||||
addToSet
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger);
|
||||
|
||||
const activeCallIds = new Map();
|
||||
@@ -124,6 +125,17 @@ const {getRtpEngine, setRtpEngines} = require('@jambonz/rtpengine-utils')([], lo
|
||||
});
|
||||
srf.locals.getRtpEngine = getRtpEngine;
|
||||
|
||||
/* report our call count to redis so a draining sbc-inbound process can count
|
||||
calls across all sbc-inbound and sbc-outbound processes on this server */
|
||||
if (!process.env.K8S && 'test' !== process.env.NODE_ENV) {
|
||||
srf.locals.callCountReporter = require('./lib/call-count-reporter')({
|
||||
logger,
|
||||
addKey,
|
||||
addToSet,
|
||||
getCount: () => activeCallIds.size
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.DRACHTIO_HOST && !process.env.K8S) {
|
||||
const cidrs = process.env.JAMBONES_NETWORK_CIDR
|
||||
.split(',')
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"ICE": "force",
|
||||
"SDES": "off",
|
||||
"flags": ["generate mid", "SDES-no", "port latching"],
|
||||
"rtcp-mux": ["require"]
|
||||
"rtcp-mux": ["offer"]
|
||||
},
|
||||
"teams": {
|
||||
"transport-protocol": "RTP/SAVP",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Periodically report this process's count of calls in progress to redis.
|
||||
* A server may host several sbc-inbound and sbc-outbound processes; when one
|
||||
* of them handles an autoscale drain it needs to know when the entire server
|
||||
* has no calls in progress, not just its own process. Each process writes
|
||||
* its own count under a per-pid key (with a short expiry, so keys from dead
|
||||
* processes evaporate) and registers that key in a per-host set that the
|
||||
* draining process can enumerate.
|
||||
*/
|
||||
const REPORT_INTERVAL = 15000;
|
||||
const KEY_EXPIRY_SECS = 120;
|
||||
|
||||
module.exports = ({logger, addKey, addToSet, getCount}) => {
|
||||
const prefix = process.env.JAMBONES_CLUSTER_ID || 'default';
|
||||
const setName = `${prefix}:call-count-keys:${os.hostname()}`;
|
||||
const key = `${prefix}:call-count:${os.hostname()}:${process.pid}`;
|
||||
|
||||
const report = () => {
|
||||
addKey(key, `${getCount()}`, KEY_EXPIRY_SECS)
|
||||
.catch((err) => logger.error({err}, 'call-count-reporter: error writing call count'));
|
||||
};
|
||||
|
||||
addToSet(setName, key)
|
||||
.catch((err) => logger.error({err}, `call-count-reporter: error adding ${key} to ${setName}`));
|
||||
setInterval(report, REPORT_INTERVAL);
|
||||
report();
|
||||
|
||||
return {key, setName};
|
||||
};
|
||||
+38
-4
@@ -237,7 +237,20 @@ class CallSession extends Emitter {
|
||||
this.logger.info(`sending call to registered user ${destUri}`);
|
||||
}
|
||||
else if (this.req.locals.target === 'forward') {
|
||||
if (process.env.JAMBONES_SIPS_FORWARD_SRTP && this.req.uri.startsWith('sips:')) {
|
||||
/* the feature-server sets X-Jambonz-SRTP (from the dial verb's srtpEncryption
|
||||
option) to request encrypted media on a per-call basis */
|
||||
const srtpMode = this.req.get('X-Jambonz-SRTP');
|
||||
if (srtpMode) {
|
||||
/* SDES (RTP/SAVP): pass teams=true to select the SDES srtp profile, matching
|
||||
the proven carrier tls/srtp path above. This offers a=crypto to the target
|
||||
(what SIP endpoints such as LiveKit expect), not DTLS-SRTP. */
|
||||
this.logger.info({uri: this.req.uri, srtpMode},
|
||||
'using SRTP (SDES) for forwarded call per X-Jambonz-SRTP');
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, false, true);
|
||||
encryptedMedia = true;
|
||||
}
|
||||
else if (process.env.JAMBONES_SIPS_FORWARD_SRTP && this.req.uri.startsWith('sips:')) {
|
||||
/* legacy global opt-in for sips: forwards (uses the DTLS srtp profile) */
|
||||
this.logger.info({uri: this.req.uri}, 'using SRTP/TLS for forwarded sips: call');
|
||||
this.rtpEngineOpts = makeRtpEngineOpts(this.req, false, true, false, false);
|
||||
encryptedMedia = true;
|
||||
@@ -531,6 +544,7 @@ class CallSession extends Emitter {
|
||||
'-X-Preferred-From-Host',
|
||||
'-X-Jambonz-FS-UUID',
|
||||
'-X-Voip-Carrier-Sid',
|
||||
'-X-Jambonz-SRTP',
|
||||
'-X-SIP-Proxy'
|
||||
],
|
||||
proxyResponseHeaders: [
|
||||
@@ -665,7 +679,21 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if ('abandonded' !== err.message) this.logger.error({err}, `Error setting up outbound call to: ${uris}`);
|
||||
if ('abandoned' !== err.message) this.logger.error({err}, `Error setting up outbound call to: ${uris}`);
|
||||
|
||||
/* the call count was incremented on 'init' but we bailed out before connecting (e.g. caller
|
||||
hung up before the B leg answered), so we must decrement it here - no failure response is
|
||||
sent on this path, so the res.once('end') safety net in middleware would not fire.
|
||||
nudgeCallCounts is a no-op if the count was already decremented elsewhere. */
|
||||
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
|
||||
nudgeCallCounts(this.req, 'failure', {
|
||||
service_provider_sid: this.service_provider_sid,
|
||||
account_sid: this.account_sid,
|
||||
application_sid: this.application_sid,
|
||||
callId: this.req.locals.callId
|
||||
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
|
||||
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
|
||||
|
||||
this.emit('failed');
|
||||
this.srf.endSession(this.req);
|
||||
this.rtpEngineResource.destroy();
|
||||
@@ -892,8 +920,14 @@ Duration=${payload.duration} `
|
||||
process.env.JAMBONES_ACCEPT_G729 ? 'g729' : '';
|
||||
offerMedia = { ...offerMedia, ...(acceptCodecs && dlg.type === 'uac' &&
|
||||
{ codec: { mask: acceptCodecs, transcode: 'pcmu,pcma' } })};
|
||||
answerMedia = { ...answerMedia, ...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER && dlg.type === 'uac' &&
|
||||
{ codec: {offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' }})};
|
||||
answerMedia = { ...answerMedia, ...((process.env.JAMBONES_CODEC_OFFER_WITH_ORDER ||
|
||||
process.env.JAMBONES_CODEC_TRANSCODE) && dlg.type === 'uac' &&
|
||||
{ codec: {
|
||||
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
|
||||
{ offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' }),
|
||||
...(process.env.JAMBONES_CODEC_TRANSCODE &&
|
||||
{ transcode: process.env.JAMBONES_CODEC_TRANSCODE.split(',') })
|
||||
}})};
|
||||
let opts = {
|
||||
...this.rtpEngineOpts.common,
|
||||
...offerMedia,
|
||||
|
||||
@@ -58,6 +58,10 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
|
||||
offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','),
|
||||
strip: 'all'
|
||||
}),
|
||||
...(process.env.JAMBONES_CODEC_TRANSCODE &&
|
||||
{
|
||||
transcode: process.env.JAMBONES_CODEC_TRANSCODE.split(',')
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Generated
+7
-6
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.9.6",
|
||||
"version": "0.9.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.9.6",
|
||||
"version": "0.9.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helpers": "^7.26.10",
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.21",
|
||||
"@jambonz/rtpengine-utils": "^0.4.4",
|
||||
"@jambonz/siprec-client-utils": "^0.2.10",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
@@ -594,9 +594,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/realtimedb-helpers": {
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.8.13.tgz",
|
||||
"integrity": "sha512-/7c8kCu/dMSk8CWTYdEv4xU/7+vmVL30VCjtHPRAXVBRIVejAPDf1xK5n0rXwG8Fda1Oa+0TFsymkiRpew8P6w==",
|
||||
"version": "0.8.21",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.8.21.tgz",
|
||||
"integrity": "sha512-Zn/Zw14U0L1pB2RlN5i160FwWqphonhQ7p880OzsKOuvg+wmj6qIkxdLjDXo6TSxFPXDHD25s33zfGOT6ZIfsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"ioredis": "^5.3.2"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-outbound",
|
||||
"version": "0.9.6",
|
||||
"version": "0.9.9",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 20.0.0"
|
||||
@@ -31,7 +31,7 @@
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.21",
|
||||
"@jambonz/rtpengine-utils": "^0.4.4",
|
||||
"@jambonz/siprec-client-utils": "^0.2.10",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
|
||||
Reference in New Issue
Block a user