From d743baa28b86b56f4d9c4fce6d382bf879763cb6 Mon Sep 17 00:00:00 2001 From: Chuck Reeves Date: Tue, 11 Jun 2024 09:31:48 -0400 Subject: [PATCH] feat: adding support for captions (#332) --- lib/captions.js | 131 ++++++++++++++++++++++++++++++++++ lib/errors.js | 3 + lib/opentok.js | 96 +++++++++++++++++++++++++ test/opentok-test.js | 166 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 396 insertions(+) create mode 100644 lib/captions.js diff --git a/lib/captions.js b/lib/captions.js new file mode 100644 index 00000000..c9a7c4f4 --- /dev/null +++ b/lib/captions.js @@ -0,0 +1,131 @@ +const fetch = require('node-fetch'); +const errors = require('./errors'); +const pkg = require('../package.json'); +const generateJwt = require('./generateJwt'); +const generateHeaders = (config) => { + return { + 'User-Agent': 'OpenTok-Node-SDK/' + pkg.version, + 'X-OPENTOK-AUTH': generateJwt(config), + Accept: 'application/json', + }; +}; +const api = (config, method, path, body, callback) => { + const rurl = config.apiEndpoint + '/v2/project/' + config.apiKey + path; + + const headers = generateHeaders(config); + + if (body && ['POST', 'PATCH', 'PUT'].includes(method)) { + headers['Content-Type'] = 'application/json'; + } + + Promise.resolve(fetch( + rurl, + { + method: method, + body: body ? JSON.stringify(body) : null, + headers: headers, + } + )) + .then(async (response) => { + const otResponse = { + statusCode: response.status, + } + + const body = await response.text(); + + callback(null, otResponse, body) + }) + .catch(async (error) => { + callback(error); + }); +}; + +exports.startCaptions = ( + config, + sessionId, + token, + { + languageCode = 'en-US', + maxDuration = 1800, + partialCaptions = true + }, + callback, + ) => { + if (typeof callback !== 'function') { + throw new errors.ArgumentError('No callback given to startCaptions'); + } + + api( + config, + 'POST', + '/captions', + { + sessionId: sessionId, + token: token, + languageCode: languageCode, + maxDuration: maxDuration, + partialCaptions: partialCaptions, + }, + (err, response, body) => { + if (err) { + callback(err); + return; + } + + let responseJson = {}; + try { + responseJson = JSON.parse(body); + } catch { + + } + switch (response?.statusCode) { + case 200: + const { captionsId } = responseJson + callback(null, captionsId); + break; + case 400: + callback(new errors.RequestError(body)); + break; + case 409: + callback(new errors.CaptionsError()); + break; + default: + callback(new errors.RequestError('Unexpected response from OpenTok: ' + JSON.stringify({ statusCode: response.statusCode, statusMessage: response.statusMessage }))); + } + } + ); +}; + +exports.stopCaptions = ( + config, + captionsId, + callback, +) => { + if (typeof callback !== 'function') { + throw new errors.ArgumentError('No callback given to stopArchive'); + } + + api( + config, + 'POST', + `/captions/${captionsId}/stop`, + {}, + (err, response, body) => { + if (err) { + callback(err); + return; + } + + switch (response?.statusCode) { + case 202: + callback(null, true); + break; + case 400: + callback(new errors.NotFoundError(`No matching captions found for ${captionsId}`)); + break; + default: + callback(new errors.RequestError('Unexpected response from OpenTok: ' + JSON.stringify({ statusCode: response.statusCode, statusMessage: response.statusMessage }))); + } + } + ); +}; diff --git a/lib/errors.js b/lib/errors.js index 63e8841c..e8017177 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -18,6 +18,9 @@ exports.ArchiveError = function (message) { exports.ArchiveError.prototype = Object.create(Error.prototype); +exports.CaptionsError = function (message = 'Live captions have already started for this OpenTok Session') { + this.message = message; +}; exports.SipError = function (message) { this.message = message; diff --git a/lib/opentok.js b/lib/opentok.js index a8bad3cf..266ad3d5 100644 --- a/lib/opentok.js +++ b/lib/opentok.js @@ -18,6 +18,7 @@ var errors = require('./errors'); var callbacks = require('./callbacks'); var generateJwt = require('./generateJwt'); var render = require('./render.js'); +var captions = require('./captions.js'); var OpenTok; var key; @@ -487,6 +488,100 @@ OpenTok = function (apiKey, apiSecret, env) { ); }; + + /** + * Starts live captions for an OpenTok Session + *

+ * The maximum allowed duration is 4 hours, after which the audio captioning will stop without + * any effect on the ongoing OpenTok Session. An event will be posted to your callback URL if + * provided when starting the captions. + *

+ * Each OpenTok Session supports only one audio captioning session. + * + * @param sessionId The session ID of the OpenTok session to archive. + * + * @param token A valid OpenTok token with role set to Moderator. + * + * @param options {Object} An optional options object with the following properties (each + * of which is optional): + *

+ *

+ * + * For more information on captions, see the + * OpenTok captions + * programming guide. + * + * @param callback {Function} The function to call upon completing the operation. Two arguments + * are passed to the function: + * + * + * + * @method #startCaptions + * @memberof OpenTok + */ + + this.startCaptions = captions.startCaptions.bind(null, apiConfig); + + + /** + * Stops live captions for an OpenTok Session + * + * @param captionId The session ID of the OpenTok session to archive. + * + * + * + * For more information on captions, see the + * OpenTok captions + * programming guide. + * + * @param callback {Function} The function to call upon completing the operation. Two arguments + * are passed to the function: + * + * + * + * @method #startCaptions + * @memberof OpenTok + */ + + this.stopCaptions = captions.stopCaptions.bind(null, apiConfig); + + /** * Retrieves a List of {@link Render} objects, representing any renders in the starting, * started, stopped or failed state, for your API key. @@ -2106,6 +2201,7 @@ OpenTok.prototype.generateToken = function (sessionId, opts) { } return encodeToken(tokenData, this.apiKey, this.apiSecret); + }; /* diff --git a/test/opentok-test.js b/test/opentok-test.js index 93b03610..ea2bf1cf 100644 --- a/test/opentok-test.js +++ b/test/opentok-test.js @@ -2322,3 +2322,169 @@ describe('listStreams', function () { }); }); }); + + + +describe('Captions', () => { + const opentok = new OpenTok('123456', 'APISECRET'); + const sessionId = '1_MX4xMDB-MTI3LjAuMC4xflR1ZSBKYW4gMjggMTU6NDg6NDAgUFNUIDIwMTR-MC43NjAyOTYyfg'; + const token = 'my awesome token'; + const testCaptionId = 'my-caption-id'; + + afterEach( () => { + nock.cleanAll(); + }); + + describe('start Captions', () => { + afterEach( () => { + nock.cleanAll(); + }); + + it('Starts Captions', (done) => { + nock('https://api.opentok.com') + .post( + '/v2/project/123456/captions', + { + "sessionId": sessionId, + "token": token, + "languageCode": "en-US", + "maxDuration": 1800, + "partialCaptions": true, + }, + ) + .reply( + 200, + {captionsId: testCaptionId}, + { 'Content-Type': 'application/json' } + ); + + opentok.startCaptions(sessionId, token, {}, (err, captionId) => { + expect(err).to.be.null; + expect(captionId).to.equal(testCaptionId) + done(); + }); + }); + + it('Starts Captions with options', (done) => { + nock('https://api.opentok.com') + .post( + '/v2/project/123456/captions', + { + "sessionId": sessionId, + "token": token, + "languageCode": "hi-IN", + "maxDuration": 42, + "partialCaptions": false, + }, + ) + .reply( + 200, + {captionsId: testCaptionId}, + { 'Content-Type': 'application/json' } + ); + + opentok.startCaptions( + sessionId, + token, + { + languageCode: "hi-IN", + maxDuration: 42, + partialCaptions: false, + }, + (err, captionId) => { + expect(err).to.be.null; + expect(captionId).to.equal(testCaptionId) + done(); + }); + }); + + it('Fails to Start Captions with invalid data', (done) => { + nock('https://api.opentok.com') + .post( + '/v2/project/123456/captions', + { + "sessionId": sessionId, + "token": token, + "languageCode": "en-US", + "maxDuration": 1800, + "partialCaptions": true, + }, + ) + .reply( + 400 + ); + + opentok.startCaptions( + sessionId, + token, + {}, + (err, captionId) => { + expect(err).not.to.be.null; + expect(captionId).to.be.undefined; + done(); + }); + }); + + + it('Fails to Start Captions when captions have started', (done) => { + nock('https://api.opentok.com') + .post( + '/v2/project/123456/captions', + { + "sessionId": sessionId, + "token": token, + "languageCode": "en-US", + "maxDuration": 1800, + "partialCaptions": true, + }, + ) + .reply( + 409 + ); + + opentok.startCaptions( + sessionId, + token, + {}, + (err, captionId) => { + expect(err).not.to.be.null; + expect(err.message).to.equal('Live captions have already started for this OpenTok Session'); + expect(captionId).to.be.undefined; + done(); + }); + }); + + it('Stops Captions', (done) => { + nock('https://api.opentok.com') + .post( + `/v2/project/123456/captions/${testCaptionId}/stop`, + ) + .reply( + 202 + ); + + opentok.stopCaptions(testCaptionId, (err, status) => { + expect(err).to.be.null; + expect(status).to.be.true; + done(); + }); + }); + + it('Fails to Stop Captions with invalid id', (done) => { + nock('https://api.opentok.com') + .post( + `/v2/project/123456/captions/${testCaptionId}/stop`, + ) + .reply( + 404 + ); + + opentok.stopCaptions(testCaptionId, (err, status) => { + expect(err).not.to.be.null; + expect(err.message).contain('Not Found'); + expect(status).to.be.undefined; + done(); + }); + }); + }); +});