diff --git a/README.md b/README.md index 650c3d6b..3019965d 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,37 @@ opentok.forceDisconnect(sessionId, connectionId, function (error) { This is the server-side equivalent to the forceDisconnect() method in OpenTok.js: . +### Force Mute a participant + +You can force mute participants from an OpenTok Session using the +`openTok.forceMute(sessionId, streamId, callback)` method. + +```javascript +opentok.forceMute(sessionId, streamId, function (error) { + if (error) return console.log("error:", error); +}); +``` + +### Force Mute All participants + +You can force mute all participants from an OpenTok Session using the +`openTok.forceMuteAll(sessionId, excludedStreamIds, callback)` method. +To mute all participants call forceMuteAll with out excludedStreamIds. +To exclude streams from being muted use the excludedStreamIds. + +```javascript +const excludedStreamIds = ['stream1','stream2']; +opentok.forceMuteAll(sessionId, excludedStreamIds, function (error) { + if (error) return console.log("error:", error); +}); +``` + +```javascript +opentok.forceMuteAll(sessionId, null, function (error) { + if (error) return console.log("error:", error); +}); +``` + ### Working with SIP Interconnect You can add an audio-only stream from an external third-party SIP gateway using the SIP Interconnect diff --git a/lib/errors.js b/lib/errors.js index dc7437b7..55e381b9 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -33,13 +33,18 @@ exports.SignalError = function (message) { exports.SignalError.prototype = Object.create(Error.prototype); - exports.ForceDisconnectError = function (message) { this.message = message; }; exports.ForceDisconnectError.prototype = Object.create(Error.prototype); +exports.ForceMuteError = function (message) { + this.message = message; +}; + +exports.ForceMuteError.prototype = Object.create(Error.prototype); + exports.CallbackError = function (message) { this.message = message; diff --git a/lib/muteModeration.js b/lib/muteModeration.js new file mode 100644 index 00000000..d30346c6 --- /dev/null +++ b/lib/muteModeration.js @@ -0,0 +1,63 @@ +/* global require, exports */ +/* jshint strict:false, eqnull:true */ + +var request = require('request'); +var errors = require('./errors'); +var pkg = require('../package.json'); +var _ = require('lodash'); +var generateJwt = require('./generateJwt'); + +var api = function (config, method, session, stream, body, callback) { + var rurl = config.apiEndpoint + '/v2/project/' + config.apiKey + '/session/' + session; + if (stream) { + rurl += '/stream/' + stream; + } + rurl += '/mute'; + request.defaults(_.pick(config, 'proxy', 'timeout'))({ + url: rurl, + method: method, + headers: { + 'X-OPENTOK-AUTH': generateJwt(config), + 'User-Agent': 'OpenTok-Node-SDK/' + pkg.version + + (config.uaAddendum ? ' ' + config.uaAddendum : '') + }, + json: body + }, callback); +}; + +handleForceMute = function (config, sessionId, streamId, excludedStreamIds = null, callback) { + if (typeof callback !== 'function') { + throw (new errors.ArgumentError('No callback given to signal')); + } + if (sessionId == null) { + return callback(new errors.ArgumentError('No sessionId or connectionId given to signal')); + } + let body=null; + if(excludedStreamIds){ + body = JSON.stringify({excluded:excludedStreamIds}); + } + return api(config, 'POST', sessionId, streamId, body, function (err, response) { + if (err || Math.floor(response.statusCode / 100) !== 2) { + if (response && response.statusCode === 403) { + callback(new errors.AuthError('Invalid API key or secret')); + } + else if (response && response.statusCode === 404) { + callback(new errors.ForceMuteError('Session or Stream not found')); + } + else { + callback(new errors.RequestError('Unexpected response from OpenTok')); + } + } + else { + callback(null); + } + }); +}; + +exports.forceMute = function(config, sessionId, streamId, callback) { + return this.handleForceMute(config,sessionId,streamId,null,callback); +} + +exports.forceMuteAll = function(config, sessionId, excludedStreamIds=null, callback) { + return this.handleForceMute(config,sessionId,null,excludedStreamIds,callback); +} diff --git a/lib/opentok.js b/lib/opentok.js index fc58588a..fbb7a71c 100644 --- a/lib/opentok.js +++ b/lib/opentok.js @@ -13,6 +13,7 @@ var archiving = require('./archiving'); var Broadcast = require('./broadcast'); var SipInterconnect = require('./sipInterconnect'); var moderation = require('./moderation'); +var muteModeration = require('./muteModeration'); var signaling = require('./signaling'); var errors = require('./errors'); var callbacks = require('./callbacks'); @@ -798,6 +799,54 @@ OpenTok = function (apiKey, apiSecret, env) { */ this.forceDisconnect = moderation.forceDisconnect.bind(null, apiConfig); + /** + * Mutes a participant from an OpenTok session. + * + * @param sessionId The session ID for the OpenTok session that the client you want + * to mute is connected to. + * + * @param streamId The stream ID of the client you want to mute. + * + * @param callback {Function} The function to call upon completing the operation. Two arguments + * are passed to the function: + * + * + * + * @method #forceMute + * @memberof OpenTok + */ + this.forceMute = muteModeration.forceMute.bind(null, apiConfig); + + /** + * Mutes a participant from an OpenTok session. + * + * @param sessionId The session ID for the OpenTok session that the client you want + * to mute is connected to. + * + * @param excludedSteamIds (optional) A list of stream Ids to exclude from mute. + * + * @param callback {Function} The function to call upon completing the operation. Two arguments + * are passed to the function: + * + * + * + * @method #forceMuteAll + * @memberof OpenTok + */ + this.forceMuteAll = muteModeration.forceMuteAll.bind(null, apiConfig); + /** * Gets info about a stream. The stream must be an active stream in an OpenTok session. * @@ -1157,7 +1206,7 @@ OpenTok.prototype.createSession = function (opts, callback) { * *
  • 'moderator' — In addition to the privileges granted to a * publisher, in clients using the OpenTok.js library, a moderator can call the -* forceUnpublish() and forceDisconnect() method of the +* forceMute, forceMuteAll(), forceUnpublish() and forceDisconnect() method of the * Session object.
  • * * diff --git a/test/muteModeration-test.js b/test/muteModeration-test.js new file mode 100644 index 00000000..e4f8139a --- /dev/null +++ b/test/muteModeration-test.js @@ -0,0 +1,130 @@ +var expect = require('chai').expect; +var nock = require('nock'); + +// Subject +var OpenTok = require('../lib/opentok.js'); + +describe('MuteModeration', function () { + var opentok = new OpenTok('APIKEY', 'APISECRET'); + var SESSIONID = '1_MX4xMDB-MTI3LjAuMC4xflR1ZSBKYW4gMjggMTU6NDg6NDAgUFNUIDIwMTR-MC43NjAyOTYyfg'; + var STREAMID = '4072fe0f-d499-4f2f-8237-64f5a9d936f5'; + + afterEach(function () { + nock.cleanAll(); + }); + + describe('forceMute', function () { + function mockRequest(status, body) { + var url = '/v2/project/APIKEY/session/' + SESSIONID + '/stream/' + STREAMID +'/mute'; + nock('https://api.opentok.com:443') + .post(url) + .reply(status, body, { + server: 'nginx', + date: 'Fri, 31 Jan 2014 06:32:16 GMT', + connection: 'keep-alive' + }); + } + + describe('valid responses', function () { + beforeEach(function () { + mockRequest(204, ''); + }); + + it('should not return an error', function (done) { + opentok.forceMute(SESSIONID, STREAMID, function (err) { + expect(err).to.be.null; + done(); + }); + }); + + it('should return an error for empty stream Id', function (done) { + opentok.forceMute(SESSIONID, null, function (err) { + expect(err).to.not.be.null; + done(); + }); + }); + + it('should return an error for empty session', function (done) { + opentok.forceMute(null, STREAMID, function (err) { + expect(err).to.not.be.null; + done(); + }); + }); + }); + + describe('invalid responses', function () { + var errors = [400, 403, 404, 500]; + var i; + function test(error) { + it('should fail for status ' + error, function (done) { + mockRequest(error, ''); + + opentok.forceMute(SESSIONID, STREAMID, function (err) { + expect(err).to.not.be.null; + done(); + }); + }); + } + for (i = 0; i < errors.length; i++) { + test(errors[i]); + } + }); + }); + describe('forceMuteAll', function () { + function mockRequest(status, body) { + var url = '/v2/project/APIKEY/session/' + SESSIONID +'/mute'; + nock('https://api.opentok.com:443') + .post(url) + .reply(status, body, { + server: 'nginx', + date: 'Fri, 31 Jan 2014 06:32:16 GMT', + connection: 'keep-alive' + }); + } + const excludedSteamIds = JSON.parse('{"1":1, "2":2}'); + describe('valid responses', function () { + beforeEach(function () { + mockRequest(204, ''); + }); + + it('should not return an error', function (done) { + opentok.forceMuteAll(SESSIONID, excludedSteamIds, function (err) { + expect(err).to.be.null; + done(); + }); + }); + + it('should not return an error for empty stream Id list', function (done) { + opentok.forceMuteAll(SESSIONID, null, function (err) { + expect(err).to.be.null; + done(); + }); + }); + + it('should return an error for empty session', function (done) { + opentok.forceMuteAll(null, excludedSteamIds, function (err) { + expect(err).to.not.be.null; + done(); + }); + }); + }); + + describe('invalid responses', function () { + var errors = [400, 403, 404, 500]; + var i; + function test(error) { + it('should fail for status ' + error, function (done) { + mockRequest(error, ''); + + opentok.forceMuteAll(SESSIONID, excludedSteamIds, function (err) { + expect(err).to.not.be.null; + done(); + }); + }); + } + for (i = 0; i < errors.length; i++) { + test(errors[i]); + } + }); + }); +});