mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-10 00:13:41 +00:00
Compare commits
31 Commits
v0.8.5-rc1
...
feat/aws-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c71a58dcb5 | ||
|
|
8022f9d16c | ||
|
|
c4dcb051be | ||
|
|
29dc2f7052 | ||
|
|
7718f50877 | ||
|
|
11283edf6f | ||
|
|
6043921067 | ||
|
|
8ad947c0fd | ||
|
|
63c925c731 | ||
|
|
e8647b2b55 | ||
|
|
dab83423cf | ||
|
|
864a673ea0 | ||
|
|
86a14daf79 | ||
|
|
c66ad39001 | ||
|
|
0a0cbd57ba | ||
|
|
eb2d90ffaa | ||
|
|
454ff7d1b8 | ||
|
|
7e349fe4e5 | ||
|
|
9478f3a1b8 | ||
|
|
a3c241b569 | ||
|
|
5a68563f96 | ||
|
|
1cdd0cf611 | ||
|
|
9ae4b04fc5 | ||
|
|
170c3c7ec4 | ||
|
|
7c36a08852 | ||
|
|
633237da1b | ||
|
|
708c2c661f | ||
|
|
87632c549e | ||
|
|
31559cbb3b | ||
|
|
1156bae2de | ||
|
|
c6c599ab99 |
@@ -1,4 +1,4 @@
|
||||
# jambonz-feature-server 
|
||||
# jambonz-feature-server [](https://github.com/jambonz/jambonz-feature-server/actions/workflows/build.yml)
|
||||
|
||||
This application implements the core feature server of the jambones platform.
|
||||
|
||||
@@ -40,6 +40,8 @@ Configuration is provided via environment variables:
|
||||
|JAMBONZ_RECORD_WS_BASE_URL| recording websocket URL to send the recording audio|no|
|
||||
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|
||||
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
|
||||
|ANCHOR_MEDIA_ALWAYS| keep media on media server|no|
|
||||
|JAMBONZ_DISABLE_DIAL_PAI_HEADER| control P-Asserted-Identity header on B-Leg|no|
|
||||
|
||||
### 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:
|
||||
|
||||
0
bin/k8s-pre-stop-hook.js
Executable file → Normal file
0
bin/k8s-pre-stop-hook.js
Executable file → Normal file
@@ -28,10 +28,6 @@ const JAMBONES_MYSQL_PORT = parseInt(process.env.JAMBONES_MYSQL_PORT, 10) || 330
|
||||
const JAMBONES_MYSQL_REFRESH_TTL = parseInt(process.env.JAMBONES_MYSQL_REFRESH_TTL, 10) || 0;
|
||||
const JAMBONES_MYSQL_CONNECTION_LIMIT = parseInt(process.env.JAMBONES_MYSQL_CONNECTION_LIMIT, 10) || 10;
|
||||
|
||||
/* redis */
|
||||
const JAMBONES_REDIS_HOST = process.env.JAMBONES_REDIS_HOST;
|
||||
const JAMBONES_REDIS_PORT = parseInt(process.env.JAMBONES_REDIS_PORT, 10) || 6379;
|
||||
|
||||
/* gather and hints */
|
||||
const JAMBONES_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONES_GATHER_EARLY_HINTS_MATCH;
|
||||
const JAMBONZ_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH;
|
||||
@@ -127,26 +123,6 @@ const HTTP_PROXY_PROTOCOL = process.env.JAMBONES_HTTP_PROXY_PROTOCOL || 'http';
|
||||
|
||||
const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) || 30000;
|
||||
|
||||
const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
|
||||
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
|
||||
let host, port = 26379;
|
||||
if (sentinel.includes(':')) {
|
||||
const arr = sentinel.split(':');
|
||||
host = arr[0];
|
||||
port = parseInt(arr[1], 10);
|
||||
} else {
|
||||
host = sentinel;
|
||||
}
|
||||
return {host, port};
|
||||
}),
|
||||
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
|
||||
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
|
||||
}),
|
||||
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
|
||||
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
|
||||
})
|
||||
} : null;
|
||||
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL || process.env.JAMBONES_RECORD_WS_BASE_URL;
|
||||
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
|
||||
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
|
||||
@@ -170,9 +146,6 @@ module.exports = {
|
||||
JAMBONZ_GATHER_EARLY_HINTS_MATCH,
|
||||
JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS,
|
||||
JAMBONES_FREESWITCH,
|
||||
JAMBONES_REDIS_HOST,
|
||||
JAMBONES_REDIS_PORT,
|
||||
JAMBONES_REDIS_SENTINELS,
|
||||
SMPP_URL,
|
||||
JAMBONES_NETWORK_CIDR,
|
||||
JAMBONES_API_BASE_URL,
|
||||
|
||||
@@ -5,7 +5,7 @@ const CallInfo = require('../../session/call-info');
|
||||
const {CallDirection, CallStatus} = require('../../utils/constants');
|
||||
const uuidv4 = require('uuid-random');
|
||||
const SipError = require('drachtio-srf').SipError;
|
||||
const { validationResult } = require('express-validator');
|
||||
const { validationResult, body } = require('express-validator');
|
||||
const { validate } = require('@jambonz/verb-specifications');
|
||||
const sysError = require('./error');
|
||||
const HttpRequestor = require('../../utils/http-requestor');
|
||||
@@ -13,7 +13,7 @@ const WsRequestor = require('../../utils/ws-requestor');
|
||||
const RootSpan = require('../../utils/call-tracer');
|
||||
const dbUtils = require('../../utils/db-utils');
|
||||
const { mergeSdpMedia, extractSdpMedia } = require('../../utils/sdp-utils');
|
||||
const { createCallSchema } = require('../schemas/create-call');
|
||||
const { createCallSchema, customSanitizeFunction } = require('../schemas/create-call');
|
||||
|
||||
const removeNullProperties = (obj) => (Object.keys(obj).forEach((key) => obj[key] === null && delete obj[key]), obj);
|
||||
const removeNulls = (req, res, next) => {
|
||||
@@ -24,6 +24,12 @@ const removeNulls = (req, res, next) => {
|
||||
router.post('/',
|
||||
removeNulls,
|
||||
createCallSchema,
|
||||
body('tag').custom((value) => {
|
||||
if (value) {
|
||||
customSanitizeFunction(value);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
async(req, res) => {
|
||||
const {logger} = req.app.locals;
|
||||
const errors = validationResult(req);
|
||||
|
||||
@@ -45,7 +45,7 @@ function retrieveCallSession(callSid, opts) {
|
||||
router.post('/:callSid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const callSid = req.params.callSid;
|
||||
logger.debug({body: req.body}, 'got upateCall request');
|
||||
logger.debug({body: req.body}, 'got updateCall request');
|
||||
try {
|
||||
const cs = retrieveCallSession(callSid, req.body);
|
||||
if (!cs) {
|
||||
|
||||
@@ -46,11 +46,6 @@ const createCallSchema = checkSchema({
|
||||
optional: true,
|
||||
errorMessage: 'Invalid tag',
|
||||
},
|
||||
'tag.*': {
|
||||
trim: true,
|
||||
escape: true,
|
||||
stripLow: true,
|
||||
},
|
||||
app_json: {
|
||||
isString: true,
|
||||
optional: true,
|
||||
@@ -109,6 +104,34 @@ const createCallSchema = checkSchema({
|
||||
}
|
||||
}, ['body']);
|
||||
|
||||
module.exports = {
|
||||
createCallSchema
|
||||
const customSanitizeFunction = (value) => {
|
||||
try {
|
||||
if (Array.isArray(value)) {
|
||||
value = value.map((item) => customSanitizeFunction(item));
|
||||
} else if (typeof value === 'object') {
|
||||
Object.keys(value).forEach((key) => {
|
||||
value[key] = customSanitizeFunction(value[key]);
|
||||
});
|
||||
} else if (typeof value === 'string') {
|
||||
/* trims characters at the beginning and at the end of a string */
|
||||
value = value.trim();
|
||||
|
||||
/* We don't escape URLs but verify them via new URL */
|
||||
if (value.includes('http')) {
|
||||
value = new URL(value).toString();
|
||||
} else {
|
||||
/* replaces <, >, &, ', " and / with their corresponding HTML entities */
|
||||
value = escape(value);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
value = `Error: ${error.message}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createCallSchema,
|
||||
customSanitizeFunction
|
||||
};
|
||||
|
||||
@@ -23,7 +23,8 @@ module.exports = function(srf, logger) {
|
||||
lookupAppBySid,
|
||||
lookupAppByRealm,
|
||||
lookupAppByTeamsTenant,
|
||||
registrar
|
||||
registrar,
|
||||
lookupClientByAccountAndUsername
|
||||
} = srf.locals.dbHelpers;
|
||||
const {
|
||||
writeAlerts,
|
||||
@@ -48,10 +49,17 @@ module.exports = function(srf, logger) {
|
||||
const account_sid = req.get('X-Account-Sid');
|
||||
req.locals = {callSid, account_sid, callId};
|
||||
|
||||
if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User');
|
||||
let clientDb = null;
|
||||
if (req.has('X-Authenticated-User')) {
|
||||
req.locals.originatingUser = req.get('X-Authenticated-User');
|
||||
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
|
||||
if (arr) {
|
||||
[clientDb] = await lookupClientByAccountAndUsername(account_sid, arr[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// check for call to application
|
||||
if (uri.user?.startsWith('app-') && req.locals.originatingUser) {
|
||||
if (uri.user?.startsWith('app-') && req.locals.originatingUser && clientDb?.allow_direct_app_calling) {
|
||||
const application_sid = uri.user.match(/app-(.*)/)[1];
|
||||
logger.debug(`got application from Request URI header: ${application_sid}`);
|
||||
req.locals.application_sid = application_sid;
|
||||
@@ -61,13 +69,13 @@ module.exports = function(srf, logger) {
|
||||
req.locals.application_sid = application_sid;
|
||||
}
|
||||
// check for call to queue
|
||||
if (uri.user?.startsWith('queue-') && req.locals.originatingUser) {
|
||||
if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) {
|
||||
const queue_name = uri.user.match(/queue-(.*)/)[1];
|
||||
logger.debug(`got Queue from Request URI header: ${queue_name}`);
|
||||
req.locals.queue_name = queue_name;
|
||||
}
|
||||
// check for call to registered user
|
||||
if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser) {
|
||||
if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) {
|
||||
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
|
||||
if (arr) {
|
||||
const sipRealm = arr[2];
|
||||
|
||||
@@ -19,6 +19,7 @@ class AdultingCallSession extends CallSession {
|
||||
rootSpan
|
||||
});
|
||||
this.sd = singleDialer;
|
||||
this.req = callInfo.req;
|
||||
|
||||
this.sd.dlg.on('destroy', () => {
|
||||
this.logger.info('AdultingCallSession: called party hung up');
|
||||
|
||||
@@ -833,13 +833,19 @@ class CallSession extends Emitter {
|
||||
} else if ('elevenlabs' === vendor) {
|
||||
return {
|
||||
api_key: credential.api_key,
|
||||
model_id: credential.model_id
|
||||
model_id: credential.model_id,
|
||||
options: credential.options
|
||||
};
|
||||
} else if ('assemblyai' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key
|
||||
};
|
||||
} else if ('whisper' === vendor) {
|
||||
return {
|
||||
api_key: credential.api_key,
|
||||
model_id: credential.model_id
|
||||
};
|
||||
} else if (vendor.startsWith('custom:')) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
@@ -1116,6 +1122,21 @@ class CallSession extends Emitter {
|
||||
transcribeTask.updateTranscribe(opts.transcribe_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- update customer data
|
||||
* @param {object} opts
|
||||
* @param {object} opts.tag - customer data
|
||||
*/
|
||||
_lccTag(opts) {
|
||||
const {tag} = opts;
|
||||
if (typeof tag !== 'object' || Array.isArray(tag) || tag === null) {
|
||||
this.logger.info('CallSession:_lccTag - invalid tag data');
|
||||
return;
|
||||
}
|
||||
this.logger.debug({customerData: tag}, 'CallSession:_lccTag set customer data in callInfo');
|
||||
this.callInfo.customerData = tag;
|
||||
}
|
||||
|
||||
async _lccMuteStatus(callSid, mute) {
|
||||
// this whole thing requires us to be in a Dial or Conference verb
|
||||
const task = this.currentTask;
|
||||
@@ -1166,6 +1187,38 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control - send RFC 2833 DTMF
|
||||
* @param {obj} opts
|
||||
* @param {string} opts.dtmf.digit - DTMF digit
|
||||
* @param {string} opts.dtmf.duration - Optional, Duration
|
||||
*/
|
||||
async _lccDtmf(opts, callSid) {
|
||||
const {dtmf} = opts;
|
||||
const {digit, duration = 250} = dtmf;
|
||||
if (!this.hasStableDialog) {
|
||||
this.logger.info('CallSession:_lccDtmf - invalid command as we do not have a stable call');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dlg = callSid === this.callSid ? this.dlg : this.currentTask.dlg;
|
||||
const res = await dlg.request({
|
||||
method: 'INFO',
|
||||
headers: {
|
||||
'Content-Type': 'application/dtmf',
|
||||
'X-Reason': 'Dtmf'
|
||||
},
|
||||
body: `Signal=${digit}
|
||||
Duration=${duration} `
|
||||
});
|
||||
this.logger.debug({res}, `CallSession:_lccDtmf
|
||||
got response to INFO DTMF digit=${digit} and duration=${duration}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'CallSession:_lccDtmf - error sending INFO RFC 2833 DTMF');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- whisper to one party or the other on a call
|
||||
* @param {array} opts - array of play or say tasks
|
||||
@@ -1220,7 +1273,7 @@ class CallSession extends Emitter {
|
||||
/**
|
||||
* perform live call control
|
||||
* @param {object} opts - update instructions
|
||||
* @param {string} callSid - identifies call toupdate
|
||||
* @param {string} callSid - identifies call to update
|
||||
*/
|
||||
async updateCall(opts, callSid) {
|
||||
this.logger.debug(opts, 'CallSession:updateCall');
|
||||
@@ -1249,13 +1302,18 @@ class CallSession extends Emitter {
|
||||
else if (opts.sip_request) {
|
||||
const res = await this._lccSipRequest(opts, callSid);
|
||||
return {status: res.status, reason: res.reason};
|
||||
} else if (opts.dtmf) {
|
||||
await this._lccDtmf(opts, callSid);
|
||||
}
|
||||
else if (opts.record) {
|
||||
await this.notifyRecordOptions(opts.record);
|
||||
}
|
||||
else if (opts.tag) {
|
||||
return this._lccTag(opts);
|
||||
}
|
||||
|
||||
// whisper may be the only thing we are asked to do, or it may that
|
||||
// we are doing a whisper after having muted, paused reccording etc..
|
||||
// we are doing a whisper after having muted, paused recording etc..
|
||||
if (opts.whisper) {
|
||||
return this._lccWhisper(opts, callSid);
|
||||
}
|
||||
@@ -1428,6 +1486,13 @@ class CallSession extends Emitter {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'dtmf':
|
||||
this._lccDtmf(data, call_sid)
|
||||
.catch((err) => {
|
||||
this.logger.info({err, data}, `CallSession:_onCommand - error sending RFC 2833 DTMF ${data}`);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const dbUtils = require('../utils/db-utils');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config');
|
||||
const { isOnhold } = require('../utils/sdp-utils');
|
||||
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
function parseDtmfOptions(logger, dtmfCapture) {
|
||||
@@ -488,7 +488,8 @@ class TaskDial extends Task {
|
||||
headers: this.headers,
|
||||
proxy: `sip:${sbcAddress}`,
|
||||
callingNumber: this.callerId || req.callingNumber,
|
||||
...(this.callerName && {callingName: this.callerName})
|
||||
...(this.callerName && {callingName: this.callerName}),
|
||||
opusFirst: isOpusFirst(this.cs.ep.remote.sdp)
|
||||
};
|
||||
|
||||
const t = this.target.find((t) => t.type === 'teams');
|
||||
@@ -790,9 +791,11 @@ class TaskDial extends Task {
|
||||
assert(cs.ep && sd.ep);
|
||||
|
||||
try {
|
||||
// Wait until we got new SDP from B leg to ofter to A Leg
|
||||
const aLegSdp = cs.ep.remote.sdp;
|
||||
await sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp);
|
||||
const bLegSdp = sd.dlg.remote.sdp;
|
||||
await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)];
|
||||
await cs.releaseMediaToSBC(bLegSdp);
|
||||
this.epOther = null;
|
||||
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
|
||||
} catch (err) {
|
||||
|
||||
@@ -58,13 +58,13 @@ class Dialogflow extends Task {
|
||||
this.vendor = this.data.tts.vendor || 'default';
|
||||
this.language = this.data.tts.language || 'default';
|
||||
this.voice = this.data.tts.voice || 'default';
|
||||
this.speechSynthesisLabel = this.data.tts.label || 'default';
|
||||
this.speechSynthesisLabel = this.data.tts.label;
|
||||
|
||||
// fallback tts
|
||||
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
|
||||
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
|
||||
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
|
||||
this.fallbackLabel = this.data.tts.fallbackLabel || 'default';
|
||||
this.fallbackLabel = this.data.tts.fallbackLabel;
|
||||
}
|
||||
this.bargein = this.data.bargein;
|
||||
}
|
||||
|
||||
@@ -528,7 +528,9 @@ class TaskGather extends SttTask {
|
||||
this._clearTimer();
|
||||
this._timeoutTimer = setTimeout(() => {
|
||||
if (this.isContinuousAsr) this._startAsrTimer();
|
||||
else this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||
else if (this.interDigitTimeout <= 0 || this.digitBuffer.length < this.minDigits || this.needsStt) {
|
||||
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||
}
|
||||
}, this.timeout);
|
||||
}
|
||||
|
||||
@@ -822,13 +824,14 @@ class TaskGather extends SttTask {
|
||||
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
|
||||
}
|
||||
this.logger.info({evt}, 'TaskGather:_onJambonzError');
|
||||
const errMessage = evt.error || evt.Message;
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
|
||||
message: `Speech vendor ${this.vendor} error: ${errMessage}`,
|
||||
vendor: this.vendor,
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
|
||||
this.notifyError({msg: 'ASR error', details:`Speech vendor ${this.vendor} error: ${evt.error}`});
|
||||
}
|
||||
|
||||
_onVendorConnectFailure(cs, _ep, evt) {
|
||||
|
||||
@@ -16,6 +16,7 @@ class TaskRestDial extends Task {
|
||||
this.to = this.data.to;
|
||||
this.call_hook = this.data.call_hook;
|
||||
this.timeout = this.data.timeout || 60;
|
||||
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
|
||||
|
||||
this.on('connect', this._onConnect.bind(this));
|
||||
this.on('callStatus', this._onCallStatus.bind(this));
|
||||
@@ -64,7 +65,7 @@ class TaskRestDial extends Task {
|
||||
const cs = this.callSession;
|
||||
cs.setDialog(dlg);
|
||||
this.logger.debug('TaskRestDial:_onConnect - call connected');
|
||||
|
||||
if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg);
|
||||
try {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
@@ -142,6 +143,16 @@ class TaskRestDial extends Task {
|
||||
this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook');
|
||||
});
|
||||
}
|
||||
|
||||
_initSipRequestWithinDialogHandler(cs, dlg) {
|
||||
cs.sipRequestWithinDialogHook = this.sipRequestWithinDialogHook;
|
||||
dlg.on('info', this._onRequestWithinDialog.bind(this, cs));
|
||||
dlg.on('message', this._onRequestWithinDialog.bind(this, cs));
|
||||
}
|
||||
|
||||
async _onRequestWithinDialog(cs, req, res) {
|
||||
cs._onRequestWithinDialog(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskRestDial;
|
||||
|
||||
@@ -86,6 +86,13 @@ class TaskSay extends Task {
|
||||
credentials.api_key = this.options.apiKey || credentials.apiKey;
|
||||
credentials.region = this.options.region || credentials.region;
|
||||
voice = this.options.voice || voice;
|
||||
} else if (vendor === 'elevenlabs') {
|
||||
credentials = credentials || {};
|
||||
credentials.model_id = this.options.model_id || credentials.model_id;
|
||||
credentials.voice_settings = this.options.voice_settings || {};
|
||||
credentials.optimize_streaming_latency = this.options.optimize_streaming_latency
|
||||
|| credentials.optimize_streaming_latency;
|
||||
voice = this.options.voice_id || voice;
|
||||
}
|
||||
|
||||
this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
|
||||
@@ -127,6 +134,7 @@ class TaskSay extends Task {
|
||||
model,
|
||||
salt,
|
||||
credentials,
|
||||
options: this.options,
|
||||
disableTtsCache : this.disableTtsCache
|
||||
});
|
||||
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
|
||||
|
||||
@@ -53,7 +53,7 @@ class SttTask extends Task {
|
||||
}
|
||||
|
||||
async _initSpeechCredentials(cs, vendor, label) {
|
||||
const {getNuanceAccessToken, getIbmAccessToken} = this.cs.srf.locals.dbHelpers;
|
||||
const {getNuanceAccessToken, getIbmAccessToken, getAwsAuthToken} = this.cs.srf.locals.dbHelpers;
|
||||
let credentials = cs.getSpeechCredentials(vendor, 'stt', label);
|
||||
|
||||
if (!credentials) {
|
||||
@@ -87,6 +87,15 @@ class SttTask extends Task {
|
||||
this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
credentials = {...credentials, access_token, stt_region};
|
||||
}
|
||||
else if (vendor == 'aws') {
|
||||
/* get AWS access token */
|
||||
const {accessKeyId, secretAccessKey, securityToken, region } = credentials;
|
||||
if (!securityToken) {
|
||||
const { servedFromCache, ...newCredentials} = await getAwsAuthToken(accessKeyId, secretAccessKey, region);
|
||||
this.logger.debug({newCredentials}, `got aws security token ${servedFromCache ? 'from cache' : ''}`);
|
||||
credentials = {...newCredentials, region};
|
||||
}
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class TaskTag extends Task {
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
cs.callInfo.customerData = this.data;
|
||||
//this.logger.debug({callInfo: cs.callInfo.toJSON()}, 'TaskTag:exec set customer data in callInfo');
|
||||
this.logger.debug({customerData: cs.callInfo.customerData}, 'TaskTag:exec set customer data in callInfo');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,10 +219,6 @@ class TaskTranscribe extends SttTask {
|
||||
this.bugname = 'nuance_transcribe';
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
|
||||
this._onStartOfSpeech.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
|
||||
this._onTranscriptionComplete.bind(this, cs, ep, channel));
|
||||
break;
|
||||
case 'deepgram':
|
||||
this.bugname = 'deepgram_transcribe';
|
||||
@@ -286,12 +282,6 @@ class TaskTranscribe extends SttTask {
|
||||
this.bugname = 'nvidia_transcribe';
|
||||
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
|
||||
this._onStartOfSpeech.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
|
||||
this._onTranscriptionComplete.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
|
||||
this._onVadDetected.bind(this, cs, ep));
|
||||
break;
|
||||
|
||||
case 'assemblyai':
|
||||
@@ -309,9 +299,9 @@ class TaskTranscribe extends SttTask {
|
||||
this.bugname = `${this.vendor}_transcribe`;
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
|
||||
this._onJambonzConnectFailure.bind(this, cs, ep));
|
||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||
break;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -91,9 +91,14 @@ const speechMapper = (cred) => {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.options = o.options;
|
||||
} else if ('assemblyai' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
} else if ('whisper' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
} else if (obj.vendor.startsWith('custom:')) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.auth_token = o.auth_token;
|
||||
|
||||
@@ -8,9 +8,6 @@ const {
|
||||
JAMBONES_MYSQL_CONNECTION_LIMIT,
|
||||
JAMBONES_MYSQL_PORT,
|
||||
JAMBONES_FREESWITCH,
|
||||
JAMBONES_REDIS_HOST,
|
||||
JAMBONES_REDIS_PORT,
|
||||
JAMBONES_REDIS_SENTINELS,
|
||||
SMPP_URL,
|
||||
JAMBONES_TIME_SERIES_HOST,
|
||||
JAMBONES_ESL_LISTEN_ADDRESS,
|
||||
@@ -140,7 +137,8 @@ function installSrfLocals(srf, logger) {
|
||||
lookupTeamsByAccount,
|
||||
lookupAccountBySid,
|
||||
lookupAccountCapacitiesBySid,
|
||||
lookupSmppGateways
|
||||
lookupSmppGateways,
|
||||
lookupClientByAccountAndUsername
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: JAMBONES_MYSQL_HOST,
|
||||
user: JAMBONES_MYSQL_USER,
|
||||
@@ -174,19 +172,14 @@ function installSrfLocals(srf, logger) {
|
||||
retrieveByPatternSortedSet,
|
||||
sortedSetLength,
|
||||
sortedSetPositionByPattern
|
||||
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: JAMBONES_REDIS_HOST,
|
||||
port: JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
} = require('@jambonz/realtimedb-helpers')({}, logger, tracer);
|
||||
const registrar = new Registrar(logger, client);
|
||||
const {
|
||||
synthAudio,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken,
|
||||
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
|
||||
host: JAMBONES_REDIS_HOST,
|
||||
port: JAMBONES_REDIS_PORT || 6379
|
||||
}, logger, tracer);
|
||||
getAwsAuthToken
|
||||
} = require('@jambonz/speech-utils')({redis_client: client}, logger);
|
||||
const {
|
||||
writeAlerts,
|
||||
AlertType
|
||||
@@ -217,6 +210,7 @@ function installSrfLocals(srf, logger) {
|
||||
lookupAccountBySid,
|
||||
lookupAccountCapacitiesBySid,
|
||||
lookupSmppGateways,
|
||||
lookupClientByAccountAndUsername,
|
||||
updateCallStatus,
|
||||
retrieveCall,
|
||||
listCalls,
|
||||
@@ -238,6 +232,7 @@ function installSrfLocals(srf, logger) {
|
||||
getListPosition,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken,
|
||||
getAwsAuthToken,
|
||||
addToSortedSet,
|
||||
retrieveFromSortedSet,
|
||||
retrieveByPatternSortedSet,
|
||||
|
||||
@@ -13,6 +13,9 @@ const moment = require('moment');
|
||||
const stripCodecs = require('./strip-ancillary-codecs');
|
||||
const RootSpan = require('./call-tracer');
|
||||
const uuidv4 = require('uuid-random');
|
||||
const HttpRequestor = require('./http-requestor');
|
||||
const WsRequestor = require('./ws-requestor');
|
||||
const {makeOpusFirst} = require('./sdp-utils');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) {
|
||||
@@ -76,7 +79,8 @@ class SingleDialer extends Emitter {
|
||||
...(this.from.host && {'X-Preferred-From-Host': this.from.host}),
|
||||
'X-Jambonz-Routing': this.target.type,
|
||||
'X-Call-Sid': this.callSid,
|
||||
...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
|
||||
...(this.applicationSid && {'X-Application-Sid': this.applicationSid}),
|
||||
...(this.target.proxy && {'X-SIP-Proxy': this.target.proxy})
|
||||
};
|
||||
if (srf.locals.fsUUID) {
|
||||
opts.headers = {
|
||||
@@ -148,7 +152,7 @@ class SingleDialer extends Emitter {
|
||||
|
||||
Object.assign(opts, {
|
||||
proxy: `sip:${this.sbcAddress}`,
|
||||
localSdp: this.ep.local.sdp
|
||||
localSdp: opts.opusFirst ? makeOpusFirst(this.ep.local.sdp) : this.ep.local.sdp
|
||||
});
|
||||
if (this.target.auth) opts.auth = this.target.auth;
|
||||
inviteSpan = this.startSpan('invite', {
|
||||
@@ -176,6 +180,7 @@ class SingleDialer extends Emitter {
|
||||
* (a) create a logger for this call
|
||||
*/
|
||||
req.srf = srf;
|
||||
this.req = req;
|
||||
this.callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
parentCallInfo: this.parentCallInfo,
|
||||
@@ -334,6 +339,7 @@ class SingleDialer extends Emitter {
|
||||
// verify it contains only allowed verbs
|
||||
const allowedTasks = tasks.filter((task) => {
|
||||
return [
|
||||
TaskPreconditions.None,
|
||||
TaskPreconditions.StableCall,
|
||||
TaskPreconditions.Endpoint
|
||||
].includes(task.preconditions);
|
||||
@@ -381,15 +387,35 @@ class SingleDialer extends Emitter {
|
||||
this.dlg.linkedSpanId = this.rootSpan.traceId;
|
||||
const rootSpan = new RootSpan('outbound-call', this.dlg);
|
||||
const newLogger = logger.child({traceId: rootSpan.traceId});
|
||||
//clone application from parent call with new requestor
|
||||
//parrent application will be closed in case the parent hangup
|
||||
const app = {...application};
|
||||
if ('WS' === app.call_hook?.method ||
|
||||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
|
||||
const requestor = new WsRequestor(logger, this.accountInfo.account.account_sid,
|
||||
app.call_hook, this.accountInfo.account.webhook_secret);
|
||||
app.requestor = requestor;
|
||||
app.notifier = requestor;
|
||||
app.call_hook.method = 'WS';
|
||||
}
|
||||
else {
|
||||
app.requestor = new HttpRequestor(logger, this.accountInfo.account.account_sid,
|
||||
app.call_hook, this.accountInfo.account.webhook_secret);
|
||||
if (app.call_status_hook) app.notifier = new HttpRequestor(logger,
|
||||
this.accountInfo.account.account_sid, app.call_status_hook,
|
||||
this.accountInfo.account.webhook_secret);
|
||||
else app.notifier = {request: () => {}, close: () => {}};
|
||||
}
|
||||
const cs = new AdultingCallSession({
|
||||
logger: newLogger,
|
||||
singleDialer: this,
|
||||
application,
|
||||
application: app,
|
||||
callInfo: this.callInfo,
|
||||
accountInfo: this.accountInfo,
|
||||
tasks,
|
||||
rootSpan
|
||||
});
|
||||
cs.req = this.req;
|
||||
cs.exec().catch((err) => newLogger.error({err}, 'doAdulting: error executing session'));
|
||||
return cs;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,30 @@ const mergeSdpMedia = (sdp1, sdp2) => {
|
||||
return sdpTransform.write(parsedSdp1);
|
||||
};
|
||||
|
||||
const getCodecPlacement = (parsedSdp, codec) => parsedSdp?.media[0]?.rtp?.findIndex((e) => e.codec === codec);
|
||||
|
||||
const isOpusFirst = (sdp) => {
|
||||
return getCodecPlacement(sdpTransform.parse(sdp), 'opus') === 0;
|
||||
};
|
||||
|
||||
const makeOpusFirst = (sdp) => {
|
||||
const parsedSdp = sdpTransform.parse(sdp);
|
||||
// Find the index of the OPUS codec
|
||||
const opusIndex = getCodecPlacement(parsedSdp, 'opus');
|
||||
|
||||
// Move OPUS codec to the beginning
|
||||
if (opusIndex > 0) {
|
||||
const opusEntry = parsedSdp.media[0].rtp.splice(opusIndex, 1)[0];
|
||||
parsedSdp.media[0].rtp.unshift(opusEntry);
|
||||
|
||||
// Also move the corresponding payload type in the "m" line
|
||||
const opusPayloadType = parsedSdp.media[0].payloads.split(' ')[opusIndex];
|
||||
const otherPayloadTypes = parsedSdp.media[0].payloads.split(' ').filter((pt) => pt != opusPayloadType);
|
||||
parsedSdp.media[0].payloads = [opusPayloadType, ...otherPayloadTypes].join(' ');
|
||||
}
|
||||
return sdpTransform.write(parsedSdp);
|
||||
};
|
||||
|
||||
const extractSdpMedia = (sdp) => {
|
||||
const parsedSdp1 = sdpTransform.parse(sdp);
|
||||
if (parsedSdp1.media.length > 1) {
|
||||
@@ -28,5 +52,7 @@ const extractSdpMedia = (sdp) => {
|
||||
module.exports = {
|
||||
isOnhold,
|
||||
mergeSdpMedia,
|
||||
extractSdpMedia
|
||||
extractSdpMedia,
|
||||
isOpusFirst,
|
||||
makeOpusFirst
|
||||
};
|
||||
|
||||
@@ -60,7 +60,13 @@ const stickyVars = {
|
||||
aws: [
|
||||
'AWS_VOCABULARY_NAME',
|
||||
'AWS_VOCABULARY_FILTER_METHOD',
|
||||
'AWS_VOCABULARY_FILTER_NAME'
|
||||
'AWS_VOCABULARY_FILTER_NAME',
|
||||
'AWS_LANGUAGE_MODEL_NAME',
|
||||
'AWS_ACCESS_KEY_ID',
|
||||
'AWS_SECRET_ACCESS_KEY',
|
||||
'AWS_REGION',
|
||||
'AWS_SECURITY_TOKEN',
|
||||
'AWS_PII_ENTITY_TYPES'
|
||||
],
|
||||
nuance: [
|
||||
'NUANCE_ACCESS_TOKEN',
|
||||
@@ -368,11 +374,19 @@ const normalizeMicrosoft = (evt, channel, language) => {
|
||||
|
||||
const normalizeAws = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
const alternatives = evt.Transcript?.Results[0]?.Alternatives.map((alt) => {
|
||||
const items = alt.Items.filter((item) => item.Type === 'pronunciation' && 'Confidence' in item);
|
||||
const confidence = items.reduce((acc, item) => acc + item.Confidence, 0) / items.length;
|
||||
return {
|
||||
transcript: alt.Transcript,
|
||||
confidence
|
||||
};
|
||||
});
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt[0].is_final,
|
||||
alternatives: evt[0].alternatives,
|
||||
is_final: evt.Transcript?.Results[0].IsPartial === false,
|
||||
alternatives,
|
||||
vendor: {
|
||||
name: 'aws',
|
||||
evt: copy
|
||||
@@ -483,16 +497,29 @@ module.exports = (logger) => {
|
||||
};
|
||||
}
|
||||
else if (['aws', 'polly'].includes(vendor)) {
|
||||
const {awsOptions = {}} = rOpts;
|
||||
const vocabularyName = awsOptions.vocabularyName || rOpts.vocabularyName;
|
||||
const vocabularyFilterName = awsOptions.vocabularyFilterName || rOpts.vocabularyFilterName;
|
||||
const filterMethod = awsOptions.vocabularyFilterMethod || rOpts.filterMethod;
|
||||
opts = {
|
||||
...opts,
|
||||
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
|
||||
...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
|
||||
...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
|
||||
...(vocabularyName && {AWS_VOCABULARY_NAME: vocabularyName}),
|
||||
...(vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: vocabularyFilterName}),
|
||||
...(filterMethod && {AWS_VOCABULARY_FILTER_METHOD: filterMethod}),
|
||||
...(sttCredentials && {
|
||||
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
||||
AWS_REGION: sttCredentials.region
|
||||
AWS_REGION: sttCredentials.region,
|
||||
AWS_SECURITY_TOKEN: sttCredentials.securityToken
|
||||
}),
|
||||
...(awsOptions.accessKey && {AWS_ACCESS_KEY_ID: awsOptions.accessKey}),
|
||||
...(awsOptions.secretKey && {AWS_SECRET_ACCESS_KEY: awsOptions.secretKey}),
|
||||
...(awsOptions.region && {AWS_REGION: awsOptions.region}),
|
||||
...(awsOptions.securityToken && {AWS_SECURITY_TOKEN: awsOptions.securityToken}),
|
||||
...(awsOptions.languageModelName && {AWS_LANGUAGE_MODEL_NAME: awsOptions.languageModelName}),
|
||||
...(awsOptions.piiEntityTypes?.length && {AWS_PII_ENTITY_TYPES: awsOptions.piiEntityTypes.join(',')}),
|
||||
...(awsOptions.piiIdentifyEntities && {AWS_PII_IDENTIFY_ENTITIES: true}),
|
||||
...(awsOptions.languageModelName && {AWS_LANGUAGE_MODEL_NAME: awsOptions.languageModelName}),
|
||||
};
|
||||
}
|
||||
else if ('microsoft' === vendor) {
|
||||
@@ -801,6 +828,17 @@ module.exports = (logger) => {
|
||||
if (clientId && secret) return {client_id: clientId, secret};
|
||||
if (kryptonEndpoint) return {nuance_stt_uri: kryptonEndpoint};
|
||||
}
|
||||
else if (recognizer.vendor === 'aws') {
|
||||
const {accessKey, secretKey, region, securityToken} = recognizer.awsOptions || {};
|
||||
if (accessKey || secretKey || region || securityToken) {
|
||||
return {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey,
|
||||
region,
|
||||
securityToken
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (recognizer.vendor === 'nvidia') {
|
||||
const {rivaUri} = recognizer.nvidiaOptions || {};
|
||||
if (rivaUri) return {riva_uri: rivaUri};
|
||||
|
||||
3607
package-lock.json
generated
3607
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.5",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
@@ -30,25 +30,25 @@
|
||||
"@jambonz/db-helpers": "^0.9.1",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.4",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||
"@jambonz/speech-utils": "^0.0.24",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.7",
|
||||
"@jambonz/speech-utils": "^0.0.30",
|
||||
"@jambonz/stats-collector": "^0.1.9",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.45",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.9.0",
|
||||
"@opentelemetry/instrumentation": "^0.35.0",
|
||||
"@opentelemetry/resources": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.9.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.9.0",
|
||||
"@jambonz/verb-specifications": "^0.0.49",
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.18.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.45.1",
|
||||
"@opentelemetry/exporter-zipkin": "^1.18.1",
|
||||
"@opentelemetry/instrumentation": "^0.45.1",
|
||||
"@opentelemetry/resources": "^1.18.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.18.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.18.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.18.1",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.27",
|
||||
"drachtio-srf": "^4.5.29",
|
||||
"drachtio-fsmrf": "^3.0.28",
|
||||
"drachtio-srf": "^4.5.31",
|
||||
"express": "^4.18.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"ip": "^1.1.8",
|
||||
|
||||
@@ -17,5 +17,6 @@ require('./config-test');
|
||||
require('./queue-test');
|
||||
require('./in-dialog-test');
|
||||
require('./http-proxy-test');
|
||||
require('./sdp-utils-test');
|
||||
require('./remove-test-db');
|
||||
require('./docker_stop');
|
||||
26
test/sdp-utils-test.js
Normal file
26
test/sdp-utils-test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const test = require('tape');
|
||||
const {makeOpusFirst, isOpusFirst} = require('../lib/utils/sdp-utils');
|
||||
const sdpTransform = require('sdp-transform');
|
||||
|
||||
test('test opus first', (t) => {
|
||||
const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
|
||||
const opusSdp = makeOpusFirst(sdp);
|
||||
const parsedSdp = sdpTransform.parse(opusSdp);
|
||||
const opusIndex = parsedSdp.media[0].rtp.findIndex((entry) => entry.codec === 'opus');
|
||||
t.ok(opusIndex === 0, 'succesffuly move opus to be first offer')
|
||||
t.end();
|
||||
});
|
||||
|
||||
|
||||
test('test is opus first', (t) => {
|
||||
|
||||
const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
|
||||
t.ok(isOpusFirst(sdp), "opus is first offer");
|
||||
|
||||
const sdp2 = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
|
||||
t.ok(!isOpusFirst(sdp2), "opus is not first offer")
|
||||
|
||||
const sdp3 = 'v=0\r\no=xhoaluu2 1314 1504 IN IP4 192.168.1.4\r\ns=Talk\r\nc=IN IP4 192.168.1.4\r\nt=0 0\r\na=ice-pwd:397d063ea23fdc05164e3ee4\r\na=ice-ufrag:16c449a3\r\na=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics\r\na=group:BUNDLE as\r\na=record:off\r\nm=audio 56542 RTP/AVPF 0 8\r\nc=IN IP4 14.226.233.151\r\na=rtcp-mux\r\na=mid:as\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=rtcp:63076 IN IP4 192.168.1.4\r\na=candidate:1 1 UDP 2130706303 192.168.1.4 56542 typ host\r\na=candidate:1 2 UDP 2130706302 192.168.1.4 63076 typ host\r\na=candidate:2 1 UDP 2130706431 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 56542 typ host\r\na=candidate:2 2 UDP 2130706430 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 63076 typ host\r\na=candidate:3 1 UDP 2130706431 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 56542 typ host\r\na=candidate:3 2 UDP 2130706430 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 63076 typ host\r\na=candidate:4 1 UDP 1694498687 14.226.233.151 56542 typ srflx raddr 192.168.1.4 rport 56542\r\na=rtcp-fb:* trr-int 1000\r\na=rtcp-fb:* ccm tmmbr';
|
||||
t.ok(!isOpusFirst(sdp2), "opus is not first offer")
|
||||
t.end();
|
||||
});
|
||||
Reference in New Issue
Block a user