janode
Version:
Meetecho adapter for the Janus WebRTC Server
1,228 lines (1,131 loc) • 81.7 kB
JavaScript
'use strict';
/**
* This module contains the implementation of the VideoRoom plugin (ref. {@link https://janus.conf.meetecho.com/docs/videoroom.html}).
* @module videoroom-plugin
*/
import Handle from '../handle.js';
/* The plugin ID exported in the plugin descriptor */
const PLUGIN_ID = 'janus.plugin.videoroom';
/* These are the requests defined for the Janus VideoRoom API */
const REQUEST_JOIN = 'join';
const REQUEST_CONFIGURE = 'configure';
const REQUEST_JOIN_CONFIGURE = 'joinandconfigure';
const REQUEST_LIST_PARTICIPANTS = 'listparticipants';
const REQUEST_ENABLE_RECORDING = 'enable_recording';
const REQUEST_KICK = 'kick';
const REQUEST_START = 'start';
const REQUEST_PAUSE = 'pause';
const REQUEST_SWITCH = 'switch';
const REQUEST_PUBLISH = 'publish';
const REQUEST_UNPUBLISH = 'unpublish';
const REQUEST_LEAVE = 'leave';
const REQUEST_UPDATE = 'update';
const REQUEST_EXISTS = 'exists';
const REQUEST_LIST_ROOMS = 'list';
const REQUEST_CREATE = 'create';
const REQUEST_DESTROY = 'destroy';
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 PTYPE_PUBLISHER = 'publisher';
const PTYPE_LISTENER = 'subscriber';
/* These are the events/responses that the Janode plugin will manage */
/* Some of them will be exported in the plugin descriptor */
const PLUGIN_EVENT = {
PUB_JOINED: 'videoroom_joined',
SUB_JOINED: 'videoroom_subscribed',
PUB_LIST: 'videoroom_publisher_list',
PARTICIPANTS_LIST: 'videoroom_participants_list',
PUB_PEER_JOINED: 'videoroom_publisher_joined',
STARTED: 'videoroom_started',
PAUSED: 'videoroom_paused',
SWITCHED: 'videoroom_switched',
CONFIGURED: 'videoroom_configured',
SLOW_LINK: 'videoroom_slowlink',
DISPLAY: 'videoroom_display',
UNPUBLISHED: 'videoroom_unpublished',
LEAVING: 'videoroom_leaving',
UPDATED: 'videoroom_updated',
KICKED: 'videoroom_kicked',
RECORDING_ENABLED_STATE: 'videoroom_recording_enabled_state',
TALKING: 'videoroom_talking',
SC_SUBSTREAM_LAYER: 'videoroom_sc_substream_layer',
SC_TEMPORAL_LAYERS: 'videoroom_sc_temporal_layers',
ALLOWED: 'videoroom_allowed',
EXISTS: 'videoroom_exists',
ROOMS_LIST: 'videoroom_list',
CREATED: 'videoroom_created',
DESTROYED: 'videoroom_destroyed',
RTP_FWD_STARTED: 'videoroom_rtp_fwd_started',
RTP_FWD_STOPPED: 'videoroom_rtp_fwd_stopped',
RTP_FWD_LIST: 'videoroom_rtp_fwd_list',
SUCCESS: 'videoroom_success',
ERROR: 'videoroom_error',
};
/**
* The class implementing the VideoRoom plugin (ref. {@link https://janus.conf.meetecho.com/docs/videoroom.html}).<br>
*
* It extends the base Janode Handle class and overrides the "handleMessage" method.<br>
*
* Moreover it defines many methods to support VideoRoom operations.<br>
*
* @hideconstructor
* @extends module:handle~Handle
*/
class VideoRoomHandle extends Handle {
/**
* Create a Janode VideoRoom handle.
*
* @param {module:session~Session} session - A reference to the parent session
* @param {number} id - The handle identifier
*/
constructor(session, id) {
super(session, id);
/**
* Either the feed identifier assigned to this publisher handle or the publisher's feed in case this handle is a subscriber.
*
* @type {number|string}
*/
this.feed = null;
/**
* [multistream]
* Either the streams assigned to this publisher handle or the streams subscribed to in case this handle is a subscriber.
*
* @type {object[]}
*/
this.streams = null;
/**
* The identifier of the videoroom the handle has joined.
*
* @type {number|string}
*/
this.room = null;
}
/**
* The custom "handleMessage" needed for handling VideoRoom 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.videoroom) {
/**
* @type {VideoRoomData}
*/
const message_data = plugindata.data;
const { videoroom, 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 (videoroom) {
/* 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;
}
/* Tokens management (add/remove/enable) */
if (typeof message_data.allowed !== 'undefined') {
janode_event.data.list = message_data.allowed;
janode_event.event = PLUGIN_EVENT.ALLOWED;
break;
}
/* Global recording enabled or disabled */
if (typeof message_data.record !== 'undefined') {
janode_event.data.record = message_data.record;
janode_event.event = PLUGIN_EVENT.RECORDING_ENABLED_STATE;
break;
}
/* Generic success event */
janode_event.event = PLUGIN_EVENT.SUCCESS;
break;
/* Publisher joined */
case 'joined':
/* Store room and feed id */
this.room = room;
this.feed = message_data.id;
janode_event.data.feed = message_data.id;
janode_event.data.description = message_data.description;
janode_event.data.private_id = message_data.private_id;
janode_event.data.publishers = message_data.publishers.map(({ id, display, talking, audio_codec, video_codec, simulcast, streams }) => {
const pub = {
feed: id,
display,
};
if (typeof talking !== 'undefined') pub.talking = talking;
if (typeof audio_codec !== 'undefined') pub.audiocodec = audio_codec;
if (typeof video_codec !== 'undefined') pub.videocodec = video_codec;
if (typeof simulcast !== 'undefined') pub.simulcast = simulcast;
/* [multistream] add streams info for this participant */
if (typeof streams !== 'undefined') pub.streams = streams;
return pub;
});
janode_event.event = PLUGIN_EVENT.PUB_JOINED;
break;
/* Subscriber joined */
case 'attached':
/* Store room and feed id */
this.room = room;
if (typeof message_data.id !== 'undefined') {
this.feed = message_data.id;
janode_event.data.feed = message_data.id;
janode_event.data.display = message_data.display;
}
/* [multistream] add streams info to the subscriber joined event */
if (typeof message_data.streams !== 'undefined') {
this.streams = message_data.streams;
janode_event.data.streams = message_data.streams;
}
janode_event.event = PLUGIN_EVENT.SUB_JOINED;
break;
/* Slow-link event */
case 'slow_link':
if (this.feed) janode_event.data.feed = this.feed;
janode_event.data.bitrate = message_data['current-bitrate'];
janode_event.event = PLUGIN_EVENT.SLOW_LINK;
break;
/* Participants list */
case 'participants':
janode_event.data.participants = message_data.participants.map(({ id, display, publisher, talking }) => {
const peer = {
feed: id,
display,
publisher,
};
if (typeof talking !== 'undefined') peer.talking = talking;
return peer;
});
janode_event.event = PLUGIN_EVENT.PARTICIPANTS_LIST;
break;
/* Room created */
case 'created':
janode_event.event = PLUGIN_EVENT.CREATED;
janode_event.data.permanent = message_data.permanent;
break;
/* Room destroyed */
case 'destroyed':
janode_event.event = PLUGIN_EVENT.DESTROYED;
break;
/* RTP forwarding started */
case 'rtp_forward':
janode_event.data.feed = message_data.publisher_id;
if (message_data.rtp_stream) {
const f = message_data.rtp_stream;
const fwd = {
host: f.host,
};
if (f.audio_stream_id) {
fwd.audio_stream = f.audio_stream_id;
fwd.audio_port = f.audio;
if (typeof f.audio_rtcp === 'number') {
fwd.audio_rtcp_port = f.audio_rtcp;
}
}
if (f.video_stream_id) {
fwd.video_stream = f.video_stream_id;
fwd.video_port = f.video;
if (typeof f.video_rtcp === 'number') {
fwd.video_rtcp_port = f.video_rtcp;
}
if (f.video_stream_id_2) {
fwd.video_stream_2 = f.video_stream_id_2;
fwd.video_port_2 = f.video_2;
}
if (f.video_stream_id_3) {
fwd.video_stream_3 = f.video_stream_id_3;
fwd.video_port_3 = f.video_3;
}
}
if (f.data_stream_id) {
fwd.data_stream = f.data_stream_id;
fwd.data_port = f.data;
}
janode_event.data.forwarder = fwd;
}
/* [multistream] */
else if (message_data.forwarders) {
janode_event.data.forwarders = message_data.forwarders.map(f => {
const fwd = {
host: f.host,
};
if (f.type === 'audio') {
fwd.audio_stream = f.stream_id;
fwd.audio_port = f.port;
if (typeof f.remote_rtcp_port === 'number') {
fwd.audio_rtcp_port = f.remote_rtcp_port;
}
}
if (f.type === 'video') {
fwd.video_stream = f.stream_id;
fwd.video_port = f.port;
if (typeof f.remote_rtcp_port === 'number') {
fwd.video_rtcp_port = f.remote_rtcp_port;
}
if (typeof f.substream === 'number') {
fwd.sc_substream_layer = f.substream;
}
}
if (f.type === 'data') {
fwd.data_stream = f.stream_id;
fwd.data_port = f.port;
}
if (typeof f.ssrc === 'number') {
fwd.ssrc = f.ssrc;
}
if (typeof f.pt === 'number') {
fwd.pt = f.pt;
}
if (typeof f.srtp === 'boolean') {
fwd.srtp = f.srtp;
}
return fwd;
});
}
janode_event.event = PLUGIN_EVENT.RTP_FWD_STARTED;
break;
/* RTP forwarding stopped */
case 'stop_rtp_forward':
janode_event.data.feed = message_data.publisher_id;
janode_event.data.stream = message_data.stream_id;
janode_event.event = PLUGIN_EVENT.RTP_FWD_STOPPED;
break;
/* RTP forwarders list */
case 'forwarders':
if (message_data.rtp_forwarders) {
janode_event.data.forwarders = message_data.rtp_forwarders.map(({ publisher_id, rtp_forwarder }) => {
const pub = {
feed: publisher_id,
};
pub.forwarders = rtp_forwarder.map(f => {
const fwd = {
host: f.ip,
};
if (f.audio_stream_id) {
fwd.audio_stream = f.audio_stream_id;
fwd.audio_port = f.port;
if (typeof f.remote_rtcp_port === 'number') {
fwd.audio_rtcp_port = f.remote_rtcp_port;
}
}
if (f.video_stream_id) {
fwd.video_stream = f.video_stream_id;
fwd.video_port = f.port;
if (typeof f.remote_rtcp_port === 'number') {
fwd.video_rtcp_port = f.remote_rtcp_port;
}
if (typeof f.substream === 'number') {
fwd.sc_substream_layer = f.substream;
}
}
if (f.data_stream_id) {
fwd.data_stream = f.data_stream_id;
fwd.data_port = f.port;
}
if (typeof f.ssrc === 'number') {
fwd.ssrc = f.ssrc;
}
if (typeof f.pt === 'number') {
fwd.pt = f.pt;
}
if (typeof f.srtp === 'boolean') {
fwd.srtp = f.srtp;
}
return fwd;
});
return pub;
});
}
/* [multistream] */
else if (message_data.publishers) {
janode_event.data.forwarders = message_data.publishers.map(({ publisher_id, forwarders }) => {
const pub = {
feed: publisher_id,
};
pub.forwarders = forwarders.map(f => {
const fwd = {
host: f.host,
};
if (f.type === 'audio') {
fwd.audio_stream = f.stream_id;
fwd.audio_port = f.port;
if (typeof f.remote_rtcp_port === 'number') {
fwd.audio_rtcp_port = f.remote_rtcp_port;
}
}
if (f.type === 'video') {
fwd.video_stream = f.stream_id;
fwd.video_port = f.port;
if (typeof f.remote_rtcp_port === 'number') {
fwd.video_rtcp_port = f.remote_rtcp_port;
}
if (typeof f.substream === 'number') {
fwd.sc_substream_layer = f.substream;
}
}
if (f.type === 'data') {
fwd.data_stream = f.stream_id;
fwd.data_port = f.port;
}
if (typeof f.ssrc === 'number') {
fwd.ssrc = f.ssrc;
}
if (typeof f.pt === 'number') {
fwd.pt = f.pt;
}
if (typeof f.srtp === 'boolean') {
fwd.srtp = f.srtp;
}
return fwd;
});
return pub;
});
}
janode_event.event = PLUGIN_EVENT.RTP_FWD_LIST;
break;
/* Talking events */
case 'talking':
case 'stopped-talking':
janode_event.data.feed = message_data.id;
janode_event.data.talking = (videoroom === 'talking');
/* [multistream] */
if (typeof message_data.mid !== 'undefined') janode_event.data.mid = message_data.mid;
janode_event.data.audio_level = message_data['audio-level-dBov-avg'];
janode_event.event = PLUGIN_EVENT.TALKING;
break;
/* [multistream] updated event */
case 'updated':
janode_event.data.streams = message_data.streams;
janode_event.event = PLUGIN_EVENT.UPDATED;
break;
/* [multistream] updating event, sent when janus receives another "update" before getting a JSEP answer for the previous one */
case 'updating':
janode_event.data.streams = message_data.streams;
janode_event.event = PLUGIN_EVENT.UPDATED;
break;
/* Generic events (error, notifications ...) */
case 'event':
/* VideoRoom Error */
if (error) {
janode_event.event = PLUGIN_EVENT.ERROR;
janode_event.data = new Error(`${error_code} ${error}`);
janode_event.data._code = error_code;
/* In case of error, close a transaction */
this.closeTransactionWithError(transaction, janode_event.data);
break;
}
/* Participant joined notification (notify_joining) */
if (message_data.joining) {
janode_event.event = PLUGIN_EVENT.PUB_PEER_JOINED;
janode_event.data.feed = message_data.joining.id;
if (message_data.joining.display) janode_event.data.display = message_data.joining.display;
break;
}
/* Publisher list notification */
if (message_data.publishers) {
janode_event.event = PLUGIN_EVENT.PUB_LIST;
janode_event.data.publishers = message_data.publishers.map(({ id, display, talking, audio_codec, video_codec, simulcast, streams }) => {
const pub = {
feed: id,
display,
};
if (typeof talking !== 'undefined') pub.talking = talking;
if (typeof audio_codec !== 'undefined') pub.audiocodec = audio_codec;
if (typeof video_codec !== 'undefined') pub.videocodec = video_codec;
if (typeof simulcast !== 'undefined') pub.simulcast = simulcast;
/* [multistream] add streams info for this participant */
if (typeof streams !== 'undefined') pub.streams = streams;
return pub;
});
break;
}
/* Configuration events (publishing, general configuration) */
if (typeof message_data.configured !== 'undefined') {
janode_event.event = PLUGIN_EVENT.CONFIGURED;
if (this.feed) janode_event.data.feed = this.feed;
/* [multistream] add streams info */
if (typeof message_data.streams !== 'undefined') janode_event.data.streams = message_data.streams;
janode_event.data.configured = message_data.configured;
break;
}
/* Subscribed feed started */
if (typeof message_data.started !== 'undefined') {
janode_event.event = PLUGIN_EVENT.STARTED;
if (this.feed) janode_event.data.feed = this.feed;
janode_event.data.started = message_data.started;
break;
}
/* Subscribed feed paused */
if (typeof message_data.paused !== 'undefined') {
janode_event.event = PLUGIN_EVENT.PAUSED;
if (this.feed) janode_event.data.feed = this.feed;
janode_event.data.paused = message_data.paused;
break;
}
/* Subscribed feed switched */
if (typeof message_data.switched !== 'undefined') {
janode_event.event = PLUGIN_EVENT.SWITCHED;
janode_event.data.switched = message_data.switched;
if (message_data.switched === 'ok') {
if (typeof message_data.id !== 'undefined') {
janode_event.data.from_feed = this.feed;
this.feed = message_data.id;
janode_event.data.to_feed = this.feed;
janode_event.data.display = message_data.display;
}
if (typeof message_data.streams != 'undefined') {
this.streams = message_data.streams;
janode_event.data.streams = message_data.streams;
}
}
break;
}
/* Unpublished own or other feed */
if (typeof message_data.unpublished !== 'undefined') {
janode_event.event = PLUGIN_EVENT.UNPUBLISHED;
janode_event.data.feed = (message_data.unpublished === 'ok') ? this.feed : message_data.unpublished;
if (message_data.display) janode_event.data.display = message_data.display;
break;
}
/* Leaving confirmation */
if (typeof message_data.leaving !== 'undefined') {
janode_event.event = PLUGIN_EVENT.LEAVING;
janode_event.data.feed = (message_data.leaving === 'ok') ? this.feed : message_data.leaving;
if (message_data.reason) janode_event.data.reason = message_data.reason;
if (message_data.display) janode_event.data.display = message_data.display;
break;
}
/* Display name changed event */
if (typeof message_data.display !== 'undefined' && typeof message_data.switched === 'undefined') {
janode_event.event = PLUGIN_EVENT.DISPLAY;
janode_event.data.feed = message_data.id;
janode_event.data.display = message_data.display;
break;
}
/* Participant kicked out */
if (typeof message_data.kicked !== 'undefined') {
janode_event.event = PLUGIN_EVENT.KICKED;
janode_event.data.feed = message_data.kicked;
break;
}
/* Participant left (for subscribers "leave") */
if (typeof message_data.left !== 'undefined') {
janode_event.event = PLUGIN_EVENT.LEAVING;
if (this.feed) janode_event.data.feed = this.feed;
break;
}
/* Simulcast substream layer switch */
if (typeof message_data.substream !== 'undefined') {
janode_event.event = PLUGIN_EVENT.SC_SUBSTREAM_LAYER;
if (this.feed) janode_event.data.feed = this.feed;
/* [multistream] */
if (typeof message_data.mid !== 'undefined') janode_event.data.mid = message_data.mid;
janode_event.data.sc_substream_layer = message_data.substream;
break;
}
/* Simulcast temporal layers switch */
if (typeof message_data.temporal !== 'undefined') {
janode_event.event = PLUGIN_EVENT.SC_TEMPORAL_LAYERS;
if (this.feed) janode_event.data.feed = this.feed;
/* [multistream] */
if (typeof message_data.mid !== 'undefined') janode_event.data.mid = message_data.mid;
janode_event.data.sc_temporal_layers = message_data.temporal;
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 videoroom plugin */
/**
* Join a videoroom as publisher.
*
* @param {Object} params
* @param {number|string} params.room - The room to join to
* @param {number|string} [params.feed] - The feed identifier to use, if missing it is picked by Janus
* @param {boolean} [params.audio] - True to request audio relaying
* @param {boolean} [params.video] - True to request video relaying
* @param {boolean} [params.data] - True to request datachannel relaying
* @param {string} [params.display] - The display name to use
* @param {number} [params.bitrate] - Bitrate cap
* @param {string} [params.token] - The optional token needed to join the room
* @param {string} [params.pin] - The optional pin needed to join the room
* @param {boolean} [params.record] - Enable the recording
* @param {string} [params.filename] - If recording, the base path/file to use for the recording
* @param {object[]} [params.descriptions] - [multistream] The descriptions object, can define a description for the tracks separately e.g. track mid:0 'Video Camera', track mid:1 'Screen'
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_PUB_JOINED>}
*/
async joinPublisher({ room, feed, audio, video, data, bitrate, record, filename, display, token, pin, descriptions }) {
const body = {
request: REQUEST_JOIN,
ptype: PTYPE_PUBLISHER,
room,
};
if (typeof feed === 'string' || typeof feed === 'number') body.id = feed;
if (typeof display === 'string') body.display = display;
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof token === 'string') body.token = token;
if (typeof pin === 'string') body.pin = pin;
/* [multistream] */
if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.PUB_JOINED) {
if (body.display) evtdata.display = body.display;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Join and configure videoroom handle as publisher.
*
* @param {Object} params
* @param {number|string} params.room - The room to join to
* @param {number|string} [params.feed] - The feed identifier to use, if missing it is picked by Janus
* @param {boolean} [params.audio] - True to request audio relaying
* @param {boolean} [params.video] - True to request video relaying
* @param {boolean} [params.data] - True to request datachannel relaying
* @param {string} [params.display] - The display name to use
* @param {number} [params.bitrate] - Bitrate cap
* @param {string} [params.token] - The optional token needed to join the room
* @param {string} [params.pin] - The optional pin needed to join the room
* @param {boolean} [params.record] - Enable the recording
* @param {string} [params.filename] - If recording, the base path/file to use for the recording
* @param {boolean} [params.e2ee] - True to notify end-to-end encryption for this connection
* @param {object[]} [params.descriptions] - [multistream] The descriptions object, can define a description for the tracks separately e.g. track mid:0 'Video Camera', track mid:1 'Screen'
* @param {RTCSessionDescription} [params.jsep] - The JSEP offer
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_PUB_JOINED>}
*/
async joinConfigurePublisher({ room, feed, audio, video, data, bitrate, record, filename, display, token, pin, e2ee, descriptions, jsep }) {
const body = {
request: REQUEST_JOIN_CONFIGURE,
ptype: PTYPE_PUBLISHER,
room,
};
if (typeof feed === 'string' || typeof feed === 'number') body.id = feed;
if (typeof display === 'string') body.display = display;
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof token === 'string') body.token = token;
if (typeof pin === 'string') body.pin = pin;
if (typeof e2ee === 'boolean' && jsep) jsep.e2ee = e2ee;
/* [multistream] */
if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
const response = await this.message(body, jsep).catch(e => {
/* Cleanup the WebRTC status in Janus in case of errors when publishing */
/*
*
* JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED 428
* JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT 429
* JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT 430
* JANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE 431
* JANUS_VIDEOROOM_ERROR_PUBLISHERS_FULL 432
* JANUS_VIDEOROOM_ERROR_UNAUTHORIZED 433
* JANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED 434
* JANUS_VIDEOROOM_ERROR_NOT_PUBLISHED 435
* JANUS_VIDEOROOM_ERROR_ID_EXISTS 436
* JANUS_VIDEOROOM_ERROR_INVALID_SDP 437
*
*/
if (jsep && e._code && e._code >= 429 && e._code <= 437 && e._code != 434)
this.hangup().catch(() => { });
throw e;
});
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.PUB_JOINED) {
if (body.display) evtdata.display = body.display;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Configure a publisher or subscriber handle.<br>
* Room is detected from the context since a handle must have joined before.<br>
* Can also be used by publishers to publish a feed.<br>
*
* Use this API also to trigger ICE restarts. Publishers can omit the
* restart/update flags, while subscribers need to use them to force
* the operation.
*
* @param {Object} params
* @param {boolean} [params.audio] - True to request audio relaying
* @param {boolean} [params.video] - True to request video relaying
* @param {boolean} [params.data] - True to request datachannel relaying
* @param {string} [params.display] - The display name to use (publishers only)
* @param {number} [params.bitrate] - Bitrate cap (publishers only)
* @param {boolean} [params.record] - True to record the feed (publishers only)
* @param {string} [params.filename] - If recording, the base path/file to use for the recording (publishers only)
* @param {boolean} [params.restart] - Set to force a ICE restart
* @param {boolean} [params.update] - Set to force a renegotiation
* @param {object[]} [params.streams] - [multistream] The streams object, each stream includes mid, keyframe, send, min_delay, max_delay
* @param {object[]} [params.descriptions] - [multistream] The descriptions object, can define a description for the tracks separately e.g. track mid:0 'Video Camera', track mid:1 'Screen'
* @param {number} [params.sc_substream_layer] - Substream layer to receive (0-2), in case simulcasting is enabled (subscribers only)
* @param {number} [params.sc_substream_fallback_ms] - How much time in ms without receiving packets will make janus drop to the substream below (subscribers only)
* @param {number} [params.sc_temporal_layers] - Temporal layers to receive (0-2), in case VP8 simulcasting is enabled (subscribers only)
* @param {boolean} [params.e2ee] - True to notify end-to-end encryption for this connection
* @param {RTCSessionDescription} [params.jsep] - The JSEP offer (publishers only)
* @param {boolean} [params.keyframe] - True to request a keyframe (publishers only)
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_CONFIGURED>}
*/
async configure({ audio, video, data, bitrate, record, filename, display, restart, update, streams, descriptions, sc_substream_layer, sc_substream_fallback_ms, sc_temporal_layers, e2ee, jsep, keyframe }) {
const body = {
request: REQUEST_CONFIGURE,
};
/* [multistream] */
if (streams && Array.isArray(streams)) {
body.streams = streams;
}
else {
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
if (typeof sc_substream_layer === 'number') body.substream = sc_substream_layer;
if (typeof sc_substream_fallback_ms === 'number') body.fallback = 1000 * sc_substream_fallback_ms;
if (typeof sc_temporal_layers === 'number') body.temporal = sc_temporal_layers;
}
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof display === 'string') body.display = display;
if (typeof restart === 'boolean') body.restart = restart;
if (typeof update === 'boolean') body.update = update;
if (typeof e2ee === 'boolean' && jsep) jsep.e2ee = e2ee;
if (typeof keyframe === 'boolean') body.keyframe = keyframe;
/* [multistream] */
if (descriptions && Array.isArray(descriptions)) body.descriptions = descriptions;
const response = await this.message(body, jsep).catch(e => {
/* Cleanup the WebRTC status in Janus in case of errors when publishing */
/*
*
* JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED 428
* JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT 429
* JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT 430
* JANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE 431
* JANUS_VIDEOROOM_ERROR_PUBLISHERS_FULL 432
* JANUS_VIDEOROOM_ERROR_UNAUTHORIZED 433
* JANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED 434
* JANUS_VIDEOROOM_ERROR_NOT_PUBLISHED 435
* JANUS_VIDEOROOM_ERROR_ID_EXISTS 436
* JANUS_VIDEOROOM_ERROR_INVALID_SDP 437
*
*/
if (jsep && e._code && e._code >= 429 && e._code <= 437 && e._code != 434)
this.hangup().catch(() => { });
throw e;
});
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.CONFIGURED && evtdata.configured === 'ok') {
if (body.display) evtdata.display = body.display;
if (typeof body.request === 'boolean') evtdata.restart = body.restart;
if (typeof body.update === 'boolean') evtdata.update = body.update;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Publish a feed in the room.
* Room is detected from the context since a handle must have joined before.
*
* @param {Object} params
* @param {boolean} [params.audio] - True to request audio relaying
* @param {boolean} [params.video] - True to request video relaying
* @param {boolean} [params.data] - True to request datachannel relaying
* @param {string} [params.display] - The display name to use
* @param {number} [params.bitrate] - Bitrate cap
* @param {boolean} [params.record] - True to record the feed
* @param {string} [params.filename] - If recording, the base path/file to use for the recording
* @param {object[]} [params.descriptions] - [multistream] The descriptions object, for each stream you can define description
* @param {boolean} [params.e2ee] - True to notify end-to-end encryption for this connection
* @param {RTCSessionDescription} params.jsep - The JSEP offer
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_CONFIGURED>}
*/
async publish({ audio, video, data, bitrate, record, filename, display, descriptions, e2ee, jsep }) {
if (typeof jsep === 'object' && jsep && jsep.type !== 'offer') {
const error = new Error('jsep must be an offer');
return Promise.reject(error);
}
const body = {
request: REQUEST_PUBLISH,
};
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
if (typeof bitrate === 'number') body.bitrate = bitrate;
if (typeof record === 'boolean') body.record = record;
if (typeof filename === 'string') body.filename = filename;
if (typeof display === 'string') body.display = display;
if (typeof e2ee === 'boolean' && jsep) jsep.e2ee = e2ee;
/* [multistream] */
if (descriptions && Array.isArray(descriptions)) {
body.descriptions = descriptions;
}
const response = await this.message(body, jsep).catch(e => {
/* Cleanup the WebRTC status in Janus in case of errors when publishing */
/*
*
* JANUS_VIDEOROOM_ERROR_NO_SUCH_FEED 428
* JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT 429
* JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT 430
* JANUS_VIDEOROOM_ERROR_INVALID_SDP_TYPE 431
* JANUS_VIDEOROOM_ERROR_PUBLISHERS_FULL 432
* JANUS_VIDEOROOM_ERROR_UNAUTHORIZED 433
* JANUS_VIDEOROOM_ERROR_ALREADY_PUBLISHED 434
* JANUS_VIDEOROOM_ERROR_NOT_PUBLISHED 435
* JANUS_VIDEOROOM_ERROR_ID_EXISTS 436
* JANUS_VIDEOROOM_ERROR_INVALID_SDP 437
*
*/
if (jsep && e._code && e._code >= 429 && e._code <= 437 && e._code != 434)
this.hangup().catch(() => { });
throw e;
});
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.CONFIGURED && evtdata.configured === 'ok') {
if (body.display) evtdata.display = body.display;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Unpublish a feed in the room.
*
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_UNPUBLISHED>}
*/
async unpublish() {
const body = {
request: REQUEST_UNPUBLISH,
};
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.UNPUBLISHED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Join a room as subscriber.
*
* @param {Object} params
* @param {number|string} params.room - The room to join
* @param {number|string} [params.feed=0] - The feed the user wants to subscribe to
* @param {boolean} [params.audio] - Whether or not audio should be relayed
* @param {boolean} [params.video] - Whether or not video should be relayed
* @param {boolean} [params.data] - Whether or not data should be relayed
* @param {boolean} [params.offer_audio] - Whether or not audio should be negotiated
* @param {boolean} [params.offer_video] - Whether or not video should be negotiated
* @param {boolean} [params.offer_data] - Whether or not data should be negotiated
* @param {number} [params.private_id] - The private id to correlate with publisher
* @param {number} [params.sc_substream_layer] - Substream layer to receive (0-2), in case simulcasting is enabled
* @param {number} [params.sc_substream_fallback_ms] - How much time in ms without receiving packets will make janus drop to the substream below
* @param {number} [params.sc_temporal_layers] - Temporal layers to receive (0-2), in case VP8 simulcasting is enabled
* @param {object[]} [params.streams] - [multistream] The streams object, each stream includes feed, mid, send, ...
* @param {boolean} [params.autoupdate] - [multistream] Whether a new SDP offer is sent automatically when a subscribed publisher leaves
* @param {boolean} [params.use_msid] - [multistream] Whether subscriptions should include an msid that references the publisher
* @param {string} [params.token] - The optional token needed
* @param {string} [params.pin] - The optional password required to join the room
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_SUB_JOINED>}
*/
async joinSubscriber({ room, feed, audio, video, data, offer_audio, offer_video, offer_data, private_id, sc_substream_layer, sc_substream_fallback_ms, sc_temporal_layers, streams, autoupdate, use_msid, token, pin }) {
const body = {
request: REQUEST_JOIN,
ptype: PTYPE_LISTENER,
room,
};
/* [multistream] */
if (streams && Array.isArray(streams)) {
body.streams = streams;
}
else {
body.feed = feed;
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
if (typeof offer_audio === 'boolean') body.offer_audio = offer_audio;
if (typeof offer_video === 'boolean') body.offer_video = offer_video;
if (typeof offer_data === 'boolean') body.offer_data = offer_data;
if (typeof sc_substream_layer === 'number') body.substream = sc_substream_layer;
if (typeof sc_substream_fallback_ms === 'number') body.fallback = 1000 * sc_substream_fallback_ms;
if (typeof sc_temporal_layers === 'number') body.temporal = sc_temporal_layers;
}
if (typeof private_id === 'number') body.private_id = private_id;
if (typeof token === 'string') body.token = token;
if (typeof pin === 'string') body.pin = pin;
/* [multistream] */
if (typeof autoupdate === 'boolean') body.autoupdate = autoupdate;
if (typeof use_msid === 'boolean') body.use_msid = use_msid;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SUB_JOINED)
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Alias for "joinSubscriber".
*
* @see module:videoroom-plugin~VideoRoomHandle#joinSubscriber
*/
async joinListener(params) {
return this.joinSubscriber(params);
}
/**
* Start a subscriber stream.
*
* @param {Object} params
* @param {RTCSessionDescription} params.jsep - The JSEP answer
* @param {boolean} [params.e2ee] - True to hint an end-to-end encrypted negotiation
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_STARTED>}
*/
async start({ jsep, e2ee }) {
const body = {
request: REQUEST_START,
};
if (jsep)
jsep.e2ee = (typeof e2ee === 'boolean') ? e2ee : jsep.e2ee;
const response = await this.message(body, jsep);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.STARTED && evtdata.started === 'ok')
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Pause a subscriber feed.
*
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_PAUSED>}
*/
async pause() {
const body = {
request: REQUEST_PAUSE,
};
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.PAUSED && evtdata.paused === 'ok')
return evtdata;
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Switch to another feed.
*
* @param {Object} params
* @param {number|string} [params.to_feed] - The feed id of the new publisher to switch to
* @param {boolean} [params.audio] - True to subscribe to the audio feed
* @param {boolean} [params.video] - True to subscribe to the video feed
* @param {boolean} [params.data] - True to subscribe to the datachannels of the feed
* @param {object[]} [params.streams] - [multistream] streams array containing feed, mid, sub_mid ...
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_SWITCHED>}
*/
async switch({ to_feed, audio, video, data, streams }) {
const body = {
request: REQUEST_SWITCH,
};
/* [multistream] */
if (streams && Array.isArray(streams)) {
body.streams = streams;
}
else {
body.feed = to_feed;
if (typeof audio === 'boolean') body.audio = audio;
if (typeof video === 'boolean') body.video = video;
if (typeof data === 'boolean') body.data = data;
}
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.SWITCHED && evtdata.switched === 'ok') {
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Leave a room.
* Can be used by both publishers and subscribers.
*
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_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);
}
/**
* [multistream] Update a subscription.
*
* @param {Object} params
* @param {object[]} params.subscribe - The array of streams to subscribe
* @param {object[]} params.unsubscribe - The array of streams to unsubscribe
*
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_UPDATED>}
*/
async update({ subscribe, unsubscribe }) {
const body = {
request: REQUEST_UPDATE,
};
if (subscribe && Array.isArray(subscribe)) body.subscribe = subscribe;
if (unsubscribe && Array.isArray(unsubscribe)) body.unsubscribe = unsubscribe;
const response = await this.message(body);
const { event, data: evtdata } = this._getPluginEvent(response);
if (event === PLUGIN_EVENT.UPDATED) {
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/*----------------*/
/* Management API */
/*----------------*/
/* These are the APIs needed to manage videoroom resources (rooms, forwarders ...) */
/**
* List the participants inside a room.
*
* @param {Object} params
* @param {number|string} params.room - The room where the list is being requested
* @param {string} params.secret - The optional secret for the operation
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_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);
}
/**
* Enable or disable recording for all participants in a room while the conference is in progress.
*
* @param {Object} params
* @param {number|string} params.room - The room where the change of recording state is being requested
* @param {string} params.secret - The optional secret for the operation
* @param {boolean} params.record - True starts recording for all participants in an already running conference, false stops the recording
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_RECORDING_ENABLED_STATE>}
*/
async enable_recording({ room, secret, record }) {
const body = {
request: REQUEST_ENABLE_RECORDING,
room,
record
};
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_ENABLED_STATE) {
evtdata.room = body.room;
return evtdata;
}
const error = new Error(`unexpected response to ${body.request} request`);
throw (error);
}
/**
* Kick a publisher out from a room.
*
* @param {Object} params
* @param {number|string} params.room - The room where the kick is being requested
* @param {number|string} params.feed - The identifier of the feed to kick out
* @param {string} params.secret - The optional secret for the operation
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_KICKED>}
*/
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) {
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 room to check
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_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 all the available rooms.
*
* @param {Object} params
* @param {string} [params.admin_key] - The admin key needed for invoking the API
* @returns {Promise<module:videoroom-plugin~VIDEOROOM_EVENT_LIST>}
*/
async list({ admin_key } = {}) {
const body = {
request: REQUEST_LIST_ROOMS,
};
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.ROOMS_LIST)
return evtdata;
const error = new Error(`unexpected response to ${body.r