Fork me on GitHub
Loading...
Searching...
No Matches
VideoRoom plugin documentation

This is a plugin implementing a videoconferencing SFU (Selective Forwarding Unit) for Janus, that is an audio/video router. This means that the plugin implements a virtual conferencing room peers can join and leave at any time. This room is based on a Publish/Subscribe pattern. Each peer can publish his/her own live audio/video feeds: this feed becomes an available stream in the room the other participants can attach to. This means that this plugin allows the realization of several different scenarios, ranging from a simple webinar (one speaker, several watchers) to a fully meshed video conference (each peer sending and receiving to and from all the others).

Considering that this plugin allows for several different WebRTC PeerConnections to be on at the same time for the same peer (specifically, each peer potentially has 1 PeerConnection on for publishing and N on for subscriptions from other peers), each peer may need to attach several times to the same plugin for every stream: this means that each peer needs to have at least one handle active for managing its relation with the plugin (joining a room, leaving a room, muting/unmuting, publishing, receiving events), and needs to open a new one each time he/she wants to subscribe to a feed from another publisher participant. The handle used for a subscription, however, would be logically a "slave" to the master one used for managing the room: this means that it cannot be used, for instance, to unmute in the room, as its only purpose would be to provide a context in which creating the recvonly PeerConnection for the subscription to an active publisher participant.

Note
Work is going on to implement SSRC multiplexing (Unified Plan), meaning that in the future you'll be able to use the same Janus handle/VideoRoom subscriber/PeerConnection to receive multiple publishers at the same time.

Rooms to make available are listed in the plugin configuration file. A pre-filled configuration file is provided in conf/janus.plugin.videoroom.jcfg and includes a demo room for testing. The same plugin is also used dynamically (that is, with rooms created on the fly via API) in the Screen Sharing demo as well.

To add more rooms or modify the existing one, you can use the following syntax:

