janode
Version:
Meetecho adapter for the Janus WebRTC Server
1,226 lines (1,137 loc) • 68.2 kB
JavaScript
'use strict';
/**
* This module contains the implementation of the AudioBridge plugin (ref. {@link https://janus.conf.meetecho.com/docs/audiobridge.html}).
* @module audiobridge-plugin
*/
import Handle from '../handle.js';
/* The plugin ID exported in the plugin descriptor */
const PLUGIN_ID = 'janus.plugin.audiobridge';
/* These are the requests defined for the Janus AudioBridge API */
const REQUEST_JOIN = 'join';
const REQUEST_LIST_PARTICIPANTS = 'listparticipants';
const REQUEST_KICK = 'kick';
const REQUEST_CONFIGURE = 'configure';
const REQUEST_LEAVE = 'leave';
const REQUEST_AUDIO_HANGUP = 'hangup';
const REQUEST_EXISTS = 'exists';
const REQUEST_LIST_ROOMS = 'list';
const REQUEST_CREATE = 'create';
const REQUEST_DESTROY = 'destroy';
const REQUEST_RECORDING = 'enable_recording';
const REQUEST_ALLOW = 'allowed';
const REQUEST_RTP_FWD_START = 'rtp_forward';
const REQUEST_RTP_FWD_STOP = 'stop_rtp_forward';
const REQUEST_RTP_FWD_LIST = 'listforwarders';
const REQUEST_SUSPEND_PARTICIPANT = 'suspend';
const REQUEST_RESUME_PARTICIPANT = 'resume';
const REQUEST_MUTE_PARTICIPANT = 'mute';
const REQUEST_UNMUTE_PARTICIPANT = 'unmute';
const REQUEST_MUTE_ROOM = 'unmute_room';
const REQUEST_UNMUTE_ROOM = 'unmute_room';
const REQUEST_PLAY_FILE = 'play_file';
const REQUEST_IS_PLAYING = 'is_playing';
const REQUEST_STOP_FILE = 'stop_file';
const REQUEST_STOP_ALL_FILES = 'stop_all_files';
const REQUEST_LIST_ANNOUNCEMENTS = 'listannouncements';
/* These are the events/responses that the Janode plugin will manage */
/* Some of them will be exported in the plugin descriptor */
const PLUGIN_EVENT = {
JOINED: 'audiobridge_joined',
PEER_JOINED: 'audiobridge_peer_joined',
PARTICIPANTS_LIST: 'audiobridge_participants_list',
CONFIGURED: 'audiobridge_configured',
PEER_CONFIGURED: 'audiobridge_peer_configured',
LEAVING: 'audiobridge_leaving',
AUDIO_HANGINGUP: 'audiobridge_hangingup',
PEER_LEAVING: 'audiobridge_peer_leaving',
KICKED: 'audiobridge_kicked',
PEER_KICKED: 'audiobridge_peer_kicked',
TALKING: 'audiobridge_talking',
PEER_TALKING: 'audiobridge_peer_talking',
SUSPENDED: 'audiobridge_suspended',
PEER_SUSPENDED: 'audiobridge_peer_suspended',
RESUMED: 'audiobridge_resumed',
PEER_RESUMED: 'audiobridge_peer_resumed',
EXISTS: 'audiobridge_exists',
ROOMS_LIST: 'audiobridge_list',
CREATED: 'audiobridge_created',
DESTROYED: 'audiobridge_destroyed',
RECORDING: 'audiobrige_recording',
RTP_FWD: 'audiobridge_rtp_fwd',
FWD_LIST: 'audiobridge_rtp_list',
ALLOWED: 'audiobridge_allowed',
ROOM_MUTED: 'audiobridge_room_muted',
ANNOUNCEMENTS_LIST: 'audiobridge_announcements_list',
ANNOUNCEMENT_STARTED: 'audiobridge_announcement_started',
ANNOUNCEMENT_STOPPED: 'audiobridge_announcement_stopped',
SUCCESS: 'audiobridge_success',
ERROR: 'audiobridge_error',
};
/**
* The class implementing the AudioBridge plugin (ref. {@link https://janus.conf.meetecho.com/docs/audiobridge.html}).<br>
*
* It extends the base Janode Handle class and overrides the base "handleMessage" method.<br>
*
* Moreover it defines many methods to support AudioBridge operations.
*
* @hideconstructor
* @extends module:handle~Handle
*/
class AudioBridgeHandle extends Handle {
/**
* Create a Janode AudioBridge handle.
*
* @param {module:session~Session} session - A reference to the parent session
* @param {number} id - The handle identifier
*/
constructor(session, id) {
super(session, id);
/**
* The feed identifier assigned to this handle when it joined the audio bridge.
*
* @type {number|string}
*/
this.feed = null;
/**
* The identifier of the room the audiobridge handle has joined.
*
* @type {number|string}
*/
this.room = null;
}
/**
* The custom "handleMessage" needed for handling AudioBridge messages.
*
* @private
* @param {Object} janus_message
* @returns {Object} A falsy value for unhandled events, a truthy value for handled events
*/
handleMessage(janus_message) {
const { plugindata, transaction } = janus_message;
if (plugindata && plugindata.data && plugindata.data.audiobridge) {
/**
* @type {AudioBridgeData}
*/
const message_data = plugindata.data;
const { audiobridge, error, error_code, room } = message_data;
/* Prepare an object for the output Janode event */
const janode_event = this._newPluginEvent(janus_message);
/* Add room information if available */
if (room) janode_event.data.room = room;
/* The plugin will emit an event only if the handle does not own the transaction */
/* That means that a transaction has already been closed or this is an async event */
const emit = (this.ownsTransaction(transaction) === false);
switch (audiobridge) {
/* success response */
case 'success':
/* Room exists API */
if (typeof message_data.exists !== 'undefined') {
janode_event.data.exists = message_data.exists;
janode_event.event = PLUGIN_EVENT.EXISTS;
break;
}
/* Room list API */
if (typeof message_data.list !== 'undefined') {
janode_event.data.list = message_data.list;
janode_event.event = PLUGIN_EVENT.ROOMS_LIST;
break;
}
/* Enable recording API */
if (typeof message_data.record !== 'undefined') {
janode_event.data.record = message_data.record;
janode_event.event = PLUGIN_EVENT.RECORDING;
break;
}
/* RTP forwarding started/stopped */
if (typeof message_data.stream_id !== 'undefined') {
janode_event.data.forwarder = {
host: message_data.host,
audio_port: message_data.port,
audio_stream: message_data.stream_id,
};
/* Forwarding group info */
if (message_data.group) janode_event.data.forwarder.group = message_data.group;
janode_event.event = PLUGIN_EVENT.RTP_FWD;
break;
}
/* Generic success (might be token disable) */
if (typeof message_data.allowed !== 'undefined') {
janode_event.data.list = message_data.allowed;
}
if (typeof message_data.file_id !== 'undefined') {
janode_event.data.file_id = message_data.file_id;
}
if (typeof message_data.file_id_list !== 'undefined') {
janode_event.data.file_id_list = message_data.file_id_list;
}
if (typeof message_data.playing !== 'undefined') {
janode_event.data.playing = message_data.playing;
}
/* In this case the "event" field of the Janode event is "success" */
janode_event.event = PLUGIN_EVENT.SUCCESS;
break;
/* Joined an audio bridge */
case 'joined':
/* If the message contains the id field, the event is for this handle */
if (typeof message_data.id !== 'undefined') {
/* Set the room, feed and display properties */
this.room = room;
this.feed = message_data.id;
/* Set event data (feed, display name, setup, muted etc.) */
janode_event.data.feed = message_data.id;
if (typeof message_data.rtp !== 'undefined') {
janode_event.data.rtp_participant = message_data.rtp;
/* This is left here just for backward compatibility */
/* It will be removed eventually */
janode_event.data.rtp = message_data.rtp;
}
/* Add participants data */
janode_event.data.participants = message_data.participants.map(({ id, display, muted, setup, talking, suspended }) => {
const peer = {
feed: id,
display,
muted,
setup,
};
if (typeof talking !== 'undefined') peer.talking = talking;
if (typeof suspended !== 'undefined') peer.suspended = suspended;
return peer;
});
janode_event.event = PLUGIN_EVENT.JOINED;
}
/* If the event contains the participants field, this is the join of another peer */
else if (message_data.participants && message_data.participants.length == 1) {
janode_event.data.feed = message_data.participants[0].id;
if (typeof message_data.participants[0].display === 'string') janode_event.data.display = message_data.participants[0].display;
if (typeof message_data.participants[0].muted !== 'undefined') janode_event.data.muted = message_data.participants[0].muted;
if (typeof message_data.participants[0].setup !== 'undefined') janode_event.data.setup = message_data.participants[0].setup;
if (typeof message_data.participants[0].suspended !== 'undefined') janode_event.data.suspended = message_data.participants[0].suspended;
janode_event.event = PLUGIN_EVENT.PEER_JOINED;
}
break;
/* Participants list */
case 'participants':
janode_event.data.participants = message_data.participants.map(({ id, display, muted, setup, talking, suspended }) => {
const peer = {
feed: id,
display,
muted,
setup,
};
if (typeof talking !== 'undefined') peer.talking = talking;
if (typeof suspended !== 'undefined') peer.suspended = suspended;
return peer;
});
janode_event.event = PLUGIN_EVENT.PARTICIPANTS_LIST;
break;
/* Audio bridge room created */
case 'created':
janode_event.event = PLUGIN_EVENT.CREATED;
janode_event.data.permanent = message_data.permanent;
break;
/* Audio bridge room destroyed */
case 'destroyed':
janode_event.event = PLUGIN_EVENT.DESTROYED;
break;
/* Audio bridge explicit hangup (different from core hangup!) */
case 'hangingup':
janode_event.data.feed = message_data.id || this.feed;
janode_event.event = PLUGIN_EVENT.AUDIO_HANGINGUP;
break;
/* This handle left the audio bridge */
case 'left':
janode_event.data.feed = message_data.id || this.feed;
this.feed = null;
this.room = null;
janode_event.event = PLUGIN_EVENT.LEAVING;
break;
/* Active forwarders list */
case 'forwarders':
janode_event.data.forwarders = message_data.rtp_forwarders.map(({ ip, port, stream_id, always_on, group }) => {
const forwarder = {
host: ip,
audio_port: port,
audio_stream: stream_id,
always: always_on,
};
if (group) forwarder.group = group;
return forwarder;
});
janode_event.event = PLUGIN_EVENT.FWD_LIST;
break;
/* Announcements list */
case 'announcements':
janode_event.data.announcements = message_data.announcements;
janode_event.event = PLUGIN_EVENT.ANNOUNCEMENTS_LIST;
break;
/* Talking events */
case 'talking':
case 'stopped-talking':
janode_event.data.feed = message_data.id;
janode_event.data.talking = (audiobridge === 'talking');
janode_event.event = message_data.id !== this.feed ? PLUGIN_EVENT.PEER_TALKING : PLUGIN_EVENT.TALKING;
break;
/* Announcement events */
case 'announcement-started':
case 'announcement-stopped':
janode_event.data.file_id = message_data.file_id;
janode_event.event = audiobridge === 'announcement-started' ? PLUGIN_EVENT.ANNOUNCEMENT_STARTED : PLUGIN_EVENT.ANNOUNCEMENT_STOPPED;
break;
/* Generic event (e.g. errors) */
case 'event':
/* AudioBridge error */
if (error) {
janode_event.event = PLUGIN_EVENT.ERROR;
janode_event.data = new Error(`${error_code} ${error}`);
/* In case of error, close a transaction */
this.closeTransactionWithError(transaction, janode_event.data);
break;
}
/* Configuration success for this handle */
if (typeof message_data.result !== 'undefined') {
if (message_data.result === 'ok') {
janode_event.event = PLUGIN_EVENT.CONFIGURED;
}
break;
}
/* This handle or another participant has been resumed */
if (typeof message_data.resumed != 'undefined') {
janode_event.data.feed = message_data.resumed;
if (message_data.participants) {
/* Add participants data */
janode_event.data.participants = message_data.participants.map(({ id, display, muted, setup, talking, suspended }) => {
const peer = {
feed: id,
display,
muted,
setup,
};
if (typeof talking !== 'undefined') peer.talking = talking;
if (typeof suspended !== 'undefined') peer.suspended = suspended;
return peer;
});
}
if (this.feed === janode_event.data.feed) {
janode_event.event = PLUGIN_EVENT.RESUMED;
}
else {
janode_event.event = PLUGIN_EVENT.PEER_RESUMED;
}
break;
}
/* This handle or another participant has been suspended */
if (typeof message_data.suspended != 'undefined') {
janode_event.data.feed = message_data.suspended;
if (this.feed === janode_event.data.feed) {
janode_event.event = PLUGIN_EVENT.SUSPENDED;
}
else {
janode_event.event = PLUGIN_EVENT.PEER_SUSPENDED;
}
break;
}
/* Peer leaving confirmation */
if (typeof message_data.leaving !== 'undefined') {
janode_event.data.feed = message_data.leaving;
janode_event.event = PLUGIN_EVENT.PEER_LEAVING;
break;
}
/* Room muted event */
if (typeof message_data.muted !== 'undefined') {
janode_event.data.muted = message_data.muted;
janode_event.event = PLUGIN_EVENT.ROOM_MUTED;
break;
}
/* This handle or another participant kicked-out */
if (typeof message_data.kicked !== 'undefined') {
janode_event.data.feed = message_data.kicked;
if (this.feed === janode_event.data.feed) {
/* Reset handle status */
this.feed = null;
this.room = null;
janode_event.event = PLUGIN_EVENT.KICKED;
}
else {
janode_event.event = PLUGIN_EVENT.PEER_KICKED;
}
break;
}
/* Configuration events for other participants */
if (typeof message_data.participants !== 'undefined' && message_data.participants.length == 1) {
janode_event.data.feed = message_data.participants[0].id;
if (typeof message_data.participants[0].display === 'string') janode_event.data.display = message_data.participants[0].display;
if (typeof message_data.participants[0].muted !== 'undefined') janode_event.data.muted = message_data.participants[0].muted;
if (typeof message_data.participants[0].setup !== 'undefined') janode_event.data.setup = message_data.participants[0].setup;
if (typeof message_data.participants[0].suspended !== 'undefined') janode_event.data.suspended = message_data.participants[0].suspended;
/* when using "mute"/"unmute" management requests, janus will notify "configured" to all participants, including the involved one */
janode_event.event = janode_event.data.feed !== this.feed ? PLUGIN_EVENT.PEER_CONFIGURED : PLUGIN_EVENT.CONFIGURED;
break;
}
}
/* The event has been handled */
if (janode_event.event) {
/* Try to close the transaction */
this.closeTransactionWithSuccess(transaction, janus_message);
/* If the transaction was not owned, emit the event */
if (emit) this.emit(janode_event.event, janode_event.data);
return janode_event;
}
}
/* The event has not been handled, return a falsy value */
return null;
}
/*----------*/
/* USER API */
/*----------*/
/* These are the APIs that users need to work with the audiobridge plugin */
/**
* Join an audiobridge room.
*
* @param {Object} params
* @param {number|string} params.room - The room to join
* @param {number|string} [params.feed] - The feed identifier for the participant, picked by Janus if omitted
* @param {string} [params.display] - The display name to use
* @param {boolean} [params.muted] - True to join in muted status
* @param {string} [params.pin] - The pin needed to join
* @param {string} [params.token] - The token to use when joining
* @param {number} [params.quality] - The opus quality for the encoder
* @param {number} [params.volume] - The percent volume
* @param {boolean} [params.record] - True to enable recording
* @param {string} [params.filename] - The recording filename
* @param {boolean} [params.suspended] - True to join in suspended status
* @param {boolean} [params.pause_events] - Wheter to pause notification events for suspended participants
* @param {module:audiobridge-plugin~RtpParticipant} [params.rtp_participant] - Set a descriptor object if you need a RTP participant
* @param {string} [params.group] - The group to assign to this participant
* @param {boolean} [params.generate_offer] - True to get Janus to send the SDP offer.
* @param {string} [params.codec] - The codec to be used among opus, pcma or pcmu (Janus will default to opus)
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_JOINED>}
*/
async join({ room, feed, display, muted, pin, token, quality, volume, record, filename, suspended, pause_events, rtp_participant, group, generate_offer, codec }) {
const body = {
request: REQUEST_JOIN,
room,
};
if (typeof feed === 'string' || typeof feed === 'number') body.id = feed;
if (typeof display === 'string') body.display = display;
if (typeof muted === 'boolean') body.muted = muted;
if (typeof pin === 'string') body.pin = pin;
if (typeof token === 'string') body.token = token;
if (typeof quality === 'number') body.quality = quality;
if (typeof volume === 'number') body.volume = volume;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof suspended === 'boolean') body.suspended = suspended;
if (typeof pause_events === 'boolean') body.pause_events = pause_events;
if (typeof rtp_participant === 'object' && rtp_participant) body.rtp = rtp_participant;
if (typeof group === 'string') body.group = group;
if (typeof generate_offer === 'boolean') body.generate_offer = generate_offer;
if (typeof codec === 'string') body.codec = codec;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.JOINED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Configure an audiobridge handle.
*
* @param {Object} params
* @param {string} [params.display] - The display name to use
* @param {boolean} [params.muted] - Set muted status
* @param {number} [params.quality] - Set opus quality
* @param {number} [params.bitrate] - Set received bitrate (overrides room default)
* @param {number} [params.volume] - Set volume percent
* @param {boolean} [params.record] - Enable recording
* @param {string} [params.filename] - Set recording filename
* @param {number} [params.expected_loss] - Set a new expected_loss value for this participant (overrides room default)
* @param {number} [params.prebuffer] - Set a new prebuffer value (overrides room default)
* @param {string} [params.group] - Set the group that the participant belongs to
* @param {module:audiobridge-plugin~RtpParticipant} [params.rtp_participant] - Set a descriptor object if you need a RTP participant
* @param {RTCSessionDescription} [params.jsep=null] - JSEP offer/answer to be sent to Janus
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_CONFIGURED>}
*/
async configure({ display, muted, quality, bitrate, volume, record, filename, expected_loss, prebuffer, group, rtp_participant, jsep = null }) {
const body = {
request: REQUEST_CONFIGURE,
};
if (typeof display === 'string') body.display = display;
if (typeof muted === 'boolean') body.muted = muted;
if (typeof quality === 'number') body.quality = quality;
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof volume === 'number') body.volume = volume;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof expected_loss === 'number') body.expected_loss = expected_loss;
if (typeof prebuffer === 'number') body.prebuffer = prebuffer;
if (typeof group === 'string') body.group = group;
if (typeof rtp_participant === 'object' && rtp_participant) body.rtp = rtp_participant;
const response = await this.message(body, jsep);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.CONFIGURED) {
/* Janus does not reply with configured data, so we need to re-use the requested configuration */
/* Use feed and room from handle status */
evtdata.feed = this.feed;
evtdata.room = this.room;
if (typeof body.display !== 'undefined') evtdata.display = body.display;
if (typeof body.muted !== 'undefined') evtdata.muted = body.muted;
if (typeof body.quality !== 'undefined') evtdata.quality = body.quality;
if (typeof body.volume !== 'undefined') evtdata.volume = body.volume;
if (typeof body.record !== 'undefined') evtdata.record = body.record;
if (typeof body.filename !== 'undefined') evtdata.filename = body.filename;
if (typeof body.prebuffer !== 'undefined') evtdata.prebuffer = body.prebuffer;
if (typeof body.group !== 'undefined') evtdata.group = body.group;
if (typeof body.rtp !== 'undefined') evtdata.rtp_participant = body.rtp;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Request an audiobridge handle hangup.
*
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_AUDIO_HANGINGUP>}
*
*/
async audioHangup() {
const body = {
request: REQUEST_AUDIO_HANGUP,
};
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.AUDIO_HANGINGUP)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Leave an audiobridge room.
*
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_LEAVING>}
*/
async leave() {
const body = {
request: REQUEST_LEAVE,
};
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.LEAVING)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/*----------------*/
/* Management API */
/*----------------*/
/* These are the APIs needed to manage audiobridge resources (rooms, forwarders ...) */
/**
* List participants inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The room where to execute the list
* @param {string} [params.secret] - The optional secret needed for managing the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_PARTICIPANTS_LIST>}
*/
async listParticipants({ room, secret }) {
const body = {
request: REQUEST_LIST_PARTICIPANTS,
room,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.PARTICIPANTS_LIST)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Kick an user out from a room.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {number|string} params.feed - The feed to kick out
* @param {string} [params.secret] - The optional secret needed for managing the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_KICK_RESPONSE>}
*/
async kick({ room, feed, secret }) {
const body = {
request: REQUEST_KICK,
room,
id: feed,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
/* Add data missing from Janus response */
evtdata.room = body.room;
evtdata.feed = body.id;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Check if a room exists.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_EXISTS>}
*/
async exists({ room }) {
const body = {
request: REQUEST_EXISTS,
room,
};
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.EXISTS)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* List available audiobridge rooms.
*
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_ROOMS_LIST>}
*/
async list() {
const body = {
request: REQUEST_LIST_ROOMS,
};
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.ROOMS_LIST)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Create an audiobridge room.
*
* @param {Object} params
* @param {number|string} params.room - The room identifier
* @param {string} [params.description] - A room description
* @param {boolean} [params.permanent] - Set to true to persist the room in the Janus config file
* @param {number} [params.sampling_rate] - The sampling rate (bps) to be used in the room
* @param {number} [params.bitrate] - The bitrate (bps) to be used in the room, if missing the encoder decides
* @param {boolean} [params.is_private] - Set room as private (hidden in list)
* @param {string} [params.secret] - The secret to be used when managing the room
* @param {string} [params.pin] - The ping needed for joining the room
* @param {string} [params.admin_key] - The admin key needed for invoking the API
* @param {boolean} [params.record] - True to record the mixed audio
* @param {string} [params.filename] - The recording filename
* @param {string} [params.rec_dir] - The optional recording folder
* @param {boolean} [params.talking_events] - True to enable talking events
* @param {number} [params.talking_level_threshold] - Audio level threshold for talking events in the range [0, 127]
* @param {number} [params.talking_packets_threshold] - Audio packets threshold for talking events
* @param {number} [params.expected_loss] - The expected loss percentage in the audiobridge, if > 0 enables FEC
* @param {number} [params.prebuffer] - The prebuffer to use for every participant
* @param {boolean} [params.allow_rtp] - Allow plain RTP participants
* @param {string[]} [params.groups] - The available groups in the room
* @param {boolean} [params.denoise] - Enable denoising with rnnoise for all participants
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_CREATED>}
*/
async create({ room, description, permanent, sampling_rate, bitrate, is_private, secret, pin, admin_key, record, filename, rec_dir,
talking_events, talking_level_threshold, talking_packets_threshold, expected_loss, prebuffer, allow_rtp, groups, denoise }) {
const body = {
request: REQUEST_CREATE,
room,
};
if (typeof description === 'string') body.description = description;
if (typeof permanent === 'boolean') body.permanent = permanent;
if (typeof sampling_rate === 'number') body.sampling = sampling_rate;
if (typeof bitrate === 'number') body.default_bitrate = bitrate;
if (typeof is_private === 'boolean') body.is_private = is_private;
if (typeof secret === 'string') body.secret = secret;
if (typeof pin === 'string') body.pin = pin;
if (typeof admin_key === 'string') body.admin_key = admin_key;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.record_file = filename;
if (typeof rec_dir === 'string') body.record_dir = rec_dir;
if (typeof talking_events === 'boolean') body.audiolevel_event = talking_events;
if (typeof talking_level_threshold === 'number' && talking_level_threshold >= 0 && talking_level_threshold <= 127) body.audio_level_average = talking_level_threshold;
if (typeof talking_packets_threshold === 'number' && talking_packets_threshold > 0) body.audio_active_packets = talking_packets_threshold;
if (typeof expected_loss === 'number') body.default_expectedloss = expected_loss;
if (typeof prebuffer === 'number') body.default_prebuffering = prebuffer;
if (typeof allow_rtp === 'boolean') body.allow_rtp_participants = allow_rtp;
if (Array.isArray(groups)) body.groups = groups;
if (typeof denoise === 'boolean') body.denoise = denoise;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.CREATED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Destroy an audiobridge room.
*
* @param {Object} params
* @param {number|string} params.room - The room to destroy
* @param {boolean} [params.permanent] - Set to true to remove the room from the Janus config file
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_DESTROYED>}
*/
async destroy({ room, permanent, secret }) {
const body = {
request: REQUEST_DESTROY,
room,
};
if (typeof permanent === 'boolean') body.permanent = permanent;
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.DESTROYED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Enable/disable mixed audio recording.
*
* @param {Object} params
* @param {number|string} params.room - The room identifier
* @param {boolean} params.record - Enable/disable recording
* @param {string} [params.secret] - The secret to be used when managing the room
* @param {string} [params.filename] - The recording filename
* @param {string} [params.rec_dir] - The optional recording folder
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_RECORDING>}
*/
async enableRecording({ room, record, filename, rec_dir, secret }) {
const body = {
request: REQUEST_RECORDING,
room,
record,
};
if (typeof filename === 'string') body.record_file = filename;
if (typeof rec_dir === 'string') body.record_dir = rec_dir;
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.RECORDING) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Edit an audiobridge token list.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {"enable"|"disable"|"add"|"remove"} params.action - The action to perform
* @param {string[]} params.list - The list of tokens to add/remove
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_ALLOWED>}
*/
async allow({ room, action, list, secret }) {
const body = {
request: REQUEST_ALLOW,
room,
action,
};
if (list && list.length > 0) body.allowed = list;
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Start a RTP forwarder.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {boolean} [params.always] - Whether silence should be forwarded when the room is empty
* @param {string} params.host - The host to forward to
* @param {string} [params.host_family] - ipv4|ipv6; by default, first family returned by DNS request
* @param {number} params.audio_port - The port to forward to
* @param {number} [params.ssrc] - The SSRC to use to use when forwarding
* @param {number} [params.ptype] - The payload type to use to use when forwarding
* @param {string} [params.codec] - The codec to use in the forwarder
* @param {string} [params.group] - The group to forward
* @param {string} [params.secret] - The optional secret needed to manage the room
* @param {string} [params.admin_key] - The admin key needed for invoking the API
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_RTP_FWD>}
*/
async startForward({ room, always, host, host_family, audio_port, ssrc, ptype, codec, group, secret, admin_key }) {
const body = {
request: REQUEST_RTP_FWD_START,
room,
};
if (typeof always === 'boolean') body.always_on = always;
if (typeof host === 'string') body.host = host;
if (typeof host_family === 'string') body.host_family = host_family;
if (typeof audio_port === 'number') body.port = audio_port;
if (typeof ssrc === 'number') body.ssrc = ssrc;
if (typeof ptype === 'number') body.ptype = ptype;
if (typeof codec === 'string') body.codec = codec;
if (typeof group === 'string') body.group = group;
if (typeof secret === 'string') body.secret = secret;
if (typeof admin_key === 'string') body.admin_key = admin_key;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.RTP_FWD)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Stop a RTP forwarder.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {number} params.stream - The forwarder identifier to stop
* @param {string} [params.secret] - The optional secret needed to manage the room
* @param {string} [params.admin_key] - The admin key needed for invoking the API
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_RTP_FWD>}
*/
async stopForward({ room, stream, secret, admin_key }) {
const body = {
request: REQUEST_RTP_FWD_STOP,
room,
stream_id: stream,
};
if (typeof secret === 'string') body.secret = secret;
if (typeof admin_key === 'string') body.admin_key = admin_key;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.RTP_FWD)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* List active forwarders.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @param {string} [params.admin_key] - The admin key needed for invoking the API
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_FWD_LIST>}
*/
async listForward({ room, secret, admin_key }) {
const body = {
request: REQUEST_RTP_FWD_LIST,
room,
};
if (typeof secret === 'string') body.secret = secret;
if (typeof admin_key === 'string') body.admin_key = admin_key;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.FWD_LIST)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Mute an user in the audiobridge.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {number|string} params.feed - The feed to mute
* @param {string} [params.secret] - The optional secret needed for managing the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_MUTE_PARTICIPANT_RESPONSE>}
*/
async mute({ room, feed, secret }) {
const body = {
request: REQUEST_MUTE_PARTICIPANT,
room,
id: feed,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
/* Add data missing from Janus response */
evtdata.room = body.room;
evtdata.feed = body.id;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Unmute an user in the audiobridge.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {number|string} params.feed - The feed to unmute
* @param {string} [params.secret] - The optional secret needed for managing the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_UNMUTE_PARTICIPANT_RESPONSE>}
*/
async unmute({ room, feed, secret }) {
const body = {
request: REQUEST_UNMUTE_PARTICIPANT,
room,
id: feed,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
/* Add data missing from Janus response */
evtdata.room = body.room;
evtdata.feed = body.id;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Mute the given room for every participant.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_MUTE_ROOM_RESPONSE>}
*/
async muteRoom({ room, secret }) {
const body = {
request: REQUEST_MUTE_ROOM,
room,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Unmute the given room for every participant.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_UNMUTE_ROOM_RESPONSE>}
*/
async unmuteRoom({ room, secret }) {
const body = {
request: REQUEST_UNMUTE_ROOM,
room,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Suspend an audiobridge participant.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {number|string} params.feed - The feed id to be suspended
* @param {boolean} [params.stop_record] - Whether the recording of this participant should be stopped too
* @param {boolean} [params.pause_events] - Wheter to pause notification events for suspended participants
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_SUSPEND_RESPONSE>}
*/
async suspend({ room, feed, stop_record, pause_events, secret }) {
const body = {
request: REQUEST_SUSPEND_PARTICIPANT,
room,
id: feed
};
if (typeof stop_record === 'boolean') body.stop_record = stop_record;
if (typeof pause_events === 'boolean') body.pause_events = pause_events;
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.feed = feed;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Resume an audiobridge participant after a suspend.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {number|string} params.feed - The feed id to be resumed
* @param {boolean} [params.record] - Whether to start recording this resumed feed
* @param {string} [params.filename] - The recording filename
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_RESUME_RESPONSE>}
*/
async resume({ room, feed, record, filename, secret }) {
const body = {
request: REQUEST_RESUME_PARTICIPANT,
room,
id: feed
};
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.feed = feed;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Play a file inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @param {string} [params.group] - The optional group to play in
* @param {string} [params.file_id] - The optional ID of the announcement
* @param {string} params.filename - The path to the Opus file to play
* @param {boolean} [params.loop] - Whether the file should be played in a loop
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_PLAY_FILE_RESPONSE>}
*/
async playFile({ room, secret, group, file_id, filename, loop }) {
const body = {
request: REQUEST_PLAY_FILE,
room,
};
if (typeof secret === 'string') body.secret = secret;
if (typeof group === 'string') body.group = group;
if (typeof file_id === 'string') body.file_id = file_id;
if (typeof filename === 'string') body.filename = filename;
if (typeof loop === 'boolean') body.loop = loop;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Check whether a file is playing inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @param {string} params.file_id - The involved announcement ID
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_IS_PLAYING_RESPONSE>}
*/
async isPlaying({ room, secret, file_id }) {
const body = {
request: REQUEST_IS_PLAYING,
room,
};
if (typeof secret === 'string') body.secret = secret;
if (typeof file_id === 'string') body.file_id = file_id;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Stop playing a file inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @param {string} params.file_id - The involved announcement ID
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_STOP_FILE_RESPONSE>}
*/
async stopFile({ room, secret, file_id }) {
const body = {
request: REQUEST_STOP_FILE,
room,
};
if (typeof secret === 'string') body.secret = secret;
if (typeof file_id === 'string') body.file_id = file_id;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Stop playing all files inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The involved room
* @param {string} [params.secret] - The optional secret needed to manage the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_STOP_ALL_FILES_RESPONSE>}
*/
async stopAllFiles({ room, secret }) {
const body = {
request: REQUEST_STOP_ALL_FILES,
room,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUCCESS) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* List announcements inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The room where to execute the list
* @param {string} [params.secret] - The optional secret needed for managing the room
* @returns {Promise<module:audiobridge-plugin~AUDIOBRIDGE_EVENT_ANNOUNCEMENTS_LIST>}
*/
async listAnnouncements({ room, secret }) {
const body = {
request: REQUEST_LIST_ANNOUNCEMENTS,
room,
};
if (typeof secret === 'string') body.secret = secret;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.ANNOUNCEMENTS_LIST)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
}
/**
* The payload of the plugin message (cfr. Janus docs).
* {@link https://janus.conf.meetecho.com/docs/audiobridge.html}
*
* @private
* @typedef {Object} AudioBridgeData
*/
/**
* @typedef {Object} RtpParticipant
* @property {string} ip - IP address you want media to be sent to
* @property {number} port - The port you want media to be sent to
* @property {number} payload_type - The payload type to use for RTP packets
*/
/**
* The response event to a join request.
*
* @typedef {Object} AUDIOBRIDGE_EVENT_JOINED
* @property {number|string} room - The involved room
* @property {number|string} feed - The feed identifier
* @property {module:audiobridge-plugin~RtpParticipant} [rtp_participant] - The descriptor in case this is a plain RTP participant
* @property {object[]} participants - The list of participants
* @property {number|string} p