diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0d9d8f..80108e8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [v4.50.0](https://github.com/plivo/plivo-go/tree/v4.50.0) (2023-06-28) +**Feature - Audio Streaming** +- `Audio Stream over Call` added API to start and stop streaming on a live call and get details of running or stopped streams. Also implemented the ability to generate stream XML. + ## [4.49.0](https://github.com/plivo/plivo-node/tree/v4.49.0) (2023-06-02) **Feature - CNAM Lookup** - Added New Param `cnam_lookup` in to the response of the [list all numbers API], [list single number API] @@ -66,14 +70,6 @@ **Delete Brand and Campaign Request** - Delete Brand and Campaign Request endpoint added -## [v4.37.0](https://github.com/plivo/plivo-node/tree/v4.37.0) (2022-12-16) -**Update Campaign Request** -- Update Campaign Request endpoint added - -## [v4.36.0](https://github.com/plivo/plivo-node/tree/v4.36.0) (2022-12-06) -**Delete Brand and Campaign Request** -- Delete Brand and Campaign Request endpoint added - ## [v4.35.0](https://github.com/plivo/plivo-node/tree/v4.35.0) (2022-11-04) **Brand Usecase Request** - Brand Usecase Request endpoint added diff --git a/lib/resources/call.js b/lib/resources/call.js index c6644d08..b9166db6 100644 --- a/lib/resources/call.js +++ b/lib/resources/call.js @@ -47,7 +47,6 @@ export class CreateCallResponse { } } - export class GetQueuedCallResponse { constructor(params) { params = params || {}; @@ -172,6 +171,15 @@ export class RecordCallResponse { } } +export class StartStreamResponse { + constructor(params) { + params = params || {}; + this.apiId = params.apiId; + this.message = params.message; + this.streamId = params.streamId; + } +} + /** * Represents a Call * @constructor @@ -235,6 +243,60 @@ export class Call extends PlivoResource { }); }); } + + + /** + * start audio stream over a call + * @method + * @param {object} params - to start stream + * @promise {object} return PlivoGenericResponse Object + * @fail {Error} return Error + */ + stream(params= {}) { + params.isVoiceRequest = 'true'; + return this.startStreaming(params); + } + + /** + * start audio stream over a call + * @method + * @param {object} params - to start stream + * @promise {object} return PlivoGenericResponse Object + * @fail {Error} return Error + */ + startStreaming(params) { + let client = this[clientKey]; + return new Promise((resolve, reject) => { + client('POST', action + this.id + '/Stream/', params) + .then(response => { + resolve(new StartStreamResponse(response.body, idField)); + }) + .catch(error => { + reject(error); + }); + }); + } + + stopStream(params= {}) { + params.isVoiceRequest = 'true'; + return super.executeAction(this.id + '/Stream/' + this.secondaryId, 'DELETE', params); + } + + stopAllStream(params= {}) { + params.isVoiceRequest = 'true'; + return super.executeAction(this.id + '/Stream/', 'DELETE', params); + } + + getStream(params= {}) { + params.isVoiceRequest = 'true'; + return super.executeAction(this.id + '/Stream/' + this.secondaryId, 'GET', params); + } + + getAllStream(params= {}) { + params.isVoiceRequest = 'true'; + return super.executeAction(this.id + '/Stream/', 'GET', params); + } + /** * record call * @method @@ -645,6 +707,125 @@ export class CallInterface extends PlivoResourceInterface { }).transfer(params, callUUID); } + /** + * Start a Stream over a Call + * @method + * @param {string} serviceUrl - Wss url over which data packets will be send. + * @param {string} callUuid - For this callUuid audio streaming will start. + * @param {object} params - optional params to start a stream + * @param {string} [params.bidirectional] Specifies if the audio being streamed over web-sockets is oneway (read only for the wss service) only or bidirectional (the wss service can read as well as write audio back). + * @param {string} [params.audioTrack] The audio track (inbound or outbound) of the underlying call which Plivo will fork and stream to the wss service. Inbound [default], outbound, both. Note: only inbound is allowed if bidirectional is true. + * @param {string} [params.streamTimeout] Maximum duration, in seconds, for which audio will be streamed once streaming starts. At the end of the specified duration, streaming will stop. This will have no impact on the rest of the call flow. Defaults to 86400 (24 hrs). + * @param {string} [params.statusCallbackUrl] URL that is notified by Plivo when stream is connected, stopped, failed to connect or disconnected. Note: not called when the call gets disconnected. + * @param {string} [params.statusCallbackMethod] POST[default], GET. + * @param {string} [params.contentType] Preferred audio codec and sampling rate. Allowed, audio/x-l16;rate=8000 [default], audio/x-l16;rate=16000 and audio/x-mulaw;rate=8000. + * @param {string} [params.extraHeaders] These are key value pairs which will be passed to the wss service along with your stream. Total length of the string being passed should be less than equal to 512 bytes. + * @promise {object} returns PlivoGenericResponse Object + * @fail {Error} returns Error + */ + stream(callUUID, serviceUrl, optionalParams = {}) { + let errors = validate([{ + field: 'serviceUrl', + value: serviceUrl, + validators: ['isRequired'] + }, + { + field: 'callUUID', + value: callUUID, + validators: ['isRequired'] + } + ]); + + if (errors) { + return errors; + } + + optionalParams.serviceUrl = serviceUrl + return new Call(this[clientKey], { + id: callUUID + }).stream(optionalParams); + } + + stopStream(callUUID, streamId){ + let errors = validate([{ + field: 'callUUID', + value: callUUID, + validators: ['isRequired'] + }, + { + field: 'streamId', + value: streamId, + validators: ['isRequired'] + } + ]); + + if (errors) { + return errors; + } + + return new Call(this[clientKey], { + id: callUUID, + secondaryId: streamId + }).stopStream(); + } + + stopAllStream(callUUID){ + let errors = validate([{ + field: 'callUUID', + value: callUUID, + validators: ['isRequired'] + } + ]); + + if (errors) { + return errors; + } + + return new Call(this[clientKey], { + id: callUUID + }).stopAllStream(); + } + + getStream(callUUID, streamId){ + let errors = validate([{ + field: 'callUUID', + value: callUUID, + validators: ['isRequired'] + }, + { + field: 'streamId', + value: streamId, + validators: ['isRequired'] + } + ]); + + if (errors) { + return errors; + } + + return new Call(this[clientKey], { + id: callUUID, + secondaryId: streamId + }).getStream(); + } + + getAllStream(callUUID){ + let errors = validate([{ + field: 'callUUID', + value: callUUID, + validators: ['isRequired'] + } + ]); + + if (errors) { + return errors; + } + + return new Call(this[clientKey], { + id: callUUID + }).getAllStream(); + } + /** * Record a Call * @method diff --git a/lib/rest/request-test.js b/lib/rest/request-test.js index 90204775..eec1f959 100644 --- a/lib/rest/request-test.js +++ b/lib/rest/request-test.js @@ -200,6 +200,16 @@ export function Request(config) { } }); } + else if (action == 'Call/aaa-deeiei3-dfddd/Stream/' && method == 'POST') { + resolve({ + response: {}, + body: { + message: 'stream started', + stream_id: 'db1d8121-1a75-4b75-bb69-35339bb26240', + api_id: 'c7b69074-58be-11e1-86da-adf28403fe48' + } + }); + } else if (action == 'Call/aaa-deeiei3-dfddd/Record/' && method == 'DELETE') { resolve({ response: {}, diff --git a/lib/rest/utils.js b/lib/rest/utils.js index d37d5720..e5de0e5a 100644 --- a/lib/rest/utils.js +++ b/lib/rest/utils.js @@ -3,9 +3,10 @@ import _snakeCase from 'lodash/snakeCase'; import _mapKeys from 'lodash/mapKeys'; import _mapValues from 'lodash/mapValues'; import _map from 'lodash/map'; -import { parseString } from 'xml2js'; +import {parseString} from 'xml2js'; -export class InvalidRequestError extends Error {} +export class InvalidRequestError extends Error { +} function recursivelyRenameObject(object, renameFunc) { if (!(object instanceof Object)) { @@ -76,7 +77,7 @@ export function camelCaseRequestWrapper(requestFunc) { export function validateSpeakAttributes(content, voice) { if (!voice || ['MAN', 'WOMAN'].indexOf(voice) != -1) { - return { success: true }; + return {success: true}; } var voiceParts = voice.split('.'); @@ -84,115 +85,112 @@ export function validateSpeakAttributes(content, voice) { return { success: false, msg: "Invalid voice " + voice + '.' }; - }; + } + ; return { success: true, } } -export function validSubAccount(accountId){ - if(accountId.constructor !== String){ +export function validSubAccount(accountId) { + if (accountId.constructor !== String) { throw new InvalidRequestError('Subaccount Id must be a string'); } - if(accountId.length !== 20){ + if (accountId.length !== 20) { throw new InvalidRequestError('Subaccount Id should be of length 20'); } - if(accountId.substring(0,2) !== 'SA'){ + if (accountId.substring(0, 2) !== 'SA') { throw new InvalidRequestError("Subaccount Id should start with 'SA'"); } return true; } -export function validMultipleDestinationNos(paramName, paramValue, options = {}){ - if(paramValue.split(options.delimiter).length > 1 && options.role.toLowerCase()!=='agent'){ +export function validMultipleDestinationNos(paramName, paramValue, options = {}) { + if (paramValue.split(options.delimiter).length > 1 && options.role.toLowerCase() !== 'agent') { throw new InvalidRequestError('Multiple ' + paramName + ' values given for role ' + options.role) - } - else if (paramValue.split(options.delimiter).length >= options.agentLimit){ + } else if (paramValue.split(options.delimiter).length >= options.agentLimit) { throw new InvalidRequestError('No of ' + paramName + ' values provided should be lesser than ' + options.agentLimit) - } - else { + } else { return true } } -export function validMultipleDestinationIntegers(paramName, paramValue){ +export function validMultipleDestinationIntegers(paramName, paramValue) { let val = paramValue.split("<"); - for (let i=0; i upperBound) { + if (lowerBound && upperBound) { + if (paramValue < lowerBound || paramValue > upperBound) { throw new InvalidRequestError(paramName + " ranges between " + lowerBound + " and " + upperBound) } - if(paramValue >= lowerBound && paramValue <= upperBound){ + if (paramValue >= lowerBound && paramValue <= upperBound) { return true; } - } - else if(lowerBound){ - if(paramValue < lowerBound){ + } else if (lowerBound) { + if (paramValue < lowerBound) { throw new InvalidRequestError(paramName + " should be greater than " + lowerBound) } - if(paramValue >= lowerBound){ + if (paramValue >= lowerBound) { return true; } - } - else if(upperBound){ - if(paramValue > upperBound){ + } else if (upperBound) { + if (paramValue > upperBound) { throw new InvalidRequestError(paramName + " should be lesser than " + upperBound) } - if(paramValue <= upperBound){ + if (paramValue <= upperBound) { return true; } - } - else{ + } else { throw new InvalidRequestError("Any one or both of lower and upper bound should be provided") } } + +export function validateStreamAttributes(body, attributes) { + + if (!body) { + return {success: false, msg: "No body set for Stream"}; + } + + return { + success: true, + attributes: attributes + } +} diff --git a/lib/utils/plivoxml.js b/lib/utils/plivoxml.js index a31a7572..4de60842 100644 --- a/lib/utils/plivoxml.js +++ b/lib/utils/plivoxml.js @@ -14,7 +14,7 @@ export class PlivoXMLError extends Error { } export function Response() { this.element = 'Response'; this.nestables = ['Speak', 'Play', 'GetDigits', 'GetInput', 'Record', 'Dial', 'Message', - 'Redirect', 'Wait', 'Hangup', 'PreAnswer', 'Conference', 'DTMF', 'MultiPartyCall']; + 'Redirect', 'Wait', 'Hangup', 'PreAnswer', 'Conference', 'DTMF', 'MultiPartyCall', 'Stream']; this.valid_attributes = []; this.elem = xmlBuilder.begin().ele(this.element); } @@ -296,6 +296,19 @@ Response.prototype = { } }, + addStream: function (body, attributes) { + + let validation; + validation = plivoUtils.validateStreamAttributes(body, attributes); + var item = this; + if (validation.success == true) { + var result = item.add(new Stream(Response), body, validation.attributes); + return result; + } else { + throw new Exceptions.PlivoXMLValidationError(validation.msg); + } + }, + /** * Add a Break element * @method @@ -703,7 +716,7 @@ Response.prototype = { }, toXML: function () { - return this.elem.toString(); + return this.elem.toString().replace(/"/g, '"'); }, toJSON: jsonStringifier.stringify @@ -1023,3 +1036,15 @@ function MultiPartyCall(Response){ 'startRecordingAudio', 'startRecordingAudioMethod', 'stopRecordingAudio', 'stopRecordingAudioMethod']; } util.inherits(MultiPartyCall, Response); + +/** + * Stream element + * @constructor + */ +function Stream(Response) { + this.element = 'Stream'; + this.valid_attributes = ['bidirectional', 'audioTrack', 'streamTimeout', 'statusCallbackUrl', 'statusCallbackMethod', + 'contentType', 'extraHeaders']; + this.nestables = ['break', 'emphasis', 'lang', 'p', 'phoneme', 'prosody', 's', 'say-as', 'sub', 'w']; +} +util.inherits(Stream, Response); diff --git a/package.json b/package.json index 187bcc57..c1c12047 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plivo", - "version": "4.49.0", + "version": "4.50.0", "description": "A Node.js SDK to make voice calls and send SMS using Plivo and to generate Plivo XML", "homepage": "https://github.com/plivo/plivo-node", "files": [ diff --git a/test/calls.js b/test/calls.js index ec654840..5ce80886 100644 --- a/test/calls.js +++ b/test/calls.js @@ -281,5 +281,16 @@ describe('calls', function () { assert.equal(resp.length, 2) }); }); - }) + }); + describe('Stream', function () { + it('should start stream!', function () { + client.calls.stream('aaa-deeiei3-dfddd', 'wss://mystream.ngrok.io/audiostream') + .then(function(call){ + return client.calls.stream(call.callUuid) + }) + .then(function(streamDetail) { + assert.equal(streamDetail.message, 'stream started') + }) + }); + }); }); diff --git a/test/xml.js b/test/xml.js index 7a630474..aeb5600f 100644 --- a/test/xml.js +++ b/test/xml.js @@ -38,4 +38,17 @@ describe('PlivoXML', function () { assert.equal('Nairobi',mpcResponse.toXML()); done(); }); + + it('tests Stream', function (done){ + const streamResponse = new Response(); + var stream_body = "text"; + var extraHeaders = "a=1,b=2"; + var params = { + 'audioTrack': "inbound", + 'extraHeaders': extraHeaders + }; + streamResponse.addStream(stream_body, params); + assert.equal('text',streamResponse.toXML()); + done(); + }); }); diff --git a/types/resources/call.d.ts b/types/resources/call.d.ts index f8945c53..0e2dc0ff 100644 --- a/types/resources/call.d.ts +++ b/types/resources/call.d.ts @@ -111,6 +111,12 @@ export class RecordCallResponse { recordingId: string; url: string; } +export class StartStreamResponse { + constructor(params: object); + apiId: string; + message: string; + streamId: string; +} /** * Represents a Call * @constructor @@ -146,6 +152,25 @@ export class Call extends PlivoResource { blegUrl: string; blegMethod: string; }, callUUid: string): Promise < CallTransferResponse > ; + + /** + * start audio stream over call + * @method + * @param {object} params - to start audio stream over a call + * @promise {object} return PlivoGenericResponse Object + * @fail {Error} return Error + */ + startStream(params: object): Promise < any > ; + + /** + * start audio stream over call + * @method + * @param {object} params - to start audio stream over a call + * @promise {object} return PlivoGenericResponse Object + * @fail {Error} return Error + */ + startStreaming(params: object): Promise < StartStreamResponse > ; + /** * record call * @method @@ -292,6 +317,7 @@ export class CallInterface extends PlivoResourceInterface { * @fail {Error} returns Error */ create(from: string, to: string, answerUrl: string, params ? : {}): Promise < CreateCallResponse > ; + /** * Hangup A Specific Call * @method @@ -320,6 +346,25 @@ export class CallInterface extends PlivoResourceInterface { blegUrl: string; blegMethod: string; }): Promise < any > ; + + /** + * Start a Stream over a Call + * @method + * @param {string} serviceUrl - Wss url over which data packets will be send. + * @param {string} callUuid - For this callUuid audio streaming will start. + * @param {object} params - optional params to start a stream + * @param {string} [params.bidirectional] Specifies if the audio being streamed over web-sockets is oneway (read only for the wss service) only or bidirectional (the wss service can read as well as write audio back). + * @param {string} [params.audioTrack] The audio track (inbound or outbound) of the underlying call which Plivo will fork and stream to the wss service. Inbound [default], outbound, both. Note: only inbound is allowed if bidirectional is true. + * @param {string} [params.streamTimeout] Maximum duration, in seconds, for which audio will be streamed once streaming starts. At the end of the specified duration, streaming will stop. This will have no impact on the rest of the call flow. Defaults to 86400 (24 hrs). + * @param {string} [params.statusCallbackUrl] URL that is notified by Plivo when stream is connected, stopped, failed to connect or disconnected. Note: not called when the call gets disconnected. + * @param {string} [params.statusCallbackMethod] POST[default], GET. + * @param {string} [params.contentType] Preferred audio codec and sampling rate. Allowed, audio/x-l16;rate=8000 [default], audio/x-l16;rate=16000 and audio/x-mulaw;rate=8000. + * @param {string} [params.extraHeaders] These are key value pairs which will be passed to the wss service along with your stream. Total length of the string being passed should be less than equal to 512 bytes. + * @promise {object} returns PlivoGenericResponse Object + * @fail {Error} returns Error + */ + startStream(serviceUrl: string, callUUID: string, optionalParams: object): Promise < any > ; + /** * Record a Call * @method