room-<unique room ID>: {
        description = This is my awesome room
        is_private = true|false (private rooms don't appear when you do a 'list' request, default=false)
        secret = <optional password needed for manipulating (e.g. destroying) the room>
        pin = <optional password needed for joining the room>
        require_pvtid = true|false (whether subscriptions are required to provide a valid private_id
                                 to associate with a publisher, default=false)
        signed_tokens = true|false (whether access to the room requires signed tokens; default=false,
                                 only works if signed tokens are used in the core as well)
        publishers = <max number of concurrent senders> (e.g., 6 for a video
                                 conference or 1 for a webinar, default=3)
        bitrate = <max video bitrate for senders> (e.g., 128000)
        bitrate_cap = <true|false, whether the above cap should act as a limit to dynamic bitrate changes by publishers, default=false>,
        fir_freq = <send a FIR to publishers every fir_freq seconds> (0=disable)
        audiocodec = opus|g722|pcmu|pcma|isac32|isac16 (audio codec to force on publishers, default=opus
                                can be a comma separated list in order of preference, e.g., opus,pcmu)
        videocodec = vp8|vp9|h264|av1|h265 (video codec to force on publishers, default=vp8
                                can be a comma separated list in order of preference, e.g., vp9,vp8,h264)
        vp9_profile = VP9-specific profile to prefer (e.g., "2" for "profile-id=2")
        h264_profile = H.264-specific profile to prefer (e.g., "42e01f" for "profile-level-id=42e01f")
        opus_fec = true|false (whether inband FEC must be negotiated; only works for Opus, default=true)
        opus_dtx = true|false (whether DTX must be negotiated; only works for Opus, default=false)
        video_svc = true|false (whether SVC support must be enabled; only works for VP9, default=false)
        audiolevel_ext = true|false (whether the ssrc-audio-level RTP extension must be
                negotiated/used or not for new publishers, default=true)
        audiolevel_event = true|false (whether to emit event to other users or not, default=false)
        audio_active_packets = 100 (number of packets with audio level, default=100, 2 seconds)
        audio_level_average = 25 (average value of audio level, 127=muted, 0='too loud', default=25)
        videoorient_ext = true|false (whether the video-orientation RTP extension must be
                negotiated/used or not for new publishers, default=true)
        playoutdelay_ext = true|false (whether the playout-delay RTP extension must be
                negotiated/used or not for new publishers, default=true)
        transport_wide_cc_ext = true|false (whether the transport wide CC RTP extension must be
                negotiated/used or not for new publishers, default=true)
        record = true|false (whether this room should be recorded, default=false)
        rec_dir = <folder where recordings should be stored, when enabled>
        lock_record = true|false (whether recording can only be started/stopped if the secret
                                is provided, or using the global enable_recording request, default=false)
        notify_joining = true|false (optional, whether to notify all participants when a new
                                participant joins the room. The Videoroom plugin by design only notifies
                                new feeds (publishers), and enabling this may result extra notification
                                traffic. This flag is particularly useful when enabled with require_pvtid
                                for admin to manage listening only participants. default=false)
        require_e2ee = true|false (whether all participants are required to publish and subscribe
                                using end-to-end media encryption, e.g., via Insertable Streams; default=false)
}

Note that recording will work with all codecs except iSAC.

Video Room API

The Video Room 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.

create , destroy , edit , exists, list, allowed, kick , moderate , enable_recording , listparticipants and listforwarders are synchronous requests, which means you'll get a response directly within the context of the transaction. create allows you to create a new video room dynamically, as an alternative to using the configuration file; edit allows you to dynamically edit some room properties (e.g., the PIN); destroy removes a video room and destroys it, kicking all the users out as part of the process; exists allows you to check whether a specific video room exists; finally, list lists all the available rooms, while listparticipants lists all the active (as in currently publishing something) participants of a specific room and their details.

The join , joinandconfigure , configure , publish , unpublish , start , pause , switch and leave requests instead are all asynchronous, which means you'll get a notification about their success or failure in an event. join allows you to join a specific video room, specifying whether that specific PeerConnection will be used for publishing or watching; configure can be used to modify some of the participation settings (e.g., bitrate cap); joinandconfigure combines the previous two requests in a single one (just for publishers); publish can be used to start sending media to broadcast to the other participants, while unpublish does the opposite; start allows you to start receiving media from a publisher you've subscribed to previously by means of a join , while pause pauses the delivery of the media; the switch request can be used to change the source of the media flowing over a specific PeerConnection (e.g., I was watching Alice, I want to watch Bob now) without having to create a new handle for that; finally, leave allows you to leave a video room for good (or, in the case of viewers, definitely closes a subscription).

create can be used to create a new video room, and has to be formatted as follows:

{
        "request" : "create",
        "room" : <unique numeric ID, optional, chosen by plugin if missing>,
        "permanent" : <true|false, whether the room should be saved in the config file, default=false>,
        "description" : "<pretty name of the room, optional>",
        "secret" : "<password required to edit/destroy the room, optional>",
        "pin" : "<password required to join the room, optional>",
        "is_private" : <true|false, whether the room should appear in a list request>,
        "allowed" : [ array of string tokens users can use to join this room, optional],
        ...
}

For the sake of brevity, not all of the available settings are listed here. You can refer to the name of the properties in the configuration file as a reference, as the ones used to programmatically create a new room are exactly the same.

A successful creation procedure will result in a created response:

{
        "videoroom" : "created",
        "room" : <unique numeric ID>,
        "permanent" : <true if saved to config file, false if not>
}

If you requested a permanent room but a false value is returned instead, good chances are that there are permission problems.

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:

{
        "videoroom" : "event",
        "error_code" : <numeric ID, check Macros below>,
        "error" : "<error description as a string>"
}

Notice that, in general, all users can create rooms. If you want to limit this functionality, you can configure an admin admin_key in the plugin settings. When configured, only "create" requests that include the correct admin_key value in an "admin_key" property will succeed, and will be rejected otherwise. Notice that you can optionally extend this functionality to RTP forwarding as well, in order to only allow trusted clients to use that feature.

Once a room has been created, you can still edit some (but not all) of its properties using the edit request. This allows you to modify the room description, secret, pin and whether it's private or not: you won't be able to modify other more static properties, like the room ID, the sampling rate, the extensions-related stuff and so on. If you're interested in changing the ACL, instead, check the allowed message. An edit request has to be formatted as follows:

{
        "request" : "edit",
        "room" : <unique numeric ID of the room to edit>,
        "secret" : "<room secret, mandatory if configured>",
        "new_description" : "<new pretty name of the room, optional>",
        "new_secret" : "<new password required to edit/destroy the room, optional>",
        "new_pin" : "<new password required to join the room, optional>",
        "new_is_private" : <true|false, whether the room should appear in a list request>,
        "new_require_pvtid" : <true|false, whether the room should require private_id from subscribers>,
        "new_bitrate" : <new bitrate cap to force on all publishers (except those with custom overrides)>,
        "new_fir_freq" : <new period for regular PLI keyframe requests to publishers>,
        "new_publishers" : <new cap on the number of concurrent active WebRTC publishers>,
        "new_lock_record" : <true|false, whether recording state can only be changed when providing the room secret>,
        "new_rec_dir" : "<the new path where the next .mjr files should being saved>",
        "permanent" : <true|false, whether the room should be also removed from the config file, default=false>
}

A successful edit procedure will result in an edited response:

{
        "videoroom" : "edited",
        "room" : <unique numeric ID>
}

On the other hand, destroy can be used to destroy an existing video room, whether created dynamically or statically, and has to be formatted as follows:

{
        "request" : "destroy",
        "room" : <unique numeric ID of the room to destroy>,
        "secret" : "<room secret, mandatory if configured>",
        "permanent" : <true|false, whether the room should be also removed from the config file, default=false>
}

A successful destruction procedure will result in a destroyed response:

{
        "videoroom" : "destroyed",
        "room" : <unique numeric ID>
}

This will also result in a destroyed event being sent to all the participants in the video room, which will look like this:

{
        "videoroom" : "destroyed",
        "room" : <unique numeric ID of the destroyed room>
}

You can check whether a room exists using the exists request, which has to be formatted as follows:

{
        "request" : "exists",
        "room" : <unique numeric ID of the room to check>
}

A successful request will result in a success response:

{
        "videoroom" : "success",
        "room" : <unique numeric ID>,
        "exists" : <true|false>
}

You can configure whether to check tokens or add/remove people who can join a room using the allowed request, which has to be formatted as follows:

{
        "request" : "allowed",
        "secret" : "<room secret, mandatory if configured>",
        "action" : "enable|disable|add|remove",
        "room" : <unique numeric ID of the room to update>,
        "allowed" : [
                // Array of strings (tokens users might pass in "join", only for add|remove)
        ]
}

A successful request will result in a success response:

{
        "videoroom" : "success",
        "room" : <unique numeric ID>,
        "allowed" : [
                // Updated, complete, list of allowed tokens (only for enable|add|remove)
        ]
}

If you're the administrator of a room (that is, you created it and have access to the secret) you can kick participants using the kick request. Notice that this only kicks the user out of the room, but does not prevent them from re-joining: to ban them, you need to first remove them from the list of authorized users (see allowed request) and then kick them. The kick request has to be formatted as follows:

{
        "request" : "kick",
        "secret" : "<room secret, mandatory if configured>",
        "room" : <unique numeric ID of the room>,
        "id" : <unique numeric ID of the participant to kick>
}

A successful request will result in a success response:

{
        "videoroom" : "success",
}

As an administrator, you can also forcibly mute/unmute any of the media streams sent by participants (i.e., audio, video and data streams), using the moderate requests. Notice that if the participant is self muted on a stream, and you unmute that stream with moderate, they will NOT be unmuted: you'll simply remove any moderation block that may have been enforced on the participant for that medium themselves. The moderate request has to be formatted as follows:

{
        "request" : "moderate",
        "secret" : "<room secret, mandatory if configured>",
        "room" : <unique numeric ID of the room>,
        "id" : <unique numeric ID of the participant to moderate>,
        "mute_audio" : <true|false, depending on whether or not audio should be muted by the moderator>,
        "mute_video" : <true|false, depending on whether or not video should be muted by the moderator>,
        "mute_data" : <true|false, depending on whether or not data should be muted by the moderator>,
}

A successful request will result in a success response:

{
        "videoroom" : "success",
}

To get a list of the available rooms you can make use of the list request. admin_key is optional. If included and correct, rooms configured/created as private will be included in the list as well.

{
        "request" : "list"
}

A successful request will produce a list of rooms in a success response:

{
        "videoroom" : "success",
        "list" : [              // Array of room objects
                {       // Room #1
                        "room" : <unique numeric ID>,
                        "description" : "<Name of the room>",
                        "pin_required" : <true|false, whether a PIN is required to join this room>,
                        "is_private" : <true|false, whether this room is 'private' (as in hidden) or not>,
                        "max_publishers" : <how many publishers can actually publish via WebRTC at the same time>,
                        "bitrate" : <bitrate cap that should be forced (via REMB) on all publishers by default>,
                        "bitrate_cap" : <true|false, whether the above cap should act as a limit to dynamic bitrate changes by publishers (optional)>,
                        "fir_freq" : <how often a keyframe request is sent via PLI/FIR to active publishers>,
                        "require_pvtid": <true|false, whether subscriptions in this room require a private_id>,
                        "require_e2ee": <true|false, whether end-to-end encrypted publishers are required>,
                        "notify_joining": <true|false, whether an event is sent to notify all participants if a new participant joins the room>,
                        "audiocodec" : "<comma separated list of allowed audio codecs>",
                        "videocodec" : "<comma separated list of allowed video codecs>",
                        "opus_fec": <true|false, whether inband FEC must be negotiated (note: only available for Opus) (optional)>,
                        "opus_dtx": <true|false, whether DTX must be negotiated (note: only available for Opus) (optional)>,
                        "video_svc": <true|false, whether SVC must be done for video (note: only available for VP9 right now) (optional)>,
                        "record" : <true|false, whether the room is being recorded>,
                        "rec_dir" : "<if recording, the path where the .mjr files are being saved>",
                        "lock_record" : <true|false, whether the room recording state can only be changed providing the secret>,
                        "num_participants" : <count of the participants (publishers, active or not; not subscribers)>
                        "audiolevel_ext": <true|false, whether the ssrc-audio-level extension must be negotiated or not for new publishers>,
                        "audiolevel_event": <true|false, whether to emit event to other users about audiolevel>,
                        "audio_active_packets": <amount of packets with audio level for checkup (optional, only if audiolevel_event is true)>,
                        "audio_level_average": <average audio level (optional, only if audiolevel_event is true)>,
                        "videoorient_ext": <true|false, whether the video-orientation extension must be negotiated or not for new publishers>,
                        "playoutdelay_ext": <true|false, whether the playout-delay extension must be negotiated or not for new publishers>,
                        "transport_wide_cc_ext": <true|false, whether the transport wide cc extension must be negotiated or not for new publishers>
                },
                // Other rooms
        ]
}

To get a list of the participants in a specific room, instead, you can make use of the listparticipants request, which has to be formatted as follows:

{
        "request" : "listparticipants",
        "room" : <unique numeric ID of the room>
}

A successful request will produce a list of participants in a participants response:

{
        "videoroom" : "participants",
        "room" : <unique numeric ID of the room>,
        "participants" : [              // Array of participant objects
                {       // Participant #1
                        "id" : <unique numeric ID of the participant>,
                        "display" : "<display name of the participant, if any; optional>",
                        "publisher" : "<true|false, whether user is an active publisher in the room>",
                        "talking" : <true|false, whether user is talking or not (only if audio levels are used)>,
                        "subscribers" : <number of subscribers for this participant, if any>
                },
                // Other participants
        ]
}

This covers almost all the synchronous requests. All the asynchronous requests, plus a couple of additional synchronous requests we'll cover later, refer to participants instead, namely on how they can publish, subscribe, or more in general manage the media streams they may be sending or receiving.

Considering the different nature of publishers and subscribers in the room, and more importantly how you establish PeerConnections in the respective cases, their API requests are addressed in separate subsections.

VideoRoom Publishers

In a VideoRoom, publishers are those participant handles that are able (although may choose not to, more on this later) publish media in the room, and as such become feeds that you can subscribe to.

To specify that a handle will be associated with a publisher, you must use the join request with ptype set to publisher (note that, as it will be explained later, you can also use joinandconfigure for the purpose). The exact syntax of the request is the following:

{
        "request" : "join",
        "ptype" : "publisher",
        "room" : <unique ID of the room to join>,
        "id" : <unique ID to register for the publisher; optional, will be chosen by the plugin if missing>,
        "display" : "<display name for the publisher; optional>",
        "token" : "<invitation token, in case the room has an ACL; optional>"
}

This will add the user to the list of participants in the room, although in a non-active role for the time being. Anyway, this participation allows the user to receive notifications about several aspects of the room on the related handle (including streams as they become available and go away). As such, it can be used even just as a way to get notifications in a room, without the need of ever actually publishing any stream at all (which explains why the "publisher" role may actually be a bit confusing in this context).

A successful join will result in a joined event, which will contain a list of the currently active (as in publishing via WebRTC) publishers, and optionally a list of passive attendees (but only if the room was configured with notify_joining set to TRUE ):

{
        "videoroom" : "joined",
        "room" : <room ID>,
        "description" : <description of the room, if available>,
        "id" : <unique ID of the participant>,
        "private_id" : <a different unique ID associated to the participant; meant to be private>,
        "publishers" : [
                {
                        "id" : <unique ID of active publisher #1>,
                        "display" : "<display name of active publisher #1, if any>",
                        "audio_codec" : "<audio codec used by active publisher #1, if any>",
                        "video_codec" : "<video codec used by active publisher #1, if any>",
                        "audio_moderated" : <set to true if audio has been moderated for this participant>,
                        "video_moderated" : <set to true if video has been moderated for this participant>,
                        "data_moderated" : <set to true if data has been moderated for this participant>,
                        "simulcast" : "<true if the publisher uses simulcast (VP8 and H.264 only)>",
                        "talking" : <true|false, whether the publisher is talking or not (only if audio levels are used)>,
                },
                // Other active publishers
        ],
        "attendees" : [         // Only present when notify_joining is set to TRUE for rooms
                {
                        "id" : <unique ID of attendee #1>,
                        "display" : "<display name of attendee #1, if any>"
                },
                // Other attendees
        ]
}

Notice that the publishers list will of course be empty if no one is currently active in the room. For what concerns the private_id property, it is meant to be used by the user when they create subscriptions, so that the plugin can associate subscriber handles (which are typically anonymous) to a specific participant; they're usually optional, unless required by the room configuration.

As explained, with a simple join you're not an active publisher (there is no WebRTC PeerConnection yet), which means that by default your presence is not notified to other participants. In fact, the publish/subscribe nature of the plugin implies that by default only active publishers are notified, to allow participants to subscribe to existing feeds: notifying all joins/leaves, even those related to who will just lurk, may be overly verbose and chatty, especially in large rooms. Anyway, rooms can be configured to notify those as well, if the notify_joining property is set to true: in that case, regular joins will be notified too, in an event formatted like this:

{
        "videoroom" : "event",
        "room" : <room ID>,
        "joining" : {
                "id" : <unique ID of the new participant>,
                "display" : "<display name of the new participant, if any>"
        }
}

If you're interested in publishing media within a room, you can do that with a publish request. This request MUST be accompanied by a JSEP SDP offer to negotiate a new PeerConnection. The plugin will match it to the room configuration (e.g., to make sure the codecs you negotiated are allowed in the room), and will reply with a JSEP SDP answer to close the circle and complete the setup of the PeerConnection. As soon as the PeerConnection has been established, the publisher will become active, and a new active feed other participants can subscribe to.

The syntax of a publish request is the following:

{
        "request" : "publish",
        "audio" : <true|false, depending on whether or not audio should be relayed; true by default>,
        "video" : <true|false, depending on whether or not video should be relayed; true by default>,
        "data" : <true|false, depending on whether or not data should be relayed; true by default>,
        "audiocodec" : "<audio codec to prefer among the negotiated ones; optional>",
        "videocodec" : "<video codec to prefer among the negotiated ones; optional>",
        "bitrate" : <bitrate cap to return via REMB; optional, overrides the global room value if present>,
        "record" : <true|false, whether this publisher should be recorded or not; optional>,
        "filename" : "<if recording, the base path/file to use for the recording files; optional>",
        "display" : "<new display name to use in the room; optional>",
        "audio_level_average" : "<if provided, overrides the room audio_level_average for this user; optional>",
        "audio_active_packets" : "<if provided, overrides the room audio_active_packets for this user; optional>"
}

As anticipated, since this is supposed to be accompanied by a JSEP SDP offer describing the publisher's media streams, the plugin will negotiate and prepare a matching JSEP SDP answer. If successful, a configured event will be sent back, formatted like this:

{
        "videoroom" : "event",
        "configured" : "ok"
}

This event will be accompanied by the prepared JSEP SDP answer.

Notice that you can also use configure as a request instead of publish to start publishing. The two are functionally equivalent for publishing, but from a semantic perspective publish is the right message to send when publishing. The configure request, as it will be clearer later, can also be used to update some properties of the publisher session: in this case the publish request can NOT be used, as it can only be invoked to publish, and will fail if you're already publishing something.

As an additional note, notice that you can also join and publish in a single request, which is useful in case you're not interested in first join as a passive attendee and only later publish something, but want to publish something right away. In this case you can use the joinandconfigure request, which as you can imagine combines the properties of both join and publish in a single request: the response to a joinandconfigure will be a joined event, and will again be accompanied by a JSEP SDP answer as usual.

However you decided to publish something, as soon as the PeerConnection setup succeeds and the publisher becomes active, an event is sent to all the participants in the room with information on the new feed. The event must contain an array with a single element, and be formatted like this:

{
        "videoroom" : "event",
        "room" : <room ID>,
        "publishers" : [
                {
                        "id" : <unique ID of the new publisher>,
                        "display" : "<display name of the new publisher, if any>",
                        "audio_codec" : "<audio codec used the new publisher, if any>",
                        "video_codec" : "<video codec used by the new publisher, if any>",
                        "audio_moderated" : <set to true if audio has been moderated for this participant>,
                        "video_moderated" : <set to true if video has been moderated for this participant>,
                        "data_moderated" : <set to true if data has been moderated for this participant>,
                        "simulcast" : "<true if the publisher uses simulcast (VP8 and H.264 only)>",
                        "talking" : <true|false, whether the publisher is talking or not (only if audio levels are used)>,
                }
        ]
}

To stop publishing and tear down the related PeerConnection, you can use the unpublish request, which requires no arguments as the context is implicit:

{
        "request" : "unpublish"
}

This will have the plugin tear down the PeerConnection, and remove the publisher from the list of active streams. If successful, the response will look like this:

{
        "videoroom" : "event",
        "unpublished" : "ok"
}

As soon as the PeerConnection is gone, all the other participants will also be notified about the fact that the stream is no longer available:

{
        "videoroom" : "event",
        "room" : <room ID>,
        "unpublished" : <unique ID of the publisher who unpublished>
}

Notice that the same event will also be sent whenever the publisher feed disappears for reasons other than an explicit unpublish , e.g., because the handle was closed or the user lost their connection. Besides, notice that you can publish and unpublish multiple times within the context of the same publisher handle.

As anticipated above, you can use a request called configure to tweak some of the properties of an active publisher session. This request must be formatted as follows:

{
        "request" : "configure",
        "audio" : <true|false, depending on whether or not audio should be relayed; true by default>,
        "video" : <true|false, depending on whether or not video should be relayed; true by default>,
        "data" : <true|false, depending on whether or not data should be relayed; true by default>,
        "bitrate" : <bitrate cap to return via REMB; optional, overrides the global room value if present (unless bitrate_cap is set)>,
        "keyframe" : <true|false, whether we should send this publisher a keyframe request>,
        "record" : <true|false, whether this publisher should be recorded or not; optional>,
        "filename" : "<if recording, the base path/file to use for the recording files; optional>",
        "display" : "<new display name to use in the room; optional>",
        "audio_active_packets" : "<new audio_active_packets to overwrite in the room one; optional>",
        "audio_level_average" : "<new audio_level_average to overwrite the room one; optional>",
        "min_delay" : <minimum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,
        "max_delay" : <maximum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,
}

As you can see, it's basically the same properties as those listed for publish . This is why both requests can be used to start publishing, as even in that case you configure some of the settings. If successful, a configured event will be sent back as before, formatted like this:

{
        "videoroom" : "event",
        "configured" : "ok"
}

When configuring the room to request the ssrc-audio-level RTP extension, ad-hoc events might be sent to all publishers if audiolevel_event is set to true. These events will have the following format:

{
        "videoroom" : <"talking"|"stopped-talking", whether the publisher started or stopped talking>,
        "room" : <unique numeric ID of the room the publisher is in>,
        "id" : <unique numeric ID of the publisher>,
        "audio-level-dBov-avg" : <average value of audio level, 127=muted, 0='too loud'>
}

An interesting feature VideoRoom publisher can take advantage of is RTP forwarding. In fact, while the main purpose of this plugin is getting media from WebRTC sources (publishers) and relaying it to WebRTC destinations (subscribers), there are actually several use cases and scenarios for making this media available to external, notnecessarily WebRTC-compliant, components. These components may benefit from having access to the RTP media sent by a publisher, e.g., for media processing, external recording, transcoding to other technologies via other applications, scalability purposes or whatever else makes sense in this context. This is made possible by a request called rtp_forward which, as the name suggests, simply forwards in real-time the media sent by a publisher via RTP (plain or encrypted) to a remote backend.

You can add a new RTP forwarder for an existing publisher using the rtp_forward request, which has to be formatted as follows:

{
        "request" : "rtp_forward",
        "room" : <unique numeric ID of the room the publisher is in>,
        "publisher_id" : <unique numeric ID of the publisher to relay externally>,
        "host" : "<host address to forward the RTP and data packets to>",
        "host_family" : "<ipv4|ipv6, if we need to resolve the host address to an IP; by default, whatever we get>",
        "audio_port" : <port to forward the audio RTP packets to>,
        "audio_ssrc" : <audio SSRC to use to use when streaming; optional>,
        "audio_pt" : <audio payload type to use when streaming; optional>,
        "audio_rtcp_port" : <port to contact to receive audio RTCP feedback from the recipient; optional, and currently unused for audio>,
        "video_port" : <port to forward the video RTP packets to>,
        "video_ssrc" : <video SSRC to use to use when streaming; optional>,
        "video_pt" : <video payload type to use when streaming; optional>,
        "video_rtcp_port" : <port to contact to receive video RTCP feedback from the recipient; optional>,
        "simulcast" : <true|false, set to true if the source is simulcast and you want the forwarder to act as a regular viewer (single stream being forwarded) or false otherwise (substreams forwarded separately); optional, default=false>,
        "video_port_2" : <if simulcasting and forwarding each substream, port to forward the video RTP packets from the second substream/layer to>,
        "video_ssrc_2" : <if simulcasting and forwarding each substream, video SSRC to use to use the second substream/layer; optional>,
        "video_pt_2" : <if simulcasting and forwarding each substream, video payload type to use the second substream/layer; optional>,
        "video_port_3" : <if simulcasting and forwarding each substream, port to forward the video RTP packets from the third substream/layer to>,
        "video_ssrc_3" : <if simulcasting and forwarding each substream, video SSRC to use to use the third substream/layer; optional>,
        "video_pt_3" : <if simulcasting and forwarding each substream, video payload type to use the third substream/layer; optional>,
        "data_port" : <port to forward the datachannel messages to>,
        "srtp_suite" : <length of authentication tag (32 or 80); optional>,
        "srtp_crypto" : "<key to use as crypto (base64 encoded key as in SDES); optional>"
}

Notice that, as explained above, in case you configured an admin_key property and extended it to RTP forwarding as well, you'll need to provide it in the request as well or it will be rejected as unauthorized. By default no limitation is posed on rtp_forward .

It's worth spending some more words on how to forward simulcast publishers, as this can lead to some confusion. There are basically two ways to forward a simulcast publisher:

  1. you treat the forwarder as a regular viewer, which means you still only forward a single stream to the recipient, that is the highest quality available at any given time: you can do that by setting simulcast: true in the rtp_forward request;
  2. you forward each substream separately instead, to different target ports: you do that by specifying video_port_2 , video_port_3 and optionally the other related _2 and _3 properties; this is what you should use when you want to forward to a simulcast-aware Streaming mountpoint (see the Streaming plugin documentation for more details).

The two approaches are mutually exclusive: you can NOT use them together in the same RTP forwarder.

A successful request will result in an rtp_forward response, containing the relevant info associated to the new forwarder(s):

{
        "videoroom" : "rtp_forward",
        "room" : <unique numeric ID, same as request>,
        "publisher_id" : <unique numeric ID, same as request>,
        "rtp_stream" : {
                "host" : "<host this forwarder is streaming to, same as request if not resolved>",
                "audio" : <audio RTP port, same as request if configured>,
                "audio_rtcp" : <audio RTCP port, same as request if configured>,
                "audio_stream_id" : <unique numeric ID assigned to the audio RTP forwarder, if any>,
                "video" : <video RTP port, same as request if configured>,
                "video_rtcp" : <video RTCP port, same as request if configured>,
                "video_stream_id" : <unique numeric ID assigned to the main video RTP forwarder, if any>,
                "video_2" : <second video port, same as request if configured>,
                "video_stream_id_2" : <unique numeric ID assigned to the second video RTP forwarder, if any>,
                "video_3" : <third video port, same as request if configured>,
                "video_stream_id_3" : <unique numeric ID assigned to the third video RTP forwarder, if any>,
                "data" : <data port, same as request if configured>,
                "data_stream_id" : <unique numeric ID assigned to datachannel messages forwarder, if any>
        }
}

To stop a previously created RTP forwarder and stop it, you can use the stop_rtp_forward request, which has to be formatted as follows:

{
        "request" : "stop_rtp_forward",
        "room" : <unique numeric ID of the room the publisher is in>,
        "publisher_id" : <unique numeric ID of the publisher to update>,
        "stream_id" : <unique numeric ID of the RTP forwarder>
}

A successful request will result in a stop_rtp_forward response:

{
        "videoroom" : "stop_rtp_forward",
        "room" : <unique numeric ID, same as request>,
        "publisher_id" : <unique numeric ID, same as request>,
        "stream_id" : <unique numeric ID, same as request>
}

To get a list of all the forwarders in a specific room, instead, you can make use of the listforwarders request, which has to be formatted as follows:

{
        "request" : "listforwarders",
        "room" : <unique numeric ID of the room>,
        "secret" : "<room secret; mandatory if configured>"
}

A successful request will produce a list of RTP forwarders in a forwarders response:

{
        "videoroom" : "forwarders",
        "room" : <unique numeric ID of the room>,
        "rtp_forwarders" : [            // Array of publishers with RTP forwarders
                {       // Publisher #1
                        "publisher_id" : <unique numeric ID of publisher #1>,
                        "rtp_forwarders" : [            // Array of RTP forwarders
                                {       // RTP forwarder #1
                                        "audio_stream_id" : <unique numeric ID assigned to this audio RTP forwarder, if any>,
                                        "video_stream_id" : <unique numeric ID assigned to this video RTP forwarder, if any>,
                                        "data_stream_id" : <unique numeric ID assigned to this datachannel messages forwarder, if any>
                                        "ip" : "<IP this forwarder is streaming to>",
                                        "port" : <port this forwarder is streaming to>,
                                        "rtcp_port" : <local port this forwarder is using to get RTCP feedback, if any>,
                                        "ssrc" : <SSRC this forwarder is using, if any>,
                                        "pt" : <payload type this forwarder is using, if any>,
                                        "substream" : <video substream this video forwarder is relaying, if any>,
                                        "srtp" : <true|false, whether the RTP stream is encrypted>
                                },
                                // Other forwarders for this publisher
                        ],
                },
                // Other publishers
        ]
}

*

To enable or disable recording on all participants while the conference is in progress, you can make use of the enable_recording request, which has to be formatted as follows:

{
        "request" : "enable_recording",
        "room" : <unique numeric ID of the room>,
        "secret" : "<room secret; mandatory if configured>"
        "record" : <true|false, whether participants in this room should be automatically recorded or not>,
}

*

To enable or disable recording of a single participant, instead, you can make use of the record_participant request, which has to be formatted as follows:

{
        "request" : "record_participant",
        "room" : <unique numeric ID of the room>,
        "secret" : "<room secret; mandatory if configured>"
        "record" : <true|false, whether this publisher should be recorded or not>,
        "filename" : "<if recording, the base path/file to use for the recording files; optional>"
}

*

Notice that, as we'll see later, participants can normally change their own recording state via configure requests as well: this was done to allow the maximum flexibility, where rather than globally or automatically record something, you may want to individually record some streams and to a specific file. That said, if you'd rather ensure that participants can't stop their recording if a global recording is enabled, or start it when the room is not supposed to be recorded instead, then you should make sure the room is created with the lock_record property set to true : this way, the recording state can only be changed if the room secret is provided, thus ensuring that only an administrator will normally be able to do that (e.g., using the enable_recording just introduced).

To conclude, you can leave a room you previously joined as publisher using the leave request. This will also implicitly unpublish you if you were an active publisher in the room. The leave request looks like follows:

{
        "request" : "leave"
}

If successful, the response will look like this:

{
        "videoroom" : "event",
        "leaving" : "ok"
}

Other participants will receive a "leaving" event to notify them the circumstance:

{
        "videoroom" : "event",
        "room" : <room ID>,
        "leaving : <unique ID of the participant who left>
}

If you were an active publisher, other users will also receive the corresponding "unpublished" event to notify them the stream is not longer available, as explained above. If you were simply lurking and not publishing, the other participants will only receive the "leaving" event.

VideoRoom Subscribers

In a VideoRoom, subscribers are NOT participants, but simply handles that will be used exclusively to receive media from a specific publisher in the room. Since they're not participants per se, they're basically streams that can be (and typically are) associated to publisher handles as the ones we introduced in the previous section, whether active or not. In fact, the typical use case is publishers being notified about new participants becoming active in the room, and as a result new subscriber sessions being created to receive their media streams; as soon as the publisher goes away, the subscriber handle is removed as well. As such, these subscriber sessions are dependent on feedback obtained by publishers, and can't exist on their own, unless you feed them the right info out of band (which is impossible in rooms configured with require_pvtid).

To specify that a handle will be associated with a subscriber, you must use the join request with ptype set to subscriber and specify which feed to subscribe to. The exact syntax of the request is the following:

{
        "request" : "join",
        "ptype" : "subscriber",
        "room" : <unique ID of the room to subscribe in>,
        "feed" : <unique ID of the publisher to subscribe to; mandatory>,
        "private_id" : <unique ID of the publisher that originated this request; optional, unless mandated by the room configuration>,
        "close_pc" : <true|false, depending on whether or not the PeerConnection should be automatically closed when the publisher leaves; true by default>,
        "audio" : <true|false, depending on whether or not audio should be relayed; true by default>,
        "video" : <true|false, depending on whether or not video should be relayed; true by default>,
        "data" : <true|false, depending on whether or not data should be relayed; true by default>,
        "offer_audio" : <true|false; whether or not audio should be negotiated; true by default if the publisher has audio>,
        "offer_video" : <true|false; whether or not video should be negotiated; true by default if the publisher has video>,
        "offer_data" : <true|false; whether or not datachannels should be negotiated; true by default if the publisher has datachannels>,
        "substream" : <substream to receive (0-2), in case simulcasting is enabled; optional>,
        "temporal" : <temporal layers to receive (0-2), in case simulcasting is enabled; optional>,
        "fallback" : <How much time (in us, default 250000) without receiving packets will make us drop to the substream below>,
        "spatial_layer" : <spatial layer to receive (0-2), in case VP9-SVC is enabled; optional>,
        "temporal_layer" : <temporal layers to receive (0-2), in case VP9-SVC is enabled; optional>
}

As you can see, it's just a matter of specifying the ID of the publisher to subscribe to and, if needed, your own private_id (if mandated by the room). The offer_audio , offer_video and offer_data are also particularly interesting, though, as they allow you to only subscribe to a subset of the mountpoint media. By default, in fact, this join request will result in the plugin preparing a new SDP offer trying to negotiate all the media streams made available by the publisher; in case the subscriber knows they don't support one of the mountpoint codecs, though (e.g., the video in the mountpoint is VP8, but they only support H.264), or are not interested in getting all the media (e.g., they're ok with just audio and not video, or don't have enough bandwidth for both), they can use those properties to shape the SDP offer to their needs. In case the publisher to subscribe to is simulcasting or doing VP9 SVC, you can choose in advance which substream you're interested in, e.g., to only get the medium quality at best, instead of higher options if available. As we'll see later, this can be changed dynamically at any time using a subsequent configure request.

As anticipated, if successful this request will generate a new JSEP SDP offer, which will accompany an attached event:

{
        "videoroom" : "attached",
        "room" : <room ID>,
        "feed" : <publisher ID>,
        "display" : "<the display name of the publisher, if any>"
}

At this stage, to complete the setup of the PeerConnection the subscriber is supposed to send a JSEP SDP answer back to the plugin. This is done by means of a start request, which in this case MUST be associated with a JSEP SDP answer but otherwise requires no arguments:

{
        "request" : "start"
}

If successful this request returns a started event:

{
        "videoroom" : "event",
        "started" : "ok"
}

Once this is done, all that's needed is waiting for the WebRTC PeerConnection establishment to succeed. As soon as that happens, the Streaming plugin can start relaying media from the mountpoint the viewer subscribed to to the viewer themselves.

Notice that, in case you want to force an ICE restart for an existing subscription, you'll need to use configure instead, and add a restart attribute set to true ; this will result in a new JSEP SDP offer originated by the plugin, which you'll have to follow with a start request (again including the JSEP answer by the viewer).

As a subscriber, you can temporarily pause and resume the whole media delivery with a pause and, again, start request (in this case without any JSEP SDP answer attached). Neither expect other arguments, as the context is implicitly derived from the handle they're sent on:

{
        "request" : "pause"
}
{
        "request" : "start"
}

Unsurprisingly, they just result in, respectively, paused and started events:

{
        "videoroom" : "event",
        "paused" : "ok"
}
{
        "videoroom" : "event",
        "started" : "ok"
}

For more drill-down manipulations of a subscription, a configure request can be used instead. This request allows subscribers to dynamically change some properties associated to their media subscription, e.g., in terms of what should and should not be sent at a specific time. A configure request must be formatted as follows:

{
        "request" : "configure",
        "audio" : <true|false, depending on whether audio should be relayed or not; optional>,
        "video" : <true|false, depending on whether video should be relayed or not; optional>,
        "data" : <true|false, depending on whether datachannel messages should be relayed or not; optional>,
        "substream" : <substream to receive (0-2), in case simulcasting is enabled; optional>,
        "temporal" : <temporal layers to receive (0-2), in case simulcasting is enabled; optional>,
        "fallback" : <How much time (in us, default 250000) without receiving packets will make us drop to the substream below>,
        "spatial_layer" : <spatial layer to receive (0-2), in case VP9-SVC is enabled; optional>,
        "temporal_layer" : <temporal layers to receive (0-2), in case VP9-SVC is enabled; optional>,
        "audio_level_average" : "<if provided, overrides the room audio_level_average for this user; optional>",
        "audio_active_packets" : "<if provided, overrides the room audio_active_packets for this user; optional>",
        "min_delay" : <minimum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>,
        "max_delay" : <maximum delay to enforce via the playout-delay RTP extension, in blocks of 10ms; optional>
}

As you can see, the audio , video and data properties can be used as a media-level pause/resume functionality, whereas pause and start simply pause and resume all streams at the same time. The substream and temporal properties, instead, only make sense when the mountpoint is configured with video simulcasting support, and as such the viewer is interested in receiving a specific substream or temporal layer, rather than any other of the available ones. The spatial_layer and temporal_layer have exactly the same meaning, but within the context of VP9-SVC publishers, and will have no effect on subscriptions associated to regular publishers.

Another interesting feature that subscribers can take advantage of is the so-called publisher "switching". Basically, when subscribed to a specific publisher and receiving media from them, you can at any time "switch" to a different publisher, and as such start receiving media from that other mountpoint instead. Think of it as changing channel on a TV: you keep on using the same PeerConnection, the plugin simply changes the source of the media transparently. Of course, while powerful and effective this request has some limitations. First of all, it switches both audio and video, meaning you can't just switch video and keep the audio from the previous publisher, for instance; besides, the two publishers must have the same media configuration, that is, use the same codecs, the same payload types, etc. In fact, since the same PeerConnection is used for this feature, switching to a publisher with a different configuration might result in media incompatible with the PeerConnection setup being relayed to the subscriber, and as such in no audio/video being played. That said, a switch request must be formatted like this:

{
        "request" : "switch",
        "feed" : <unique ID of the new publisher to switch to; mandatory>,
        "audio" : <true|false, depending on whether audio should be relayed or not; optional>,
        "video" : <true|false, depending on whether video should be relayed or not; optional>,
        "data" : <true|false, depending on whether datachannel messages should be relayed or not; optional>
}

If successful, you'll be unsubscribed from the previous publisher, and subscribed to the new publisher instead. The event to confirm the switch was successful will look like this:

{
        "videoroom" : "event",
        "switched" : "ok",
        "room" : <room ID>,
        "id" : <unique ID of the new publisher>
}

Finally, to stop the subscription to the mountpoint and tear down the related PeerConnection, you can use the leave request. Since context is implicit, no other argument is required:

{
        "request" : "leave"
}

If successful, the plugin will attempt to tear down the PeerConnection, and will send back a left event:

{
        "videoroom" : "event",
        "left" : "ok",
}