Compare commits

..
Author SHA1 Message Date
Vinod Dharashive d8ae824559 Fresh/466 (#160)
* Hold and unhold does not resume transcript for outbound call to webrtc

* WIP

* jslint

* jslint

* handle null

* Added direction

* re-invite with opus codec need to change pcmu 

on reinvite on unhold opus codec was been sent from rtpegine to freeswitch due to which transcripts were not getting generated , hence it needs to be changed to pcmu

* jslint

jslint
2024-12-23 07:20:04 -05:00
Hoan Luu Huu 334db6f84f support referby display name (#161) 2024-12-11 10:34:27 -05:00
Hoan Luu Huu c762accce8 forward all refer to feature server (#156)
* forward all refer to feature server

* forwawrd extra custom header
2024-12-05 21:16:12 -05:00
Dave Horton c6c63e26da fix duplicate attempt to destroy dialog 2024-11-22 10:46:24 -05:00
Dave Horton 916d577b75 wip (#155) 2024-11-19 09:37:53 -05:00
Dave Horton b976a62a60 when forcing PCMU or PCMA we must also include telephone-event (#153) 2024-11-14 08:37:40 -05:00
Hoan Luu Huu 5e2369e5e8 support opus transcode (#150)
* support opus transcode

* dont resend reinvite when release media for srtp

* wip

* fix review comment
2024-11-13 07:41:43 -05:00
Dave Horton 41528db630 fix call count race condition #151 (#152)
* fix call count race condition #151

* logging fix
2024-11-12 14:57:04 -05:00
8 changed files with 120 additions and 36 deletions
+4
View File
@@ -68,6 +68,8 @@ const {
decrKey,
retrieveSet,
isMemberOfSet,
addKey,
retrieveKey
} = require('@jambonz/realtimedb-helpers')({}, logger);
const activeCallIds = new Map();
@@ -101,6 +103,8 @@ srf.locals = {...srf.locals,
},
realtimeDbHelpers: {
client: redisClient,
addKey,
retrieveKey,
createHash,
retrieveHash,
incrKey,
+84 -27
View File
@@ -1,7 +1,15 @@
const Emitter = require('events');
const sdpTransform = require('sdp-transform');
const SrsClient = require('@jambonz/siprec-client-utils');
const {makeRtpEngineOpts, nudgeCallCounts, isPrivateVoipNetwork, isBlackListedSipGateway} = require('./utils');
const {
makeRtpEngineOpts,
nudgeCallCounts,
isPrivateVoipNetwork,
isBlackListedSipGateway,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey
} = require('./utils');
const { MediaPath } = require('./constants.json');
const {forwardInDialogRequests} = require('drachtio-fn-b2b-sugar');
const {SipError, stringifyUri, parseUri} = require('drachtio-srf');
const debug = require('debug')('jambonz:sbc-outbound');
@@ -89,7 +97,10 @@ 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}`);
if (['PCMU', 'PCMA'].includes(codec)) {
opts.flags.push(`codec-accept-${codec}`);
opts.flags.push('codec-accept-telephone-event');
}
} catch (err) {}
return opts;
};
@@ -108,6 +119,8 @@ class CallSession extends Emitter {
this.writeCdrs = this.srf.locals.writeCdrs;
this.decrKey = req.srf.locals.realtimeDbHelpers.decrKey;
this.addKey = req.srf.locals.realtimeDbHelpers.addKey;
this.retrieveKey = req.srf.locals.realtimeDbHelpers.retrieveKey;
const {
lookupOutboundCarrierForAccount,
@@ -120,7 +133,8 @@ class CallSession extends Emitter {
this.lookupSipGatewaysByCarrier = lookupSipGatewaysByCarrier;
this.lookupCarrierByAccountLcr = lookupCarrierByAccountLcr;
this._mediaReleased = false;
this._mediaPath = MediaPath.FullMedia;
this.recordingNoAnswerTimeout = (process.env.JAMBONES_RECORDING_NO_ANSWER_TIMEOUT || 2) * 1000;
}
@@ -141,12 +155,7 @@ class CallSession extends Emitter {
}
get isMediaReleased() {
return this._mediaReleased;
}
get calleeIsUsingSrtp() {
const tp = this.rtpEngineOpts?.uac?.mediaOpts['transport-protocol'];
return tp && -1 !== tp.indexOf('SAVP');
return this._mediaPath !== MediaPath.FullMedia;
}
subscribeForDTMF(dlg) {
@@ -653,6 +662,15 @@ class CallSession extends Emitter {
answered_at: callStart
};
}
/* save far end SDP for later use if we do a full media release */
if (process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE) {
const key = makeFullMediaReleaseKey(this.req.get('X-CID'));
const sdp = uac.remote.sdp;
this.logger.info({key, sdp}, 'saving far end sdp for full media release feature');
this.addKey(key, sdp, 3600).catch((err) => this.logger.error(err, 'Error saving far end sdp'));
}
this.uas = uas;
this.uac = uac;
[uas, uac].forEach((dlg) => {
@@ -668,7 +686,6 @@ class CallSession extends Emitter {
if (!IMMUTABLE_HEADERS.includes(h)) headers[h] = bye.headers[h];
});
await other.destroy({headers});
await other.destroy();
} catch (err) {}
const trackingOn = process.env.JAMBONES_TRACK_ACCOUNT_CALLS ||
@@ -677,12 +694,16 @@ class CallSession extends Emitter {
if (process.env.JAMBONES_HOSTING || trackingOn) {
const {writeCallCount, writeCallCountSP, writeCallCountApp} = this.req.srf.locals;
await nudgeCallCounts(this.logger, {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
if (!this.req.locals.callCountNudged) {
this.req.locals.callCountNudged = true;
this.logger.debug('decrementing call count at end of call');
await nudgeCallCounts(this.logger, {
service_provider_sid: this.service_provider_sid,
account_sid: this.account_sid,
application_sid: this.application_sid
}, this.decrKey, {writeCallCountSP, writeCallCount, writeCallCountApp})
.catch((err) => this.logger.error(err, 'Error decrementing call counts'));
}
}
/* write cdr for connected call */
@@ -759,12 +780,19 @@ class CallSession extends Emitter {
async _onRefer(dlg, req, res) {
/* REFER coming in from a sip device, forward to feature server */
try {
const customHeaders = Object.keys(req.headers)
.filter((h) => h.toLowerCase().startsWith('x-'))
.reduce((acc, h) => {
acc[h] = req.get(h);
return acc;
}, {});
const response = await dlg.other.request({
method: 'REFER',
headers: {
'Refer-To': req.get('Refer-To'),
'Referred-By': req.get('Referred-By'),
'User-Agent': req.get('User-Agent')
'User-Agent': req.get('User-Agent'),
...customHeaders
}
});
res.send(response.status, response.reason);
@@ -805,16 +833,44 @@ Duration=${payload.duration} `
try {
const reason = req.get('X-Reason');
const isReleasingMedia = reason && dlg.type === 'uas' && ['release-media', 'anchor-media'].includes(reason);
const isFullMediaRelease = reason === 'release-media-entirely' && process.env.JAMBONES_ENABLE_FULL_MEDIA_RELEASE;
const fromTag = dlg.type === 'uas' ? this.rtpEngineOpts.uas.tag : this.rtpEngineOpts.uac.tag;
const toTag = dlg.type === 'uas' ? this.rtpEngineOpts.uac.tag : this.rtpEngineOpts.uas.tag;
const offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
const answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
let offerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uac.mediaOpts : this.rtpEngineOpts.uas.mediaOpts;
let answerMedia = dlg.type === 'uas' ? this.rtpEngineOpts.uas.mediaOpts : this.rtpEngineOpts.uac.mediaOpts;
const direction = dlg.type === 'uas' ? ['private', 'public'] : ['public', 'private'];
if (isFullMediaRelease) {
const a_sdp = await this.retrieveKey(makePartnerFullMediaReleaseKey(this.req.get('X-CID')));
this.logger.info({a_sdp}, 'reinvite ourselves out of the media path with this reinvite offer');
const answerSdp = await dlg.other.modify(a_sdp);
this.logger.info({answerSdp}, 'far end response to full media release');
res.send(200, {
body: dlg.local.sdp,
headers: {
'Contact': this.contactHeader
}
});
/* no media going through us now we can destroy the rtpengine resource */
this.rtpEngineResource.destroy().catch((err) => {
this.logger.info({err}, 'Error destroying rtpengine resource after full media release');
});
this._mediaPath = MediaPath.NoMedia;
return;
}
if (isReleasingMedia) {
if (!offerMedia.flags.includes('port latching')) offerMedia.flags.push('port latching');
if (!offerMedia.flags.includes('asymmetric')) offerMedia.flags.push('asymmetric');
offerMedia.flags = offerMedia.flags.filter((f) => f !== 'media handover');
}
const acceptCodecs = process.env.JAMBONES_ACCEPT_AND_TRANSCODE ?
process.env.JAMBONES_ACCEPT_AND_TRANSCODE :
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' }})};
let opts = {
...this.rtpEngineOpts.common,
...offerMedia,
@@ -835,17 +891,20 @@ Duration=${payload.duration} `
/* if this is a re-invite from the FS to change media anchoring, avoid sending the reinvite out */
let sdp;
if (isReleasingMedia && !this.calleeIsUsingSrtp) {
//HL 2024-11-13: previously forwarded re-invites to webrtc clients but further testing has shown to be unnecessary
//if (isReleasingMedia && !this.calleeIsUsingSrtp) {
//DH 2024-11- 18: if we are going from no-media to either partial or full media, we need reinvite the far end
if (isReleasingMedia && this._mediaPath !== MediaPath.NoMedia) {
this.logger.info(`got a reinvite from FS to ${reason}`);
sdp = dlg.other.remote.sdp;
if (!answerMedia.flags.includes('port latching')) answerMedia.flags.push('port latching');
if (!answerMedia.flags.includes('asymmetric')) answerMedia.flags.push('asymmetric');
answerMedia.flags = answerMedia.flags.filter((f) => f !== 'media handover');
this._mediaReleased = 'release-media' === reason;
this._mediaPath = 'release-media' === reason ? MediaPath.PartialMedia : MediaPath.FullMedia;
}
else {
sdp = await dlg.other.modify(response.sdp);
this.logger.info({sdp}, 'CallSession:_onReinvite: got sdp from 200 OK to invite we sent');
}
opts = {
...this.rtpEngineOpts.common,
@@ -1094,7 +1153,7 @@ Duration=${payload.duration} `
// eslint-disable-next-line no-unused-vars
const { via, from, to, 'call-id': callid, cseq, 'max-forwards': maxforwards,
// eslint-disable-next-line no-unused-vars
'content-length': _contentlength, 'refer-to': _referto, 'referred-by': _referredby,
'content-length': _contentlength, 'refer-to': _referto, 'referred-by': referredby,
// eslint-disable-next-line no-unused-vars
'X-Refer-To-Leave-Untouched': _leave,
...customHeaders
@@ -1106,9 +1165,7 @@ Duration=${payload.duration} `
const arr = /context-(.*)/.exec(uri.user);
if (!arr) {
/* call transfer requested */
const referredBy = req.getParsedHeader('Referred-By');
if (!referredBy) return res.send(400);
const u = parseUri(referredBy.uri);
if (!req.has('Referred-By')) return res.send(400);
/* delete contact if it was there from feature server */
delete customHeaders['contact'];
@@ -1117,7 +1174,7 @@ Duration=${payload.duration} `
method: 'REFER',
headers: {
'Refer-To': `<${stringifyUri(uri)}>`,
'Referred-By': `<${stringifyUri(u)}>`,
'Referred-By': referredby,
...customHeaders
}
});
+7
View File
@@ -0,0 +1,7 @@
{
"MediaPath": {
"NoMedia": "no-media",
"PartialMedia": "partial-media",
"FullMedia": "full-media"
}
}
+3 -1
View File
@@ -106,7 +106,9 @@ module.exports = (srf, logger, redisClient) => {
try {
/* decrement count if INVITE is later rejected */
res.once('end', async({status}) => {
if (status > 200) {
if (status > 200 && !req.locals.callCountNudged) {
req.locals.callCountNudged = true;
logger.debug('decrementing call count due to call rejection');
nudgeCallCounts(logger, {
service_provider_sid,
account_sid,
+15 -2
View File
@@ -40,7 +40,11 @@ function makeRtpEngineOpts(req, srcIsUsingSrtp, dstIsUsingSrtp, padCrypto, teams
},
uac: {
tag: null,
mediaOpts: dstOpts
mediaOpts: {
...dstOpts,
...(process.env.JAMBONES_CODEC_OFFER_WITH_ORDER &&
{ codec: { offer: process.env.JAMBONES_CODEC_OFFER_WITH_ORDER.split(','), strip: 'all' } }),
}
}
};
}
@@ -229,6 +233,13 @@ async function isBlackListedSipGateway(client, logger, sip_gateway_sid) {
}
}
const makeFullMediaReleaseKey = (callId) => {
return `b_sdp:${callId}`;
};
const makePartnerFullMediaReleaseKey = (callId) => {
return `a_sdp:${callId}`;
};
module.exports = {
makeRtpEngineOpts,
selectHostPort,
@@ -240,5 +251,7 @@ module.exports = {
createHealthCheckApp,
nudgeCallCounts,
isPrivateVoipNetwork,
isBlackListedSipGateway
isBlackListedSipGateway,
makeFullMediaReleaseKey,
makePartnerFullMediaReleaseKey
};
+5 -4
View File
@@ -14,7 +14,7 @@
"@jambonz/mw-registrar": "0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.9",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.7",
"@jambonz/siprec-client-utils": "^0.2.9",
"@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.10",
"cidr-matcher": "^2.1.1",
@@ -628,9 +628,10 @@
}
},
"node_modules/@jambonz/siprec-client-utils": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@jambonz/siprec-client-utils/-/siprec-client-utils-0.2.7.tgz",
"integrity": "sha512-VztOToBfXnOphg/y6kO+lBYqoDH7X5Ci0daXT5zQ8v5NnaZw+UwxGeFPX3knIR3r5pmzUld0f2giFOuD4ZOF8w==",
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@jambonz/siprec-client-utils/-/siprec-client-utils-0.2.9.tgz",
"integrity": "sha512-C/1UOrAvQVG0iwiSMS0MC9VHRjhg3/sOjNna+eEjHjws1SHl8NrKBoDL8uOwLqOM2ccpbqCPH9GJAxo5OnOhww==",
"license": "MIT",
"dependencies": {
"sdp-transform": "^2.14.1",
"uuid": "^8.3.2"
+1 -1
View File
@@ -32,7 +32,7 @@
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "0.2.7",
"@jambonz/rtpengine-utils": "^0.4.4",
"@jambonz/siprec-client-utils": "^0.2.7",
"@jambonz/siprec-client-utils": "^0.2.9",
"@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.10",
"cidr-matcher": "^2.1.1",
+1 -1
View File
@@ -24,7 +24,7 @@ services:
ipv4_address: 172.39.0.2
sbc:
image: drachtio/drachtio-server:latest
image: drachtio/drachtio-server:0.8.26
command: drachtio --contact "sip:*;transport=udp" --loglevel debug --sofia-loglevel 9
ports:
- "9060:9022/tcp"