From 9aa0df256d43f8d14dbbb6c4d26f63b15f9028f3 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Thu, 23 Jun 2022 16:21:35 -0400 Subject: [PATCH] initial changes to support siprec recording (#120) * initial changes to support siprec recording * include additional params on SIP INFO to start recording * add support for maniupulating recording via REST API * fixes from testing pause/resume recording --- lib/session/call-session.js | 168 +++++++++++++++++++++++++++++++++++- lib/tasks/config.js | 4 +- lib/tasks/specs.json | 16 +++- lib/utils/constants.json | 5 ++ 4 files changed, 190 insertions(+), 3 deletions(-) diff --git a/lib/session/call-session.js b/lib/session/call-session.js index 601f307a..d4ed03c2 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -1,6 +1,13 @@ const Emitter = require('events'); const fs = require('fs'); -const {CallDirection, TaskPreconditions, CallStatus, TaskName, KillReason} = require('../utils/constants'); +const { + CallDirection, + TaskPreconditions, + CallStatus, + TaskName, + KillReason, + RecordState +} = require('../utils/constants'); const moment = require('moment'); const assert = require('assert'); const sessionTracker = require('./session-tracker'); @@ -54,6 +61,8 @@ class CallSession extends Emitter { assert(rootSpan); + this._recordState = RecordState.RecordingOff; + this.tmpFiles = new Set(); if (!this.isSmsCallSession) { @@ -85,6 +94,10 @@ class CallSession extends Emitter { return this.callInfo.direction; } + get applicationSid() { + return this.callInfo.applicationSid; + } + /** * SIP call-id for the call */ @@ -234,6 +247,153 @@ class CallSession extends Emitter { return this.rootSpan?.getTracingPropagation(); } + get recordState() { return this._recordState; } + + async notifyRecordOptions(opts) { + const {action} = opts; + this.logger.debug({opts}, 'CallSession:notifyRecordOptions'); + + /* if we have not answered yet, just save the details for later */ + if (!this.dlg) { + if (action === 'startCallRecording') { + this.recordOptions = opts; + return true; + } + return false; + } + + /* check validity of request */ + if (action == 'startCallRecording' && this.recordState !== RecordState.RecordingOff) { + this.logger.info({recordState: this.recordState}, + 'CallSession:notifyRecordOptions: recording is already started, ignoring request'); + return false; + } + if (action == 'stopCallRecording' && this.recordState === RecordState.RecordingOff) { + this.logger.info({recordState: this.recordState}, + 'CallSession:notifyRecordOptions: recording is already stopped, ignoring request'); + return false; + } + if (action == 'pauseCallRecording' && this.recordState !== RecordState.RecordingOn) { + this.logger.info({recordState: this.recordState}, + 'CallSession:notifyRecordOptions: cannot pause recording, ignoring request '); + return false; + } + if (action == 'resumeCallRecording' && this.recordState !== RecordState.RecordingPaused) { + this.logger.info({recordState: this.recordState}, + 'CallSession:notifyRecordOptions: cannot resume recording, ignoring request '); + return false; + } + + this.recordOptions = opts; + + switch (action) { + case 'startCallRecording': + return await this.startRecording(); + case 'stopCallRecording': + return await this.stopRecording(); + case 'pauseCallRecording': + return await this.pauseRecording(); + case 'resumeCallRecording': + return await this.resumeRecording(); + default: + throw new Error(`invalid record action ${action}`); + } + } + + async startRecording() { + const {recordingID, siprecServerURL} = this.recordOptions; + assert(this.dlg); + this.logger.debug(`CallSession:startRecording - sending to ${siprecServerURL}`); + try { + const res = await this.dlg.request({ + method: 'INFO', + headers: { + 'X-Reason': 'startCallRecording', + 'X-Srs-Url': siprecServerURL, + 'X-Srs-Recording-ID': recordingID, + 'X-Call-Sid': this.callSid, + 'X-Account-Sid': this.accountSid, + 'X-Application-Sid': this.applicationSid, + } + }); + if (res.status === 200) { + this._recordState = RecordState.RecordingOn; + return true; + } + this.logger.info(`CallSession:startRecording - ${res.status} failure sending to ${siprecServerURL}`); + return false; + } catch (err) { + this.logger.info({err}, `CallSession:startRecording - failure sending to ${siprecServerURL}`); + return false; + } + } + + async stopRecording() { + assert(this.dlg); + this.logger.debug('CallSession:stopRecording'); + try { + const res = await this.dlg.request({ + method: 'INFO', + headers: { + 'X-Reason': 'stopCallRecording', + } + }); + if (res.status === 200) { + this._recordState = RecordState.RecordingOff; + return true; + } + this.logger.info(`CallSession:stopRecording - ${res.status} failure`); + return false; + } catch (err) { + this.logger.info({err}, 'CallSession:startRecording - failure sending'); + return false; + } + } + + async pauseRecording() { + assert(this.dlg); + this.logger.debug('CallSession:pauseRecording'); + try { + const res = await this.dlg.request({ + method: 'INFO', + headers: { + 'X-Reason': 'pauseCallRecording', + } + }); + if (res.status === 200) { + this._recordState = RecordState.RecordingPaused; + return true; + } + this.logger.info(`CallSession:pauseRecording - ${res.status} failure`); + return false; + } catch (err) { + this.logger.info({err}, 'CallSession:pauseRecording - failure sending'); + return false; + } + } + + async resumeRecording() { + assert(this.dlg); + this.logger.debug('CallSession:resumeRecording'); + try { + const res = await this.dlg.request({ + method: 'INFO', + headers: { + 'X-Reason': 'resumeCallRecording', + } + }); + if (res.status === 200) { + this._recordState = RecordState.RecordingOn; + return true; + } + this.logger.info(`CallSession:resumeRecording - ${res.status} failure`); + return false; + } catch (err) { + this.logger.info({err}, 'CallSession:resumeRecording - failure sending'); + return false; + } + } + async enableBotMode(gather, autoEnable) { try { const t = normalizeJambones(this.logger, [gather]); @@ -724,6 +884,9 @@ class CallSession extends Emitter { const res = await this._lccSipRequest(opts, callSid); return {status: res.status, reason: res.reason}; } + else if (opts.record) { + await this.notifyRecordOptions(opts.record); + } // 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.. @@ -1076,6 +1239,9 @@ class CallSession extends Emitter { this.dlg.callSid = this.callSid; this.emit('callStatusChange', {sipStatus: 200, sipReason: 'OK', callStatus: CallStatus.InProgress}); + if (this.recordOptions && this.recordState === RecordState.RecordingOff) { + this.startRecording(); + } this.dlg.on('modify', this._onReinvite.bind(this)); this.dlg.on('refer', this._onRefer.bind(this)); diff --git a/lib/tasks/config.js b/lib/tasks/config.js index fd1c4fa2..d2182188 100644 --- a/lib/tasks/config.js +++ b/lib/tasks/config.js @@ -9,7 +9,8 @@ class TaskConfig extends Task { [ 'synthesizer', 'recognizer', - 'bargeIn' + 'bargeIn', + 'record' ].forEach((k) => this[k] = this.data[k] || {}); if (this.bargeIn.enable) { @@ -100,6 +101,7 @@ class TaskConfig extends Task { cs.disableBotMode(); } } + if (this.record) cs.notifyRecordOptions(this.record); } async kill(cs) { diff --git a/lib/tasks/specs.json b/lib/tasks/specs.json index 79e41c37..36e3b8ac 100644 --- a/lib/tasks/specs.json +++ b/lib/tasks/specs.json @@ -36,7 +36,8 @@ "properties": { "synthesizer": "#synthesizer", "recognizer": "#recognizer", - "bargeIn": "#bargeIn" + "bargeIn": "#bargeIn", + "record": "#recordOptions" }, "required": [] }, @@ -307,6 +308,19 @@ "path" ] }, + "recordOptions": { + "properties": { + "action": { + "type": "string", + "enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"] + }, + "recordingID": "string", + "siprecServerURL": "string" + }, + "required": [ + "action" + ] + }, "redirect": { "properties": { "actionHook": "object|string" diff --git a/lib/utils/constants.json b/lib/utils/constants.json index cbd54ac6..878899ff 100644 --- a/lib/utils/constants.json +++ b/lib/utils/constants.json @@ -120,6 +120,11 @@ "verb:hook", "jambonz:error" ], + "RecordState": { + "RecordingOn": "recording_on", + "RecordingOff": "recording_off", + "RecordingPaused": "recording_paused" + }, "MAX_SIMRINGS": 10, "BONG_TONE": "tone_stream://v=-7;%(100,0,941.0,1477.0);v=-7;>=2;+=.1;%(1400,0,350,440)", "FS_UUID_SET_NAME": "fsUUIDs"