UNPKG

janode

Version:

Meetecho adapter for the Janus WebRTC Server

527 lines (472 loc) 18.1 kB
'use strict'; /** * This module contains the implementation of the Record&Play plugin (ref. {@link https://janus.conf.meetecho.com/docs/recordplay.html}). * @module recordplay-plugin */ import Handle from '../handle.js'; /* The plugin ID exported in the plugin descriptor */ const PLUGIN_ID = 'janus.plugin.recordplay'; /* These are the requests defined for the Janus RecordPlay API */ const REQUEST_LIST = 'list'; const REQUEST_UPDATE = 'update'; const REQUEST_RECORD = 'record'; const REQUEST_PLAY = 'play'; const REQUEST_START = 'start'; const REQUEST_CONFIGURE = 'configure'; const REQUEST_PAUSE = 'pause'; const REQUEST_RESUME = 'resume'; const REQUEST_STOP = '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 = { RECORDINGS_LIST: 'recordplay_list', RECORDING: 'recordplay_recording', CONFIGURED: 'recordplay_configured', PAUSED: 'recordplay_paused', RESUMED: 'recordplay_resumed', PREPARING: 'recordplay_preparing', PLAYING: 'recordplay_playing', STOPPED: 'recordplay_stopped', SLOW_LINK: 'recordplay_slowlink', DONE: 'recordplay_done', SUCCESS: 'recordplay_success', ERROR: 'recordplay_error', }; /** * The class implementing the Record&Play plugin (ref. {@link https://janus.conf.meetecho.com/docs/recordplay.html}).<br> * * It extends the base Janode Handle class and overrides the base "handleMessage" method.<br> * * Moreover it defines many methods to support RecordPlay operations. * * @hideconstructor * @extends module:handle~Handle */ class RecordPlayHandle extends Handle { /** * Create a Janode RecordPlay 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 custom "handleMessage" needed for handling RecordPlay 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.recordplay) { /** * @type {RecordPlayData} */ const message_data = plugindata.data; const { recordplay, 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 (recordplay) { /* Got a list of recordings */ case 'list': /* Recordings list API */ janode_event.event = PLUGIN_EVENT.RECORDINGS_LIST; if (typeof message_data.list !== 'undefined') janode_event.data.list = message_data.list; break; /* Update success */ case 'ok': /* "ok" is treated as "success" */ janode_event.event = PLUGIN_EVENT.SUCCESS; break; /* Configure success */ case 'configure': /* Configure API */ janode_event.event = PLUGIN_EVENT.CONFIGURED; if (typeof message_data.result !== 'undefined') janode_event.data.settings = message_data.result.settings; break; /* Generic event (e.g. errors) */ case 'event': /* RecordPlay 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; } /* Update for this handle */ if (typeof message_data.result !== 'undefined') { if (typeof message_data.result.status !== 'undefined') { if (typeof message_data.result.id !== 'undefined') janode_event.data.id = message_data.result.id; if (typeof message_data.result.is_private !== 'undefined') janode_event.data.is_private = message_data.result.is_private; if (typeof message_data.result.media !== 'undefined') janode_event.data.media = message_data.result.media; if (typeof message_data.result.uplink !== 'undefined') janode_event.data.uplink = message_data.result.uplink; switch (message_data.result.status) { case 'recording': janode_event.event = PLUGIN_EVENT.RECORDING; break; case 'paused': janode_event.event = PLUGIN_EVENT.PAUSED; break; case 'resumed': janode_event.event = PLUGIN_EVENT.RESUMED; break; case 'preparing': janode_event.event = PLUGIN_EVENT.PREPARING; break; case 'playing': janode_event.event = PLUGIN_EVENT.PLAYING; break; case 'stopped': janode_event.event = PLUGIN_EVENT.STOPPED; break; case 'slow_link': janode_event.event = PLUGIN_EVENT.SLOW_LINK; break; case 'done': janode_event.event = PLUGIN_EVENT.DONE; 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 recordplay plugin */ /** * List recordings. * * @param {Object} params * @param {string} [params.admin_key] - The optional admin key needed for invoking the API * @returns {Promise<module:recordplay-plugin~RECORDPLAY_EVENT_RECORDINGS_LIST>} */ async listRecordings({ 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.RECORDINGS_LIST) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Re-index the list of recordings. * * @param {Object} params * @param {string} [params.admin_key] - The optional admin key needed for invoking the API * @returns {Promise<module:recordplay-plugin~RECORDPLAY_EVENT_UPDATE_RESPONSE>} */ async updateRecordings({ admin_key }) { const body = { request: REQUEST_UPDATE, }; 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.SUCCESS) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Configure a recording session. * * @param {Object} params * @param {number} [params.maxBitrate] - The optional bitrate to enforce via REMB * @param {number} [params.keyframeInterval] - The optional keyframe interval to enforce, in ms * @returns {Promise<module:recordplay-plugin~RECORDPLAY_EVENT_CONFIGURED>} */ async configure({ maxBitrate, keyframeInterval }) { const body = { request: REQUEST_CONFIGURE, }; if (typeof maxBitrate === 'number') body['video-bitrate-max'] = maxBitrate; if (typeof keyframeInterval === 'number') body['video-keyframe-interval'] = keyframeInterval; 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); } /** * Start a recording session. * * @param {Object} params * @param {number} [params.id] - The ID to assign to the recording * @param {string} [params.name] - The short description of the recording * @param {boolean} [params.is_private] - Flag the recording as private * @param {string} [params.filename] - Set the base path/filename for the recording * @param {string} [audiocodec] - Set the audio codec to use in the recording * @param {string} [videocodec] - Set the video codec to use in the recording * @param {string} [videoprofile] - Set the video fmtp to use in the recording * @param {boolean} [params.opusred] - Set whether RED should be negotiated for audio * @param {boolean} [params.textdata] - In case data channels are negotiated, set whether it should be text (default) or binary data * @param {boolean} [params.update] - Set to true for renegotiations * @param {RTCSessionDescription} params.jsep - JSEP offer to be sent to Janus * @returns {Promise<module:recordplay-plugin~RECORDPLAY_EVENT_STATUS>} */ async record({ id, name, is_private, filename, audiocodec, videocodec, videoprofile, opusred, textdata, update, jsep }) { if (!jsep || typeof jsep !== 'object' || jsep && jsep.type !== 'offer') { const error = new Error('jsep must be an offer'); return Promise.reject(error); } const body = { request: REQUEST_RECORD, }; if (typeof id === 'number') body.id = id; if (typeof name === 'string') body.name = name; if (typeof is_private === 'boolean') body.is_private = is_private; if (typeof filename === 'string') body.filename = filename; if (typeof audiocodec === 'string') body.audiocodec = audiocodec; if (typeof videocodec === 'string') body.videocodec = videocodec; if (typeof videoprofile === 'string') body.videoprofile = videoprofile; if (typeof opusred === 'boolean') body.opusred = opusred; if (typeof textdata === 'boolean') body.textdata = textdata; if (typeof update === 'boolean') body.update = update; const response = await this.message(body, jsep); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.RECORDING) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Play an existing recording. * * @param {Object} params * @param {number} params.id - The ID of the recording to replay * @param {boolean} [params.restart] - Set to true for triggering a renegotiation and an ICE restart * @returns {Promise<module:recordplay-plugin~RECORDPLAY_EVENT_STATUS>} */ async play({ id, restart }) { const body = { request: REQUEST_PLAY, id }; if (typeof restart === 'boolean') body.restart = restart; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); if (event === PLUGIN_EVENT.PREPARING) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Start a playback session. * * @param {Object} params * @param {RTCSessionDescription} params.jsep * @returns {Promise<module:streaming-plugin~RECORDPLAY_EVENT_STATUS>} */ async start({ jsep }) { if (!jsep || 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, }; const response = await this.message(body, jsep); const { event, data: evtdata } = this._getPluginEvent(response);; if (event === PLUGIN_EVENT.PLAYING) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Pauses the current recording session. * * @returns {Promise<module:streaming-plugin~RECORDPLAY_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.PAUSED) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Resumes the current recording session. * * @returns {Promise<module:streaming-plugin~RECORDPLAY_EVENT_STATUS>} */ async resume() { const body = { request: REQUEST_RESUME, }; const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; if (event === PLUGIN_EVENT.RESUMED) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); } /** * Stop the current recording or playback session. * * @returns {Promise<module:streaming-plugin~RECORDPLAY_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.STOPPED) 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/recordplay.html} * * @private * @typedef {Object} RecordPlayData */ /** * The response event for recordplay recordings request. * * @typedef {Object} RECORDPLAY_EVENT_RECORDINGS_LIST * @property {object[]} list - The list of the recordings as returned by Janus */ /** * The response event for recordplay update request. * * @typedef {Object} RECORDPLAY_EVENT_UPDATE_RESPONSE */ /** * The response event for record request. * * @typedef {Object} RECORDPLAY_EVENT_RECORDING * @property {number} [id] - The involved recording identifier * @property {boolean} [is_private] - True if the event mentions a private recording * @property {RTCSessionDescription} [jsep] - Optional JSEP from Janus */ /** * The response event for configure request. * * @typedef {Object} RECORDPLAY_EVENT_CONFIGURED * @property {object} [settings] - The current settings as returned by Janus */ /** * The response event for pause request. * * @typedef {Object} RECORDPLAY_EVENT_PAUSED * @property {number} [id] - The involved recording identifier */ /** * The response event for resume request. * * @typedef {Object} RECORDPLAY_EVENT_RESUMED * @property {number} [id] - The involved recording identifier */ /** * The response event for play request. * * @typedef {Object} RECORDPLAY_EVENT_PREPARING * @property {number} [id] - The involved recording identifier * @property {boolean} [is_private] - True if the event mentions a private recording * @property {RTCSessionDescription} [jsep] - Optional JSEP from Janus */ /** * The response event for start request. * * @typedef {Object} RECORDPLAY_EVENT_PLAYING * @property {number} [id] - The involved recording identifier */ /** * The response event for stop request. * * @typedef {Object} RECORDPLAY_EVENT_STOPPED * @property {number} [id] - The involved recording identifier * @property {boolean} [is_private] - True if the event mentions a private recording */ /** * A recordplay slow-link event. * * @typedef {Object} RECORDPLAY_EVENT_SLOW_LINK * @property {string} [media] - Audio or video * @property {number} [current-bitrate] - The current configured max video bitrate * @property {boolean} [uplink] - Whether this is an uplink or downlink event */ /** * A recordplay done event. * * @typedef {Object} RECORDPLAY_EVENT_DONE * @property {number} [id] - The involved recording identifier * @property {boolean} [is_private] - True if the event mentions a private recording */ /** * The exported plugin descriptor. * * @type {Object} * @property {string} id - The plugin identifier used when attaching to Janus * @property {module:recordplay-plugin~RecordPlayHandle} Handle - The custom class implementing the plugin * @property {Object} EVENT - The events emitted by the plugin * @property {string} EVENT.RECORDPLAY_SLOW_LINK {@link module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_STATUS RECORDPLAY_SLOW_LINK} * @property {string} EVENT.RECORDPLAY_DONE {@link module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_STATUS RECORDPLAY_DONE} * @property {string} EVENT.RECORDPLAY_ERROR {@link module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_ERROR RECORDPLAY_ERROR} */ export default { id: PLUGIN_ID, Handle: RecordPlayHandle, EVENT: { /** * Trouble on an active stream. * * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_SLOW_LINK * @type {module:recordplay-plugin~RECORDPLAY_EVENT_SLOW_LINK} */ RECORDPLAY_SLOW_LINK: PLUGIN_EVENT.SLOW_LINK, /** * A recording/playback session is over. * * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_DONE * @type {module:recordplay-plugin~RECORDPLAY_EVENT_DONE} */ RECORDPLAY_DONE: PLUGIN_EVENT.DONE, /** * Generic recordplay error. * * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_ERROR * @type {Error} */ RECORDPLAY_ERROR: PLUGIN_EVENT.ERROR, }, };