UNPKG

janode

Version:

Meetecho adapter for the Janus WebRTC Server

792 lines (728 loc) 30 kB
'use strict'; /** * This module contains the implementation of the Streaming plugin (ref. {@link https://janus.conf.meetecho.com/docs/streaming.html}). * @module streaming-plugin */ import Handle from '../handle.js'; /* The plugin ID exported in the plugin descriptor */ const PLUGIN_ID = 'janus.plugin.streaming'; /* These are the requests defined for the Janus Streaming API */ const REQUEST_LIST = 'list'; const REQUEST_INFO = 'info'; const REQUEST_START = 'start'; const REQUEST_WATCH = 'watch'; const REQUEST_STOP = 'stop'; const REQUEST_SWITCH = 'switch'; const REQUEST_PAUSE = 'pause'; const REQUEST_CONFIGURE = 'configure'; const REQUEST_RECORDING = 'recording'; const REQUEST_ENABLE = 'enable'; const REQUEST_DISABLE = 'disable'; const REQUEST_CREATE = 'create'; const REQUEST_DESTROY = 'destroy'; /* Recording actions */ const ACTION_START_REC = 'start'; const ACTION_STOP_REC = 'stop'; /* These are the events/responses that the Janode plugin will manage */ /* Some of them will be exported in the plugin descriptor */ const PLUGIN_EVENT = { STATUS: 'streaming_status', SWITCHED: 'streaming_switched', CONFIGURED: 'streaming_configured', LIST: 'streaming_list', INFO: 'streaming_info', CREATED: 'streaming_created', DESTROYED: 'streaming_destroyed', OK: 'streaming_ok', ERROR: 'streaming_error', }; /** * The class implementing the Streaming plugin (ref. {@link https://janus.conf.meetecho.com/docs/streaming.html}).<br> * * It extends the base Janode Handle class and overrides the "handleMessage" method.<br> * * Moreover it defines many methods to support Streaming operations.<br> * * @hideconstructor * @extends module:handle~Handle */ class StreamingHandle extends Handle { /** * Create a Janode Streaming 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 mountpoint this handle has subscribed to. * * @type {number|string} */ this.mp = null; } /** * The custom "handleMessage" needed for handling Streamiing 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.streaming) { /** * @type {StreamingData} */ const message_data = plugindata.data; const { streaming, error, error_code } = message_data; /* Prepare an object for the output Janode event */ const janode_event = this._newPluginEvent(janus_message); /* 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 (streaming) { /* Ok is a success response */ case 'ok': janode_event.event = PLUGIN_EVENT.OK; break; /* Mountpoint info */ case 'info': { janode_event.event = PLUGIN_EVENT.INFO; janode_event.data = message_data.info; break; } /* Mountpoint list */ case 'list': janode_event.event = PLUGIN_EVENT.LIST; janode_event.data.list = message_data.list; break; /* Mountpoint created */ case 'created': janode_event.event = PLUGIN_EVENT.CREATED; janode_event.data.name = message_data.created; janode_event.data.id = (message_data.stream) ? message_data.stream.id : null; janode_event.data.description = (message_data.stream) ? message_data.stream.description : null; janode_event.data.audio_port = (message_data.stream) ? message_data.stream.audio_port : null; janode_event.data.audio_rtcp_port = (message_data.stream) ? message_data.stream.audio_rtcp_port : null; janode_event.data.video_port = (message_data.stream) ? message_data.stream.video_port : null; janode_event.data.video_port_2 = (message_data.stream) ? message_data.stream.video_port_2 : null; janode_event.data.video_port_3 = (message_data.stream) ? message_data.stream.video_port_3 : null; janode_event.data.video_rtcp_port = (message_data.stream) ? message_data.stream.video_rtcp_port : null; janode_event.data.data_port = (message_data.stream) ? message_data.stream.data_port : null; break; /* Mountpoint destroyed */ case 'destroyed': janode_event.event = PLUGIN_EVENT.DESTROYED; janode_event.data.id = message_data.destroyed; break; /* Generic events (error, result ...) */ case 'event': /* Streaming 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; } /* Result event (status, switched etc.) */ if (typeof message_data.result !== 'undefined') { /* Result -> Status event */ if (typeof message_data.result.status !== 'undefined') { janode_event.event = PLUGIN_EVENT.STATUS; janode_event.data.status = message_data.result.status; if (janode_event.data.status !== 'preparing') { /* Add mp id to preparing event */ janode_event.data.id = this.mp; } if (janode_event.data.status === 'updating') { /* Flag for ongoing restarts */ janode_event.data.restart = true; } break; } /* Result -> Switched event */ if (typeof message_data.result.switched !== 'undefined') { janode_event.event = PLUGIN_EVENT.SWITCHED; janode_event.data.switched = message_data.result.switched; janode_event.data.id = message_data.result.id; break; } /* Result -> Event */ if (typeof message_data.result.event !== 'undefined') { /* Configured */ if (message_data.result.event === 'configured') { janode_event.event = PLUGIN_EVENT.CONFIGURED; break; } break; } break; } 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 streaming plugin */ /** * Subscribe to a mountpoint. * * @param {Object} params * @param {number|string} params.id - The mp id * @param {string} [params.pin] - The optional mp pin * @param {boolean} [params.audio] - True to request audio * @param {boolean} [params.video] - True to request video * @param {boolean} [params.data] - True to request data * @param {RTCSessionDescription} [params.jsep=null] - JSEP offer * @param {boolean} [params.e2ee] - True if end-to-end enecryption is used * @param {boolean} [params.restart=false] - True to trigger a restart * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_STATUS>} */ async watch({ id, pin, audio, video, data, jsep = null, e2ee, restart = false }) { 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_WATCH, id, }; if (typeof pin === 'string') body.pin = '' + pin; if (typeof audio === 'boolean') body.offer_audio = audio; if (typeof video === 'boolean') body.offer_video = video; if (typeof data === 'boolean') body.offer_data = data; if (typeof restart === 'boolean') body.restart = restart; 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.STATUS && (evtdata.status === 'preparing' || evtdata.status === 'updating' || (jsep && evtdata.status === 'starting'))) { /* Set current mp to subscribed id */ this.mp = id; return evtdata; } const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Start a mountpoint stream. * * @param {Object} params * @param {RTCSessionDescription} params.jsep * @param {boolean} [params.e2ee] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_STATUS>} */ async start({ jsep, e2ee }) { if (typeof jsep === 'object' && jsep && jsep.type !== 'answer') { const error = new Error('jsep must be an answer'); return Promise.reject(error); } const body = { request: REQUEST_START, }; 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.STATUS && (evtdata.status === 'starting' || evtdata.status === 'started')) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Pause the current stream. * * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_STATUS>} */ async pause() { const body = { request: REQUEST_PAUSE, }; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'pausing') return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Stop the current stream. * * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_STATUS>} */ async stop() { const body = { request: REQUEST_STOP, }; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'stopping') return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Switch to another mountpoint. * * @param {Object} params * @param {number|string} params.id - The mp id to switch to * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_SWITCHED>} */ async switch({ id }) { const body = { request: REQUEST_SWITCH, id, }; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; if (event === PLUGIN_EVENT.SWITCHED && evtdata.switched === 'ok') { /* Set current mp to the switched id */ this.mp = evtdata.id; return evtdata; } const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Configure an active stream. * * @param {Object} params * @param {boolean} [params.audio] - Enable/disable audio * @param {boolean} [params.video] - Enable/disable video * @param {boolean} [params.data] - Enable/disable data * @param {number} [params.substream] - Substream to switch to (simuclast) * @param {number} [params.temporal] - Temporal layer to switch to (simulcast) * @param {number} [params.fallback] - Fallback timer (simulcast) * @param {number} [params.spatial_layer] - Spatial layer to switch to (svc) * @param {number} [params.temporal_layer] - Temporal layer to switch to (svc) * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_CONFIGURED>} */ async configure({ audio, video, data, substream, temporal, fallback, spatial_layer, temporal_layer }) { const body = { request: REQUEST_CONFIGURE, }; if (typeof audio === 'boolean') body.audio = audio; if (typeof video === 'boolean') body.video = video; if (typeof data === 'boolean') body.data = data; if (typeof substream === 'number') body.substream = substream; if (typeof temporal === 'number') body.temporal = temporal; if (typeof fallback === 'number') body.fallback = fallback; if (typeof spatial_layer === 'number') body.spatial_layer = spatial_layer; if (typeof temporal_layer === 'number') body.temporal_layer = temporal_layer; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.CONFIGURED) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /*----------------*/ /* Management API */ /*----------------*/ /* These are the APIs needed to manage streaming resources (mountpoints, recording ...) */ /** * List all the available mountpoints. * * @param {Object} params * @param {string} [params.admin_key] - The admin key needed for invoking the API * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_LIST>} */ async list({ admin_key } = {}) { const body = { request: REQUEST_LIST, }; 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.LIST) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Get mountpoint info. * * @param {Object} params * @param {number|string} params.id * @param {string} [params.secret] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_INFO>} */ async info({ id, secret }) { const body = { request: REQUEST_INFO, id, }; if (typeof secret === 'string') body.secret = '' + secret; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.INFO) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Start recording on a mountpoint * * @param {Object} params * @param {number|string} params.id * @param {string} [params.audio] - The filename for audio * @param {string} [params.video] - The filename for video * @param {string} [params.data] - The filename for data * @param {object[]} [params.media] - [multistream] The media object, each media includes mid, filename * @param {string} [params.secret] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_OK>} */ async startRecording({ id, audio, video, data, media, secret }) { const body = { request: REQUEST_RECORDING, action: ACTION_START_REC, id, }; /* [multistream] */ if (media && Array.isArray(media)) { body.media = media; } else { if (audio) body.audio = audio; if (video) body.video = video; if (data) body.data = data; } if (typeof secret === 'string') body.secret = '' + secret; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.OK) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Stop recording on a mountpoint. * * @param {Object} params * @param {number|string} params.id * @param {boolean} [params.audio=true] - True to stop recording of audio * @param {boolean} [params.video=true] - True to stop recording of video * @param {boolean} [params.data=true] - True to stop recording of data * @param {object[]} [params.media] - [multistream] The media object, each media includes mid * @param {string} [params.secret] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_OK>} */ async stopRecording({ id, audio = true, video = true, data = true, media, secret }) { const body = { request: REQUEST_RECORDING, action: ACTION_STOP_REC, id, }; /* [multistream] */ if (media && Array.isArray(media)) { body.media = media; } else { body.audio = audio; body.video = video; body.data = data; } if (typeof secret === 'string') body.secret = '' + secret; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.OK) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Enable a mountpoint. * * @param {Object} params * @param {number|string} params.id * @param {string} [params.secret] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_OK>} */ async enable({ id, secret }) { const body = { request: REQUEST_ENABLE, id, }; if (typeof secret === 'string') body.secret = secret; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.OK) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Disable a mountpoint. * * @param {Object} params * @param {number|string} params.id * @param {boolean} [params.stop_recording=true] - True if the user wants to also stop the recording of a disabled moutnpoint * @param {string} [params.secret] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_OK>} */ async disable({ id, stop_recording = true, secret }) { const body = { request: REQUEST_DISABLE, id, stop_recording, }; if (typeof secret === 'string') body.secret = secret; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.OK) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Create a RTP live mountpoint. * * @param {Object} params * @param {number|string} [params.id=0] - The id for the new mountpoint (if omitted Janus will pick one) * @param {string} [params.name] - A name for the mp * @param {string} [params.description] - A description for the mp * @param {string} [params.secret] - The secret that'll be needed to edit this mountpoint * @param {string} [params.pin] - The pin that'll be needed to connect to the new mountpoint * @param {string} [params.admin_key] - The admin key needed for invoking the API * @param {boolean} [params.permanent=false] - True if Janus must persist the mp on a config file * @param {boolean} [params.is_private=false] - Flag the mp as private * @param {boolean} [params.e2ee=false] - True to set a a mp as end to end encrypted * @param {Object} [params.audio] - The audio descriptor for the mp * @param {number} [params.audio.port] - Port used for audio RTP * @param {number} [params.audio.rtcpport] - Port used for audio RTCP * @param {string} [params.audio.mcast] - Multicast address to listen to * @param {number} [params.audio.pt] - Payload type that will be used * @param {string} [params.audio.rtpmap] - rtpmap type that will be used * @param {boolean} [params.audio.skew] - Set skew compensation * @param {Object} [params.video] - The video descriptor for the mp * @param {number} [params.video.port] - Port used for video RTP * @param {number} [params.video.port2] - Port used for video RTP (simulcast layer) * @param {number} [params.video.port3] - Port used for video RTP (simulcast layer) * @param {number} [params.video.rtcpport] - Port used for video RTCP * @param {string} [params.video.mcast] - Multicast address to listen to * @param {number} [params.video.pt] - Payload type that will be used * @param {string} [params.video.rtpmap] - rtpmap that will be used * @param {boolean} [params.video.skew] - Set skew compensation * @param {string} [params.video.fmtp] - fmtp that will be used * @param {boolean} [params.video.buffer] - Enable buffering of the keyframes * @param {Object} [params.data] - The datachannel descriptor for the mp * @param {number} [params.data.port] - Port used for datachannels packets * @param {boolean} [params.data.buffer] - Enable buffering of the datachannels * @param {object[]} [params.media] - [multistream] The media object, each media includes type, mid, port, pt ... * @param {number} [params.threads] - The number of helper threads used in this mp * @param {Object} [params.metadata] - An opaque metadata to add to the mp * @param {number} [params.collision] - The stream collision discarding time in number of milliseconds (0=disabled) * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_CREATED>} */ async createRtpMountpoint({ id = 0, name, description, secret, pin, admin_key, permanent = false, is_private = false, e2ee = false, audio, video, data, media, threads, metadata, collision }) { const body = { request: REQUEST_CREATE, type: 'rtp', id, permanent, is_private, e2ee, collision: 2000, }; if (typeof name === 'string') body.name = name; if (typeof description === 'string') body.description = description; if (typeof secret === 'string') body.secret = secret; if (typeof pin === 'string') body.pin = pin; if (typeof admin_key === 'string') body.admin_key = admin_key; /* [multistream] */ if (media && Array.isArray(media)) { body.media = media; } else { body.audio = false; body.video = false; body.data = false; if (typeof audio === 'object' && audio) { body.audio = true; body.audioport = (typeof audio.port === 'number') ? audio.port : 0; if (typeof audio.rtcpport === 'number') body.audiortcpport = audio.rtcppport; if (typeof audio.mcast === 'string') body.audiomcast = audio.mcast; if (audio.pt) body.audiopt = audio.pt; if (audio.rtpmap) body.audiortpmap = audio.rtpmap; if (typeof audio.skew === 'boolean') body.audioskew = audio.skew; } if (typeof video === 'object' && video) { body.video = true; body.videoport = (typeof video.port === 'number') ? video.port : 0; if (typeof video.rtcpport === 'number') body.videortcpport = video.rtcpport; if (typeof video.mcast === 'string') body.videomcast = video.mcast; if (video.pt) body.videopt = video.pt; if (video.rtpmap) body.videortpmap = video.rtpmap; if (video.fmtp) body.videofmtp = video.fmtp; if (typeof video.buffer === 'boolean') body.videobufferkf = video.buffer; if (typeof video.skew === 'boolean') body.videoskew = video.skew; if (typeof video.port2 === 'number' && typeof video.port3 === 'number') { body.videosimulcast = true; body.videoport2 = video.port2; body.videoport3 = video.port3; } } if (typeof data === 'object' && data) { body.data = true; body.dataport = (typeof data.port === 'number') ? data.port : 0; if (typeof data.buffer === 'boolean') body.databuffermsg = data.buffer; } } if (typeof threads === 'number' && threads > 0) body.threads = threads; if (metadata) body.metadata = metadata; if (typeof collision === 'number') body.collision = collision; 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 a mountpoint. * * @param {Object} params * @param {number|string} params.id * @param {boolean} [params.permanent] * @param {string} [params.secret] * @returns {Promise<module:streaming-plugin~STREAMING_EVENT_DESTROYED>} */ async destroyMountpoint({ id, permanent, secret }) { const body = { request: REQUEST_DESTROY, id, }; 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); } } /** * The payload of the plugin message (cfr. Janus docs). * {@link https://janus.conf.meetecho.com/docs/streaming.html} * * @private * @typedef {Object} StreamingData */ /** * Success response for streaming requests. * * @typedef {Object} STREAMING_EVENT_OK */ /** * Response event for mountpoint info request. * * @typedef {Object} STREAMING_EVENT_INFO */ /** * Response event for mountpoint list request. * * @typedef {Object} STREAMING_EVENT_LIST * @property {object[]} list - The list of mountpoints as returned by Janus */ /** * Response event for mountpoint create request. * * @typedef {Object} STREAMING_EVENT_CREATED * @property {string} name - The name of the mountpoint * @property {number|string} id - The identifier for the mountpoint * @property {string} description - An optional description * @property {number} [audio_port] - The port for RTP audio * @property {number} [audio_rtcp_port] - The port RTCP audio * @property {number} [video_port] - The port for RTP video * @property {number} [video_port_2] - The port for RTP video (simulcast) * @property {number} [video_port_3] - The port for RTP video (simulcast) * @property {number} [video_rtcp_port] - The port for RTCP video * @property {number} [data_port] - The port for datachannels * @property {object[]} [ports] - [multistream] The ports used by the RTP mountpoint * @property {string} ports.type - The type of the port (audio, video, data) * @property {string} ports.mid - The unique mid of the stream * @property {string} [ports.msid] - The msid of the stream * @property {string} [ports.host] - The address in use for this stream * @property {number} [ports.port] - The port on which the plugin listens for this stream's media * @property {number} [ports.rtcp_port] - The local port for receiving and sending RTCP feedback * @property {number} [ports.port_2] - The second local port for receiving video frames (only for RTP video, and simulcasting) * @property {number} [ports.port_3] - The third local port for receiving video frames (only for RTP video, and simulcasting) */ /** * Response event for mountpoint destroy request. * * @typedef {Object} STREAMING_EVENT_DESTROYED * @property {number|string} id - The identifier of the dstroyed mountpoint */ /** * A streaming status update event. * * @typedef {Object} STREAMING_EVENT_STATUS * @property {string} status - The current status of the stream * @property {number|string} [id] - The involved mountpoint identifier * @property {boolean} [restart] - True if the event involves an ICE Restart * @property {boolean} [e2ee] - True if an offered stream is end to end encrypted * @property {RTCSessionDescription} [jsep] - Optional JSEP from Janus */ /** * Response event for mountpoint switch request. * * @typedef {Object} STREAMING_EVENT_SWITCHED * @property {string} switched - The string as returned by Janus * @property {number|string} id - The identifier of the mp that has been switched to */ /** * Response event for configure stream request * * @typedef {Object} STREAMING_EVENT_CONFIGURED */ /** * The exported plugin descriptor. * * @type {Object} * @property {string} id - The plugin identifier used when attaching to Janus * @property {module:streaming-plugin~StreamingHandle} Handle - The custom class implementing the plugin * @property {Object} EVENT - The events emitted by the plugin * @property {string} EVENT.STREAMING_STATUS {@link module:streaming-plugin~StreamingHandle#event:STREAMING_STATUS STREAMING_STATUS} * @property {string} EVENT.STREAMING_ERROR {@link module:streaming-plugin~StreamingHandle#event:STREAMING_ERROR STREAMING_ERROR} */ export default { id: PLUGIN_ID, Handle: StreamingHandle, EVENT: { /** * Update of the status for the active stream. * * @event module:streaming-plugin~StreamingHandle#event:STREAMING_STATUS * @type {Object} * @property {string} status - The current status of the stream * @property {number|string} [id] - The involved mountpoint identifier * @property {boolean} [restart] - True if the event involves an ICE Restart * @property {boolean} [e2ee] - True if an offered stream is end to end encrypted * @property {RTCSessionDescription} [jsep] - Optional JSEP from Janus */ STREAMING_STATUS: PLUGIN_EVENT.STATUS, /** * Generic streaming error. * * @event module:streaming-plugin~StreamingHandle#event:STREAMING_ERROR * @type {Error} */ STREAMING_ERROR: PLUGIN_EVENT.ERROR, }, };