This is a simple application that implements two different features: it allows you to record a message you send with WebRTC in the format defined in recorded.c (MJR recording) and subsequently replay this recording (or other previously recorded) through WebRTC as well. For more information on how Janus implements recordings natively and the MJR format, refer to the Recordings documentation.
This application aims at showing how easy recording frames sent by a peer is, and how this recording can be re-used directly, without necessarily involving a post-processing process (e.g., through the tool we provide in janus-pp-rec.c). Notice that only audio and video can be recorded and replayed in this plugin: if you're interested in recording data channel messages (which Janus and the .mjr format do support), you should use a different plugin instead.
The configuration process is relatively straightforward: in the simplest configuration, you just choose where the recordings should be saved. The same folder will also be used to list the available recordings that can be replayed.
Notice that, by default, all recordings are public, which means any user connecting to the plugin and asking for a list of recordings will be able to obtain their IDs. Recordings can be marked as private to avoid that, meaning that users will only be able to consume such a recording if they're aware of its ID via out of band mechanisms. Marking a recording as private can be done in two different ways:
private
property to true
in the plugin configuration file, which will automatically mark all new recordings as private by default unless otherwise specified in the API request..nfo
extension for each recording that is saved. This is necessary to map a specific audio .mjr file to a different video .mjr one, as they always get saved in different files. If you want to replay recordings you took in a different application (e.g., the streaming or videoroom plugins) just copy the related files in the folder you configured this plugin to use and create a .nfo file in the same folder to create a mapping, e.g.: [12345678] name = My videoroom recording date = 2014-10-14 17:11:26 private = false audio = videoroom-audio.mjr video = videoroom-video.mjr
Data channel recordings are supported via a data
attribute as well.
The Record&Play API supports several requests, some of which are synchronous and some asynchronous. There are some situations, though, (invalid JSON, invalid request) which will always result in a synchronous error response even for asynchronous requests.
list
, update
and configure
are synchronous requests, which means you'll get a response directly within the context of the transaction. list
lists all the available recordings, while update
forces the plugin to scan the folder of recordings again in case some were added manually and not indexed in the meanwhile. The configure
request can be used to tweak some settings while recording a session.
The record
, play
, start
, pause
, resume
and stop
requests instead are all asynchronous, which means you'll get a notification about their success or failure in an event. record
asks the plugin to start recording a session; play
asks the plugin to prepare the playout of one of the previously recorded sessions; start
starts the actual playout, and stop
stops whatever the session was for, i.e., recording or replaying. Recording sessions can also be dynamically paused and resumed using pause
and resume
: the pauses will be omitted from the recording, meaning recordings will not have holes in them, and will move from one section to the other with no pause.
The list
request has to be formatted as follows:
{ "request" : "list", "admin_key" : "<plugin administrator key; optional>" }
A successful request will result in an array of recordings:
{ "recordplay" : "list", "list": [ // Array of recording objects { // Recording #1 "id": <numeric ID>, "name": "<Name of the recording>", "date": "<Date of the recording>", "audio": "<Audio rec file, if any; optional>", "video": "<Video rec file, if any; optional>", "data": "<Data rec file, if any; optional>", "audio_codec": "<Audio codec, if any; optional>", "video_codec": "<Video codec, if any; optional>" }, <other recordings> ] }
An error instead (and the same applies to all other requests, so this won't be repeated) would provide both an error code and a more verbose description of the cause of the issue:
{ "recordplay" : "event", "error_code" : <numeric ID, check Macros below>, "error" : "<error description as a string>" }
Notice that, as explained previously, the list
request will only return the list of all recordings who were not marked as private. To return the list of private recordings as well, the right admin_key
must be provided as well. In case no admin_key
was configured, then the list of private recordings will never be returned.
The update
request instead has to be formatted as follows:
{ "request" : "update", "admin_key" : "<plugin administrator key; mandatory, if configured>" }
which will always result in an immediate ack ( ok
):
{ "recordplay" : "ok", }
Notice that, if an admin_key
was configured in the configuration file, it is a mandatory property to pass in an update
request as well.
Coming to the asynchronous requests, record
has to be attached to a JSEP offer (failure to do so will result in an error) and has to be formatted as follows:
{ "request" : "record", "id" : <unique numeric ID for the recording; optional, will be chosen by the server if missing> "name" : "<Pretty name for the recording>", "is_private" : <true|false, whether the recording should be listable; the default is what was configured in the plugin config file>, "filename" : "<Base path/name for the file (media type and extension added by the plugin); optional>", "audiocodec" : "<name of the audio codec we prefer for the recording; optional>", "videocodec" : "<name of the video codec we prefer for the recording; optional>", "videoprofile" : "<in case the video codec supports, profile to use (e.g., "2" for VP9, or "42e01f" for H.264); optional>", "opusred" : <true|false, whether RED should be negotiated for audio, if offered; optional (default=false)>, "textdata" : "<in case data channels have to be recorded, whether the data will be text (default) or binary; optional>" }
A successful management of this request will result in a recording
event which will include the unique ID of the recording and a JSEP answer to complete the setup of the associated PeerConnection to record:
{ "recordplay" : "event", "result": { "status" : "recording", "id" : <unique numeric ID>, "is_private" : <true|false, same as the request> } }
Some properties of the recording session can be tweaked dynamically via the configure
request, and has to be formatted as follows:
{ "request" : "configure", "video-bitrate-max", <bitrate cap that should be forced (via REMB) on the recorder>, "video-keyframe-interval", <how often, in seconds, the plugin should send a PLI to the recorder to request a keyframe> }
A successful request will result in a confirmation of :
{ "recordplay" : "configure", "status" : "ok", "settings" : { "video-bitrate-max" : <current value of the property>, "video-keyframe-interval" : <current value of the property> } }
You can pause the recording process (meaning that RTP packets will keep on flowing but will not be recorded to file) using the pause
request:
{ "request" : "pause", }
This will result in a paused
status:
{ "recordplay" : "event", "result": { "status" : "paused", "id" : <unique numeric ID of the paused recording> } }
To resume the recording process you can use the resume
request:
{ "request" : "resume", }
This will result in a resumed
status:
{ "recordplay" : "event", "result": { "status" : "resumed", "id" : <unique numeric ID of the paused recording> } }
A stop
request can interrupt the recording process and tear the associated PeerConnection down:
{ "request" : "stop", }
This will result in a stopped
status:
{ "recordplay" : "event", "result": { "status" : "stopped", "id" : <unique numeric ID of the interrupted recording>, "is_private" : <whether the interrupted recording is private> } }
For what concerns the playout, instead, the process is slightly different: you first choose a recording to replay, using play
, and then start its playout using a start
request. Just as before, a stop
request will interrupt the playout and tear the PeerConnection down. It's very important to point out that no JSEP offer must be sent for replaying a recording: in this case, it will always be the plugin to generate a JSON offer (in response to a play
request), which means you'll then have to provide a JSEP answer within the context of the following start
request which will close the circle. Notice that play
can be used to replay private recordings as well: a recording marked as private is simply not returned when retrieving the list of available recordings, but if the user is aware of a recording ID through other means, then that recording can be replayed as all other non-private recordings.
A play
request has to be formatted as follows:
{ "request" : "play", "id" : <unique numeric ID of the recording to replay> }
This will result in a preparing
status notification which will be attached to the JSEP offer originated by the plugin in order to match the media available in the recording:
{ "recordplay" : "event", "result": { "status" : "preparing", "id" : <unique numeric ID of the recording> } }
A start
request, which as anticipated must be attached to the JSEP answer to the previous offer sent by the plugin, has to be formatted as follows:
{ "request" : "start", }
This will result in a playing
status notification:
{ "recordplay" : "event", "result": { "status" : "playing" } }
Just as before, a stop
request can interrupt the playout process at any time, and tear the associated PeerConnection down:
{ "request" : "stop", }
This will result in a stopped
status:
{ "recordplay" : "event", "result": { "status" : "stopped" } }
If the plugin detects a loss of the associated PeerConnection, whether as a result of a stop
request or because the connection was closed, a done
result notification is triggered to inform the application the recording/playout session is over:
{ "recordplay" : "event", "result": { "status" : "done", "id" : <unique numeric ID of the completed recording>, "is_private" : <whether the completed recording is private> } }