From 40f52ceb56caed40ac116793e1c4723c95fe64b4 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 9 Jun 2025 16:48:38 +0200 Subject: [PATCH 1/8] Add support for Record&Play plugin --- package.json | 1 + src/plugins/recordplay-plugin.js | 403 +++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/plugins/recordplay-plugin.js diff --git a/package.json b/package.json index 202daaa..724bac2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "./handle": "./src/handle.js", "./plugins/audiobridge": "./src/plugins/audiobridge-plugin.js", "./plugins/echotest": "./src/plugins/echotest-plugin.js", + "./plugins/recordplay": "./src/plugins/recordplay-plugin.js", "./plugins/sip": "./src/plugins/sip-plugin.js", "./plugins/streaming": "./src/plugins/streaming-plugin.js", "./plugins/videoroom": "./src/plugins/videoroom-plugin.js" diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js new file mode 100644 index 0000000..484e42c --- /dev/null +++ b/src/plugins/recordplay-plugin.js @@ -0,0 +1,403 @@ +'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_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', + CONFIGURED: 'recordplay_configured', + STATUS: 'recordplay_status', + SUCCESS: 'recordplay_success', + ERROR: 'recordplay_error', +}; + +/** + * The class implementing the Record&Play plugin (ref. {@link https://janus.conf.meetecho.com/docs/recordplay.html}).
+ * + * It extends the base Janode Handle class and overrides the base "handleMessage" method.
+ * + * 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 feed identifier assigned to this handle when it joined the audio bridge. + * + * @type {number|string} + */ + this.feed = null; + + /** + * The identifier of the room the recordplay handle has joined. + * + * @type {number|string} + */ + this.room = null; + } + + /** + * 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') { + janode_event.event = PLUGIN_EVENT.STATUS; + janode_event.data.status = message_data.result.status; + 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; + } + 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} + */ + 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} + */ + 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} + */ + 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=false] - 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=false] - Set whether RED should be negotiated for audio + * @param {boolean} [params.textdata=true] - In case data channels are negotiated, set whether it should be text (default) or binary data + * @param {boolean} [params.update=false] - Set to true for renegotiations + * @param {RTCSessionDescription} params.jsep - JSEP offer to be sent to Janus + * @returns {Promise} + */ + 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.STATUS && evtdata.status === '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.update=false] - Set to true for triggering a renegotiation and an ICE restart + * @returns {Promise} + */ + 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.STATUS && evtdata.status === '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} + */ + 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.STATUS && evtdata.status === 'playing') + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Stop the current recording or playback session. + * + * @returns {Promise} + */ + 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); + } + + +} + +/** + * 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 room recordings request. + * + * @typedef {Object} RECORDPLAY_EVENT_RECORDINGS_LIST + * @property {object[]} list - The list of the recordings as returned by Janus + */ + +/** + * The response event for configure request. + * + * @typedef {Object} RECORDPLAY_EVENT_CONFIGURED + * @property {object{}} settings - The current settings as returned by Janus + */ + +/** + * A recordplay status update event. + * + * @typedef {Object} RECORDPLAY_EVENT_STATUS + * @property {string} status - The current status of the session + * @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 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_STATUS {@link module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_STATUS RECORDPLAY_STATUS} + * @property {string} EVENT.RECORDPLAY_ERROR {@link module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_ERROR RECORDPLAY_ERROR} + */ +export default { + id: PLUGIN_ID, + Handle: RecordPlayHandle, + + EVENT: { + /** + * Update of the status for the active stream. + * + * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_STATUS + * @type {Object} + * @property {string} status - The current status of the stream + * @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 + */ + RECORDPLAY_STATUS: PLUGIN_EVENT.STATUS, + + /** + * Generic recordplay error. + * + * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_ERROR + * @type {Error} + */ + RECORDPLAY_ERROR: PLUGIN_EVENT.ERROR, + }, +}; From ad15ef5a49ce3a307926d502010660b81f5aad83 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 9 Jun 2025 16:55:59 +0200 Subject: [PATCH 2/8] Removed leftovers --- src/plugins/recordplay-plugin.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index 484e42c..642c233 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -48,20 +48,6 @@ class RecordPlayHandle extends Handle { */ 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 recordplay handle has joined. - * - * @type {number|string} - */ - this.room = null; } /** From 19513cf95b4f49080a143b97ab0891a836479b44 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 9 Jun 2025 17:03:55 +0200 Subject: [PATCH 3/8] Removed JSDoc defaults --- src/plugins/recordplay-plugin.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index 642c233..c2a65ae 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -212,14 +212,14 @@ class RecordPlayHandle extends Handle { * @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=false] - Flag the recording as private + * @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=false] - Set whether RED should be negotiated for audio - * @param {boolean} [params.textdata=true] - In case data channels are negotiated, set whether it should be text (default) or binary data - * @param {boolean} [params.update=false] - Set to true for renegotiations + * @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} */ @@ -255,7 +255,7 @@ class RecordPlayHandle extends Handle { * * @param {Object} params * @param {number} params.id - The ID of the recording to replay - * @param {boolean} [params.update=false] - Set to true for triggering a renegotiation and an ICE restart + * @param {boolean} [params.update] - Set to true for triggering a renegotiation and an ICE restart * @returns {Promise} */ async play({ id, restart }) { From 44c719257f55888acd3c17942fd52268c4d26b9d Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 9 Jun 2025 17:13:34 +0200 Subject: [PATCH 4/8] Fixed documentation --- README.md | 3 ++- src/plugins/recordplay-plugin.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 95d1c30..6966403 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The supported Janus plugins are: - Streaming - VideoRoom - SIP +- Record&Play The library is available on [npm](https://www.npmjs.com/package/janode) and the source code is on [github](https://github.com/meetecho/janode). @@ -159,4 +160,4 @@ Then use the npm script: npm run build-docs ``` -Documentation in HTML format will be built under the `docs` folder. \ No newline at end of file +Documentation in HTML format will be built under the `docs` folder. diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index c2a65ae..407af05 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -255,7 +255,7 @@ class RecordPlayHandle extends Handle { * * @param {Object} params * @param {number} params.id - The ID of the recording to replay - * @param {boolean} [params.update] - Set to true for triggering a renegotiation and an ICE restart + * @param {boolean} [params.restart] - Set to true for triggering a renegotiation and an ICE restart * @returns {Promise} */ async play({ id, restart }) { @@ -338,7 +338,7 @@ class RecordPlayHandle extends Handle { * The response event for configure request. * * @typedef {Object} RECORDPLAY_EVENT_CONFIGURED - * @property {object{}} settings - The current settings as returned by Janus + * @property {object} settings - The current settings as returned by Janus */ /** From 14649ff4ced7a0fec11956eb1e079086faa97230 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 9 Jun 2025 17:24:40 +0200 Subject: [PATCH 5/8] Fixed typos --- src/plugins/recordplay-plugin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index 407af05..5287213 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -146,7 +146,7 @@ class RecordPlayHandle extends Handle { * * @param {Object} params * @param {string} [params.admin_key] - The optional admin key needed for invoking the API - * @returns {Promise} + * @returns {Promise} */ async listRecordings({ admin_key }) { const body = { @@ -221,7 +221,7 @@ class RecordPlayHandle extends Handle { * @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} + * @returns {Promise} */ async record({ id, name, is_private, filename, audiocodec, videocodec, videoprofile, opusred, textdata, update, jsep }) { if (!jsep || typeof jsep !== 'object' || jsep && jsep.type !== 'offer') { From 933e7f8ddd161777b7b07ff8d8e7e9961f29cf50 Mon Sep 17 00:00:00 2001 From: Alessandro Toppi Date: Mon, 9 Jun 2025 17:53:17 +0200 Subject: [PATCH 6/8] Refine JsDoc events --- src/plugins/recordplay-plugin.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index 5287213..79a6536 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -167,7 +167,7 @@ class RecordPlayHandle extends Handle { * * @param {Object} params * @param {string} [params.admin_key] - The optional admin key needed for invoking the API - * @returns {Promise} + * @returns {Promise} */ async updateRecordings({ admin_key }) { const body = { @@ -189,7 +189,7 @@ class RecordPlayHandle extends Handle { * @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} + * @returns {Promise} */ async configure({ maxBitrate, keyframeInterval }) { const body = { @@ -316,7 +316,6 @@ class RecordPlayHandle extends Handle { throw (error); } - } /** @@ -334,11 +333,17 @@ class RecordPlayHandle extends Handle { * @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 configure request. * * @typedef {Object} RECORDPLAY_EVENT_CONFIGURED - * @property {object} settings - The current settings as returned by Janus + * @property {object} [settings] - The current settings as returned by Janus */ /** @@ -370,11 +375,7 @@ export default { * Update of the status for the active stream. * * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_STATUS - * @type {Object} - * @property {string} status - The current status of the stream - * @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 + * @type {module:recordplay-plugin~RECORDPLAY_EVENT_STATUS} */ RECORDPLAY_STATUS: PLUGIN_EVENT.STATUS, From 739ddd04939dc54ae2059d8db3210e26d8fdca0a Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 9 Jun 2025 18:13:52 +0200 Subject: [PATCH 7/8] Added pause and resume requests --- src/plugins/recordplay-plugin.js | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index 79a6536..d0a2502 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -17,6 +17,8 @@ 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 */ @@ -298,6 +300,42 @@ class RecordPlayHandle extends Handle { throw (error); } + /** + * Pauses the current recording session. + * + * @returns {Promise} + */ + 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 === 'paused') + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + + /** + * Resumes the current recording session. + * + * @returns {Promise} + */ + async resume() { + const body = { + request: REQUEST_RESUME, + }; + + const response = await this.message(body); + const { event, data: evtdata } = this._getPluginEvent(response);; + if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'resumed') + return evtdata; + const error = new Error(`unexpected response to ${body.request} request`); + throw (error); + } + /** * Stop the current recording or playback session. * From 0243169356958aa44c9f5f3cd07e92d4bbd96839 Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Tue, 10 Jun 2025 15:53:51 +0200 Subject: [PATCH 8/8] Use separate events, instead of a single status one --- src/plugins/recordplay-plugin.js | 134 ++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 18 deletions(-) diff --git a/src/plugins/recordplay-plugin.js b/src/plugins/recordplay-plugin.js index d0a2502..7c97777 100644 --- a/src/plugins/recordplay-plugin.js +++ b/src/plugins/recordplay-plugin.js @@ -25,8 +25,15 @@ const REQUEST_STOP = 'stop'; /* Some of them will be exported in the plugin descriptor */ const PLUGIN_EVENT = { RECORDINGS_LIST: 'recordplay_list', + RECORDING: 'recordplay_recording', CONFIGURED: 'recordplay_configured', - STATUS: 'recordplay_status', + 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', }; @@ -112,12 +119,40 @@ class RecordPlayHandle extends Handle { /* Update for this handle */ if (typeof message_data.result !== 'undefined') { if (typeof message_data.result.status !== 'undefined') { - janode_event.event = PLUGIN_EVENT.STATUS; - janode_event.data.status = message_data.result.status; 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; } @@ -246,7 +281,7 @@ class RecordPlayHandle extends Handle { const response = await this.message(body, jsep); const { event, data: evtdata } = this._getPluginEvent(response); - if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'recording') + if (event === PLUGIN_EVENT.RECORDING) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); @@ -269,7 +304,7 @@ class RecordPlayHandle extends Handle { const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response); - if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'preparing') + if (event === PLUGIN_EVENT.PREPARING) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); @@ -294,7 +329,7 @@ class RecordPlayHandle extends Handle { const response = await this.message(body, jsep); const { event, data: evtdata } = this._getPluginEvent(response);; - if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'playing') + if (event === PLUGIN_EVENT.PLAYING) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); @@ -312,7 +347,7 @@ class RecordPlayHandle extends Handle { const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; - if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'paused') + if (event === PLUGIN_EVENT.PAUSED) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); @@ -330,7 +365,7 @@ class RecordPlayHandle extends Handle { const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; - if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'resumed') + if (event === PLUGIN_EVENT.RESUMED) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); @@ -348,7 +383,7 @@ class RecordPlayHandle extends Handle { const response = await this.message(body); const { event, data: evtdata } = this._getPluginEvent(response);; - if (event === PLUGIN_EVENT.STATUS && evtdata.status === 'stopping') + if (event === PLUGIN_EVENT.STOPPED) return evtdata; const error = new Error(`unexpected response to ${body.request} request`); throw (error); @@ -365,7 +400,7 @@ class RecordPlayHandle extends Handle { */ /** - * The response event for recordplay room recordings request. + * 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 @@ -377,6 +412,15 @@ class RecordPlayHandle extends Handle { * @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. * @@ -385,15 +429,60 @@ class RecordPlayHandle extends Handle { */ /** - * A recordplay status update event. + * The response event for pause request. * - * @typedef {Object} RECORDPLAY_EVENT_STATUS - * @property {string} status - The current status of the session + * @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. * @@ -401,7 +490,8 @@ class RecordPlayHandle extends Handle { * @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_STATUS {@link module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_STATUS RECORDPLAY_STATUS} + * @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 { @@ -410,12 +500,20 @@ export default { EVENT: { /** - * Update of the status for the active stream. + * 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_STATUS - * @type {module:recordplay-plugin~RECORDPLAY_EVENT_STATUS} + * @event module:recordplay-plugin~RecordPlayHandle#event:RECORDPLAY_DONE + * @type {module:recordplay-plugin~RECORDPLAY_EVENT_DONE} */ - RECORDPLAY_STATUS: PLUGIN_EVENT.STATUS, + RECORDPLAY_DONE: PLUGIN_EVENT.DONE, /** * Generic recordplay error.