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 subscribe 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).

Notice that, since Janus now supports multistream PeerConnections, subscriptions can be done either in "bulks" (you use a single PeerConnection to subscribe to multiple streams from one or more publishers) or separately (each PeerConnections represents a subscription to a single publisher). Same thing for publishers: you may choose to publish, e.g., audio and video on one PeerConnection, and share your screen on another, or publish everything on the same PeerConnection instead. While functionally both approaches (multistream vs. legacy mode) are the same (the same media flows in both cases), the differences are in how resources are used, and in how the client has to handle incoming and outgoing connections. Besides, one approach might make more sense in some scenarios, and the other make more sense in different use cases. As such, the approach to follow is left to the developer and the application.

What is important to point out, though, is that publishers and subscribers will in all cases require different PeerConnections. This means that, even with multistream, you won't be able to use a single PeerConnection to send your contributions and receive those from everyone else. This is a choice done by design, to avoid the issues that would inevitably arise when doing, for instance, renegotiations to update the streams.

On a more general note and to give some more context with respect to the core functionality in Janus, notice that, considering this plugin allows for several different WebRTC PeerConnections to be on at the same time for the same peer (different publishers and subscribers for sure, and potentially more than one of each if multistream is not in use), each peer will often need to attach several times to the same plugin for each 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 others when they want to subscribe to a feed from other participants (the number depends on the subscription approach of choice). Handles used for subscriptions, though, would be logically "subjects" to the master one used for managing the room: this means that they cannot be used, for instance, to unmute in the room, as their only purpose would be to provide a context in which creating the recvonly PeerConnections for the subscription(s).

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)
            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)
            dummy_publisher = true|false (whether a dummy publisher should be created in this room,
                                    with one separate m-line for each codec supported in the room; this is
                                    useful when there's a need to create subscriptions with placeholders
                                    for some or all m-lines, even when they aren't used yet; default=false)
            dummy_streams = in case dummy_publisher is set to true, array of codecs to offer,
                                    optionally with a fmtp attribute to match (codec/fmtp properties).
                                    If not provided, all codecs enabled in the room are offered, with no fmtp.
                                    Notice that the fmtp is parsed, and only a few codecs are supported.
            threads = number of threads to assist with the relaying of publishers in the room; as
                                    in the Streaming plugin, this setting can help if you expect a lot of subscribers
                                    that may cause the plugin to slow down and fail to catch up (default=0)
    }

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>,
            "mid" : <mid of the m-line to refer to for this moderate request>,
            "mute" : <true|false, depending on whether the media addressed by the above mid 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>,
                            "dummy_publisher": <true|false, whether a dummy publisher exists for placeholder subscriptions>,
                            "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)>,
                            "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>",
                            "metadata" : <valid json object of metadata, 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)>
                    },
                    // 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>",
            "metadata" : <valid json object with metadata; 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>",
                            "metadata" : <valid json object of metadata, if any>,
                            "dummy" : <true if this participant is a dummy publisher>,
                            "streams" : [
                                    {
                                            "type" : "<type of published stream #1 (audio|video|data)">,
                                            "mindex" : "<unique mindex of published stream #1>",
                                            "mid" : "<unique mid of of published stream #1>",
                                            "disabled" : <if true, it means this stream is currently inactive/disabled (and so codec, description, etc. will be missing)>,
                                            "codec" : "<codec used for published stream #1>",
                                            "description" : "<text description of published stream #1, if any>",
                                            "moderated" : <true if this stream audio has been moderated for this participant>,
                                            "simulcast" : "<true if published stream #1 uses simulcast>",
                                            "svc" : "<true if published stream #1 uses SVC (VP9 and AV1 only)>",
                                            "talking" : <true|false, whether the publisher stream has audio activity or not (only if audio levels are used)>,
                                    },
                                    // Other streams, if any
                            ],
                            "talking" : <true|false, whether the publisher is talking or not (only if audio levels are used); deprecated, use the stream specific ones>,
                    },
                    // 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>",
                            "metadata" : <valid json object of metadata, 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>",
                    "metadata" : <valid json object of metadata, 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",
            "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" : "<display name to use in the room; optional>",
            "metadata" : <valid json object of metadata; 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>",
            "descriptions" : [      // Optional
                    {
                            "mid" : "<unique mid of a stream being published>",
                            "description" : "<text description of the stream (e.g., My front webcam)>"
                    },
                    // Other descriptions, if any
            ]}

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. Notice that, in principle, all published streams will be only identified by their unique `mid` and by their type (e.g., audio or video). In case you want to provide more information about the streams being published (e.g., to let other participants know that the first video is a camera, while the second video is a screen share), you can use the `descriptions` array for the purpose: each object in the array can be used to add a text description to associate to a specific mid, in order to help with the UI rendering. The `descriptions` property is optional, so no text will be provided by default: notice these descriptions can be updated dynamically via `configure` requests.

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>",
                            "metadata" : <valid json object of metadata, if any>,
                            "dummy" : <true if this participant is a dummy publisher>,
                            "streams" : [
                                    {
                                            "type" : "<type of published stream #1 (audio|video|data)">,
                                            "mindex" : "<unique mindex of published stream #1>",
                                            "mid" : "<unique mid of of published stream #1>",
                                            "disabled" : <if true, it means this stream is currently inactive/disabled (and so codec, description, etc. will be missing)>,
                                            "codec" : "<codec used for published stream #1>",
                                            "description" : "<text description of published stream #1, if any>",
                                            "moderated" : <true if this stream audio has been moderated for this participant>,
                                            "simulcast" : "<true if published stream #1 uses simulcast>",
                                            "svc" : "<true if published stream #1 uses SVC (VP9 and AV1 only)>",
                                            "talking" : <true|false, whether the publisher stream has audio activity or not (only if audio levels are used)>,
                                    },
                                    // Other streams, if any
                            ],
                            "talking" : <true|false, whether the publisher is talking or not (only if audio levels are used); deprecated, use the stream specific ones>,
                    }
            ]
    }

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",
            "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>",
            "metadata" : <new metadata json object; 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>",
            "streams" : [
                    {
                            "mid" : <mid of the m-line to tweak>,
                            "keyframe" : <true|false, whether we should send this stream a keyframe request; optional>,
                            "send" : <true|false, depending on whether the media addressed by the above mid should be relayed or not; 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>
                    },
                    // Other streams, if any
            ],
            "descriptions" : [
                    // Updated descriptions for the published streams; see "publish" for syntax; optional
            ]
    }

As you can see, it's basically the same properties as those listed for `publish` , with the addition of a `streams` array that can be used to tweak individual streams (which is not available when publishing since in that case the stream doesn't exist yet). Notice that the `configure` request can also be used in renegotiations, to provide an updated SDP with changes to the published media. 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. Notice that, although we're using the term "RTP forwarder", this feature can be used to forward data channel messages as well.

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>",
            "streams" : [
                    {
                            "mid" : "<mid of publisher stream to forward>",
                            "host" : "<host address to forward the packets to; optional, will use global one if missing>",
                            "host_family" : "<optional, will use global one if missing>",
                            "port" : <port to forward the packets to>,
                            "ssrc" : <SSRC to use to use when forwarding; optional, and only for RTP streams, not data>,
                            "pt" : <payload type to use when forwarding; optional, and only for RTP streams, not data>,
                            "rtcp_port" : <port to contact to receive RTCP feedback from the recipient; optional, and only for RTP streams, not data>,
                            "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>,
                            "port_2" : <if video and simulcasting, port to forward the packets from the second substream/layer to>,
                            "ssrc_2" : <if video and simulcasting, SSRC to use to use the second substream/layer; optional>,
                            "pt_2" : <if video and simulcasting, payload type to use the second substream/layer; optional>,
                            "port_3" : <if video and simulcasting, port to forward the packets from the third substream/layer to>,
                            "ssrc_3" : <if video and simulcasting, SSRC to use to use the third substream/layer; optional>,
                            "pt_3" : <if video and simulcasting, payload type to use the third substream/layer; optional>,
                    },
                    {
                            .. other streams, if needed..
                    }
            ],
            "srtp_suite" : <length of authentication tag (32 or 80); optional>,
            "srtp_crypto" : "<key to use as crypto (base64 encoded key as in SDES); optional>"
    }

As you can see, you basically configure each stream to forward in a dedicated object of the `streams` array: for RTP streams (audio, video) this includes optionally overriding payload type or SSRC; simulcast streams can be forwarded separately for each layer. The only parameters you MUST specify are the host and port to send the packets to: the host part can be put in the global part of the request, if all streams will be sent to the same IP address, while the port must be specific to the stream itself.

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>,
            "forwarders" : [
                    {
                            "stream_id" : <unique numeric ID assigned to this forwarder, if any>,
                            "type" : "<audio|video|data>",
                            "host" : "<host this forwarder is streaming to, same as request if not resolved>",
                            "port" : <port this forwarder is streaming to, same as request if configured>,
                            "local_rtcp_port" : <local port this forwarder is using to get RTCP feedback, if any>,
                            "remote_rtcp_port" : <remote port this forwarder is getting RTCP feedback from, if any>,
                            "ssrc" : <SSRC this forwarder is using, same as request if configured>,
                            "pt" : <payload type this forwarder is using, same as request if configured>,
                            "substream" : <video substream this video forwarder is relaying, if any>,
                            "srtp" : <true|false, whether the RTP stream is encrypted (not used for data)>
                    },
                    // Other forwarders, if configured
            ]
    }

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>,
            "publishers" : [                // Array of publishers with RTP forwarders
                    {       // Publisher #1
                            "publisher_id" : <unique numeric ID of publisher #1>,
                            "forwarders" : [                // Array of RTP forwarders
                                    {       // RTP forwarder #1
                                            "stream_id" : <unique numeric ID assigned to this RTP forwarder, if any>,
                                            "type" : "<audio|video|data>",
                                            "host" : "<host this forwarder is streaming to>",
                                            "port" : <port this forwarder is streaming to>,
                                            "local_rtcp_port" : <local port this forwarder is using to get RTCP feedback, if any>,
                                            "remote_rtcp_port" : <remote port this forwarder getting RTCP feedback from, 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>,
    }

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>,
            "display" : "<display name of the leaving participant, if any>",
            "metadata" : <valid json object of metadata, if any>
    }

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 one or more publishers 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, other participants are notified so that the related subscriber handles can be removed/updated accordingly 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>,
            "use_msid" : <whether subscriptions should include an msid that references the publisher; false by default>,
            "autoupdate" : <whether a new SDP offer is sent automatically when a subscribed publisher leaves; true by default>,
            "private_id" : <unique ID of the publisher that originated this request; optional, unless mandated by the room configuration>,
            "streams" : [
                    {
                            "feed" : <unique ID of publisher owning the stream to subscribe to>,
                            "mid" : "<unique mid of the publisher stream to subscribe to; optional>"
                            "crossrefid" : "<id to map this subscription with entries in streams list; optional>"
                            // Optionally, simulcast or SVC targets (defaults if missing)
                    },
                    // Other streams to subscribe to
            ]
    }

As you can see, it's just a matter of specifying the list of streams to subscribe to: in particular, you have to provide an array of objects, where each objects represents a specific stream (or group of streams) you're interested in. For each object, the `feed_id` indicating the publisher owning the stream(s) is mandatory, while the related `mid` is optional: this gives you some flexibility when subscribing, as only providing a `feed_id` will indicate you're interested in ALL the stream from that publisher, while providing a `mid` as well will indicate you're interested in a stream in particular. Since you can provide an array of streams, just specifying the `feed_id` or explicitly listing all the `feed_id` + `mid` combinations is equivalent: of course, different objects in the array can indicate different publishers, allowing you to combine streams from different sources in the same subscription. Notice that if a publisher stream is marked as `disabled` and you try to subscribe to it, it will be skipped silently.

Depending on whether the subscription will refer to a single publisher (legacy approach) or to streams coming from different publishers (multistream), the list of streams may differ. The ability to single out the streams to subscribe to is particularly useful in case you don't want to, or can't, subscribe to all available media: e.g., you know a publisher is sending both audio and video, but video is in a codec you don't support or you don't have bandwidth for both; or maybe there are 10 participants in the room, but you only want video from the 3 most active speakers; and so on. The content of the `streams` array will shape what the SDP offer the plugin will send will look like, so that eventually a subscription for the specified streams will take place. Notice that, while for backwards compatibility you can still use the old `feed`, `audio`, `video`, `data`, `offer_audio`, `offer_video` and `offer_data` named properties, they're now deprecated and so you're highly encouraged to use this new drill-down `streams` list instead.

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

    {
            "videoroom" : "attached",
            "room" : <room ID>,
            "streams" : [
                    {
                            "mindex" : <unique m-index of this stream>,
                            "mid" : "<unique mid of this stream>",
                            "type" : "<type of this stream's media (audio|video|data)>",
                            "active" : <true|false, whether this stream is currently active>,
                            "feed_id" : <unique ID of the publisher originating this stream>,
                            "feed_mid" : "<unique mid of this publisher's stream>",
                            "feed_display" : "<display name of this publisher, if any>",
                            "feed_metadata" : <valid json object of metadata of this publisher, if any>,
                            "send" : <true|false; whether we configured the stream to relay media>,
                            "codec" : "<codec used by this stream>",
                            "h264-profile" : "<in case H.264 is used by the stream, the negotiated profile>",
                            "vp9-profile" : "<in case VP9 is used by the stream, the negotiated profile>",
                            "ready" : <true|false; whether this stream is ready to start sending media (will be false at the beginning)>,
                            "simulcast" : { .. optional object containing simulcast info, if simulcast is used by this stream .. },
                            "svc" : { .. optional object containing SVC info, if SVC is used by this stream .. },
                            "playout-delay" : { .. optional object containing info on the playout-delay extension configuration, if in use .. },
                            "sources" : <if this is a data channel stream, the number of data channel subscriptions>,
                            "source_ids" : [ .. if this is a data channel stream, an array containing the IDs of participants we've subscribed to .. ],
                    },
                    // Other streams in the subscription, if any
            ]
    }

As you can see, a summary of the streams we subscribed to will be sent back, which will be useful on the client side for both mapping and rendering purposes.

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 VideoRoom plugin can start relaying media the recipient subscribed to.

Once a WebRTC PeerConnection has been established for a subscriber, in case you want to update a subscription you have to use the `subscribe` , `unsubscribe` or `update` methods: as the names of the requests suggest, the former allows you to add more streams to subscribe to, the second instructs the plugin to remove streams you're currently subscribe to, while the latter allows you to perform both operations at the same time. Any of those requests will trigger a renegotiation, if they were successful, meaning the plugin will send you a new JSEP offer you'll have to reply to with an answer: to send the answer, just use the same `start` request we already described above. Notice that renegotiations may not be triggered right away, e.g., whenever you're trying to update a session and the plugin is still in the process of renegoting a previous update for the same subscription: in that case, an update will be scheduled and a renegotiation will be triggered as soon as it's viable, and an empty `updating` event will be triggered instead to notify the caller that the management of that request has been postponed. It's also important to point out that the number of offers generated in response to those requests may not match the amount of requests: in fact, since requests are postponed, a single offer may be sent in response to multiple requests to update a subscription at the same time, thus addressing them all in a cumulative way. This means clients should never expect an offer any time they request one.

The syntax of the `subscribe` mirrors the one for new subscriptions, meaning you use the same `streams` array to address the new streams you want to receive, and formatted the same way:

    {
            "request" : "subscribe",
            "streams" : [
                    {
                            "feed" : <unique ID of publisher owning the new stream to subscribe to>,
                            "mid" : "<unique mid of the publisher stream to subscribe to; optional>"
                            "crossrefid" : "<id to map this subscription with entries in streams list; optional>"
                            // Optionally, send, simulcast or SVC targets (defaults if missing)
                    },
                    // Other new streams to subscribe to
            ]
    }

This means the exact same considerations we made on `streams` before apply here as well: whatever they represent, will indicate the willingness to subscribe to the related stream. Notice that if you were already subscribed to one of the new streams indicated here, you'll subscribe to it again in a different m-line, so it's up to you to ensure you avoid duplicates (unless that's what you wanted, e.g., for testing purposes). In case the update was successful, you'll get an `updated` event, containing the updated layout of all subscriptions (pre-existing and new ones), and a new JSEP offer to renegotiate the session:

    {
            "videoroom" : "updated",
            "room" : <room ID>,
            "streams": [
                    {
                            "mindex" : <unique m-index of this stream>,
                            "mid" : "<unique mid of this stream>",
                            "type" : "<type of this stream's media (audio|video|data)>",
                            "feed_id" : <unique ID of the publisher originating this stream>,
                            "feed_mid" : "<unique mid of this publisher's stream>",
                            "feed_display" : "<display name of this publisher, if any>",
                            "feed_metadata" : <valid json object of metadata of this publisher, if any>,
                            "send" : <true|false; whether we configured the stream to relay media>,
                            "ready" : <true|false; whether this stream is ready to start sending media (will be false at the beginning)>
                    },
                    // Other streams in the subscription, if any; old and new
            ]
    }

Notice that if your `subscribe` request didn't change anything as far as the SDP negotiation is concerned (e.g., subscribing to new data streams where a datachannel existed already), you'll simply get an `updated` event back with no `streams` object.

As explained before, in case the message contains a JSEP offer (which may not be the case if no change occurred), then clients will need to send a new JSEP answer with a `start` request to close this renegotiation.

The `unsubscribe` request works pretty much the same way, with the difference that the `streams` array you provide to specify what to unsubscribe from may look different. Specifically, the syntax looks like this:

    {
            "request" : "unsubscribe",
            "streams" : [
                    {
                            "feed" : <unique ID of publisher owning the new stream to unsubscribe from; optional>,
                            "mid" : "<unique mid of the publisher stream to unsubscribe from; optional>"
                            "sub_mid" : "<unique mid of the subscriber stream to unsubscribe; optional>"
                    },
                    // Other streams to unsubscribe from
            ]
    }

This means that you have different ways to specify what to unsubscribe from: if an object only specifies `feed_id`, then all the subscription streams that were receiving media from that publisher will be removed; if an object specifies `feed_id` and `mid`, then all the subscription streams that were receiving media from the publisher stream with the related mid will be removed; finally, if an object only specifies `sub_mid` instead, then only the stream in the subscription that is addressed by the related mid (subscription mid, no relation to the publishers') will be removed. As such, you have a great deal of flexibility in how to unsubscribe from media. Notice that multiple streams may be removed in case you refer to the "source" ( `feed_id` ), rather than the "sink" ( `sub_mid` ), especially in case the subscription contained duplicates or multiple streams from the same publisher.

A successful `unsubscribe` will result in exactly the same `updated` event `subscribe` triggers, so the same considerations apply with respect to the potential need of a renegotiation and how to complete it with a `start` along a JSEP answer. Again, if `unsubscribe` didn't result in SDP changes (e.g., unsubscribing from a data channel stream), you'll simply get an `updated` event back with no `streams` object.

As anticipated, the `update` request allows you to combine changes to a subscription where you may want to both subscribe to new streams, and unsubscribe from existing ones, which the existing `subscribe` and `unsubscribe` requests wouldn't allow you to do as they work exclusively on the action specified by their name. The syntax for the `update` request is very similar to the previous method, meaning arrays are still used to address the streams to work on, with the key difference that they won't be named `streams`, but `subscribe` and `unsubscribe` instead:

    {
            "request" : "update",
            "subscribe" : [
                    {
                            "feed" : <unique ID of publisher owning the new stream to subscribe to>,
                            "mid" : "<unique mid of the publisher stream to subscribe to; optional>"
                            "crossrefid" : "<id to map this subscription with entries in streams list; optional>"
                            // Optionally, send, simulcast or SVC targets (defaults if missing)
                    },
                    // Other new streams to subscribe to
            ],
            "unsubscribe" : [
                    {
                            "feed" : <unique ID of publisher owning the new stream to unsubscribe from; optional>,
                            "mid" : "<unique mid of the publisher stream to unsubscribe from; optional>"
                            "sub_mid" : "<unique mid of the subscriber stream to unsubscribe; optional>"
                    },
                    // Other streams to unsubscribe from
            ]
    }

Both the `subscribe` and `unsubscribe` arrays are optional, which means that an `update` request to only subscribe to new streams will be functionally equivalent to a `subscribe` request, and an `update` request to only unsubscribe will be functionally equivalent to an `unsubscribe` request instead. That said, one of the two must be provided, which means that an `update` request that doesn't include either of them will result in an error.

A successful `update` will result in exactly the same `updated` event `subscribe` and `unsubscribe` trigger, so the same considerations apply with respect to the potential need of a renegotiation and how to complete it with a `start` along a JSEP answer. Again, if `update` didn't result in SDP changes, you'll simply get an `updated` event back with no `streams` object.

Notice that, in case you want to trigger an ICE restart rather than updating a subscription, you'll have to use a different request, named `configure:` this will be explained in a few paragraphs.

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",
            "streams" : [
                    {
                            "mid" : <mid of the m-line to refer to>,
                            "send" : <true|false, depending on whether the mindex media 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; optional>,
                            "spatial_layer" : <spatial layer to receive (0-2), in case SVC is enabled; optional>,
                            "temporal_layer" : <temporal layers to receive (0-2), in case 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>,
                    },
                    // Other streams, if any
            ],
            "restart" : <trigger an ICE restart; optional>
    }

As you can see, the `mid` and `send` properties can be used as a media-level pause/resume functionality ("only mute/unmute this mid"), 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 publisher is configured with video simulcasting support, and as such the subscriber is interested in receiving a specific substream or temporal layer, rather than any other of the available ones: notice that for them to work you'll have to specify the `mid` as well, as the same subscription may be receiving simulcast stream from multiple publishers. The `spatial_layer` and `temporal_layer` have exactly the same meaning, but within the context of SVC publishers, and will have no effect on subscriptions associated to regular publishers.

As anticipated, `configure` is also the request you use when you want to trigger an ICE restart for a subscriber: in fact, while publishers can force a restart themselves by providing the right JSEP offer, subscribers always receive an offer from Janus instead, and as such have to explicitly ask for a dedicated offer when an ICE restart is needed; in that case, just set `restart` to `true` in a `configure` request, and a new JSEP offer with ICE restart information will be sent to the client, to which the client will have to reply, as usual, via `start` along a JSEP answer. This documentation doesn't explain when or why an ICE restart is needed or appropriate: please refer to the ICE RFC or other sources of information for that.

Another interesting feature that subscribers can take advantage of is the so-called publisher "switching". Basically, when subscribed to one or more publishers and receiving media from them, you can at any time "switch" any of the subscription streams to a different publisher, and as such start receiving media on the related m-line from that publisher instead, all without doing a new `subscribe` or `unsubscribe`, and so without the need of doing any renegotiation at all; just some logic changes. 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: in fact, the source (audio or video) that you switch to must have the same media configuration (e.g., same codec) as the source you're replacing. In fact, since the same PeerConnection is used for this feature and no renegotiation is taking place, switching to a stream with a different configuration would result in media incompatible with the PeerConnection setup being relayed to the subscriber (e.g., negotiated VP9, but new source is H.264), and as such in no audio/video being played; in that case, you'll need a `subscribe` instead, and a new m-line.

That said, a `switch` request must be formatted like this:

    {
            "request" : "switch",
            "streams" : [
                    {
                            "feed" : <unique ID of the publisher the new source is from>,
                            "mid" : "<unique mid of the source we want to switch to>",
                            "sub_mid" : "<unique mid of the stream we want to pipe the new source to>"
                            .. other properties, e.g., substream, temporal, etc.
                    },
                    {
                            // Other updates, if any
                    }
            ]
    }

While apparently convoluted, this is actually a quite effective and powerful way of updating subscriptions without renegotiating. In fact, it allows for full or partial switches: for instance, sometimes you may want to replace all audio and video streams (e.g., switching from Bob to Alice in a "legacy" VideoRoom usage, where each PeerConnection subscription is a different publisher), or just replace a subset of them (e.g., you have a subscription with three video slots, and you change one of them depending on the loudest speaker). What to replace is dictated by the `streams` array, where each object in the array contains all the info needed for the switch to take place: in particular, you must specify which of your subscription m-lines you're going to update, via `sub_mid` , and which publisher stream should now start to feed it via `feed` and `mid`.

If successful, the specified subscriptions will be updated, meaning they'll be unsubscribed from the previous publisher stream, and subscribed to the new publisher stream instead, all without a renegotiation (so no new SDP offer/answer exchange to take care of). The event to confirm the switch was successful will look like this:

    {
            "videoroom" : "event",
            "switched" : "ok",
            "room" : <room ID>,
            "changes" : <number of successful changes (may be smaller than the size of the streams array provided in the request)>,
            "streams" : [
                    // Current configuration of the subscription, same format as when subscribing
                    // Will contain info on all streams, not only those that have been updated
            ]
    }

Notice that, while a `switch` request usually doesn't require a renegotiation, it MIGHT trigger one nevertheless: in fact, if a "switch" request assigns a new publisher stream to a previously inactive subscriber stream, then a renegotiation to re-activate that stream will be needed as well, as otherwise the packets from the new source will not be relayed.

Finally, to close a subscription 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",
    }

## Remote publishers (room cascading)

Normally, the VideoRoom plugin can only route streams associated to users connected to the Janus instance the plugin lives in: this means that, within the context of a room, you can only subscribe to publishers connected to the same server (and room) you're on.

That said, there are obviously ways to address this constraint. In the past, a typical approach for handling this (e.g., for scalability or geo-distribution purposes) was to use the `rtp_forward` request to feed one or more local/remote Streaming plugin mountpoints, so that a VideoRoom publisher could be consumed using the Streaming plugin instead, possibly on a completely different Janus instance. This works and has been used extensively (by ourselves too), but has the downside that this completely excludes the VideoRoom API in terms of presence and subscriptions: it's up to you, for instance, to advertise these redistributed streams somehow, and associate them to the original publisher from a semantics perspective.

That said, the VideoRoom plugin now also has a concept of remote publishers, that allows you to remotize local VideoRoom publishers to different VideoRoom instances, which can in turn advertise the presence of these remote subscribers along with their local publishers. This allows subscribers to use the VideoRoom API, transparently, to subscribe to both local and remote publishers seamlessly, knowing that the involved VideoRoom instances will exchange the media packets among them to make it happen.

It's important to point out that this is not something that's completely automated: it's still up to you, via API calls, to instruct all involved VideoRoom instances, so that the remotization can happen, and to keep it up do that (e.g., after renegotiations occur).

Specifically, the VideoRoom API exposes the `add_remote_publisher` , `update_remote_publisher` , `remove_remote_publisher` , `publish_remotely` , `unpublish_remotely` and `list_remotes` requests.

Assuming that Janus A wants to make one of its local publishers available in a room on Janus B as well, this is the process you must follow:

- you use `add_remote_publisher` on Janus B (the target instance) to add a new remote publisher; this will return some connectivity info to the caller, and immediately advertise the new publisher to other attendees in Janus B even before media actually arrives;
- you use `publish_remotely` on Janus A (the source instance), using the info returned from the previous call; this has the result of instructing Janus A to start relaying all RTP packets associated to that publisher to Janus B ;
- any time the publisher on Janus A renegotiates their session (e.g., a new audio or video stream is added, or removed), you should use `update_remote_publisher` on Janus B so that the remote instance is aware of the changes, and can notify people in the room accordingly (e.g., so that they can update their subscriptions accordingly);
- when the publisher on Janus A leaves, an `unpublish_remotely` request must be sent on Janus A to ensure no media is forwarded anymore, and at the same time a `remove_remote_publisher` must be sent to Janus B so that other attendees can be notified the participant has left.

Using these requests, the two Janus instances will transparently and automatically communicate using internally created RTP forwarders. The same ports are used for all RTP packets, so multiplexing is performed using a simple math on SSRC identifiers: this means that there's no need to open new ports as a consequence of renegotiations of a publisher, but only to notify the recipient about what media is on its way, and demultiplexing will be performed automatically.

Everything else (subscribing to, and unsubscribing from, remote publishers) works exactly the same way as shown in the previous sections. As far as local attendees are concerned, a remote publisher is advertised and looks exactly like any other local publisher. The details about how the remotization works behind the scenes is hidden from them, and not relevant to the subscription process.

Coming to how the requests need to be formatted, the `add_remote_publisher` must be formatted like the following:

    {
            "request" : "add_remote_publisher",
            "room" : <unique ID of the room to add the remote publisher to>,
            "id" : <unique ID to register for the remote publisher; optional, will be chosen by the plugin if missing; doesn't need to be the same as the source one>,
            "secret" : "<password required to edit the room, mandatory if configured in the room>",
            "display" : "<display name for the remote publisher; optional>",
            "metadata" : <valid json object of metadata; optional>,
            "mcast" : "<multicast group port for receiving RTP packets, if any>",
            "iface" : "<network interface or IP address to bind to, if any (binds to all otherwise)>",
            "port" : <local port for receiving all RTP packets; 0 will bind to a random one (default)>,
            "srtp_suite" : <length of authentication tag (32 or 80); optional>,
            "srtp_crypto" : "<key to use as crypto (base64 encoded key as in SDES); optional>",
            "streams" : [
                    {
                            "type" : "<type of published stream #1 (audio|video|data)">,
                            "mindex" : "<unique mindex of published stream #1>",
                            "mid" : "<unique mid of of published stream #1>",
                            "disabled" : <if true, it means this stream is currently inactive/disabled (and so codec, description, etc. will be missing)>,
                            "codec" : "<codec used for published stream #1>",
                            "description" : "<text description of published stream #1, if any>",
                            "disabled" : <true if published stream #1 is currently disabled>,
                            "stereo" : <true if published stream #1 is audio and stereo>,
                            "fec" : <true if published stream #1 is audio and uses FEC>,
                            "dtx" : <true if published stream #1 is audio and uses DTX>,
                            "h264-profile" : "<in case H.264 is used by the stream, the negotiated profile>",
                            "vp9-profile" : "<in case VP9 is used by the stream, the negotiated profile>",
                            "simulcast" : <true if published stream #1 is video and uses simulcast>,
                            "svc" : <true if published stream #1 is video and uses SVC (VP9 and AV1 only)>,
                            "audiolevel_ext_id" : <in case the audio level extension is used by this stream, its ID>,
                            "videoorient_ext_id" : <in case the video orientation extension is used by this stream, its ID>,
                            "playoutdelay_ext_id" : <in case the playout delay extension is used by this stream, its ID>
                    },
                    // Other streams, if any
            ]
    }

A successful request will result in a `success` response:

    {
            "videoroom" : "success",
            "room" : <same as request>,
            "id" : <unique ID associated to the new remote publisher>,
            "ip" : "<host address to use to send RTP associated to this remote publisher>",
            "port" : <port to use to send RTP associated to this remote publisher>,
            "rtcp_port" : <port to latch to in order to receive RTCP feedback from this remote publisher>
    }

To update a previously created remote publisher, the `update_remote_publisher` request is used, which must be formatted like the following:

    {
            "request" : "update_remote_publisher",
            "room" : <unique ID of the room the remote publisher is in>,
            "id" : <unique ID of the remote publisher>,
            "secret" : "<password required to edit the room, mandatory if configured in the room>",
            "display" : "<new display name for the remote publisher; optional>",
            "metadata" : <new valid json object of metadata; optional>,
            "srtp_suite" : <length of authentication tag (32 or 80); optional>,
            "srtp_crypto" : "<key to use as crypto (base64 encoded key as in SDES); optional>",
            "streams" : [
                    {
                            // Same syntax as add_remote_publisher: only needs to
                            // reference new or modified streams, not all of them
                    },
                    // Other streams, if any
            ]
    }

A successful request will result in a `success` response:

    {
            "videoroom" : "success"
    }

To remove a previously created remote publisher, the `remove_remote_publisher` request is used, which must be formatted like the following:

    {
            "request" : "remove_remote_publisher",
            "room" : <unique ID of the room the remote publisher is in>,
            "id" : <unique ID of the remote publisher>,
            "secret" : "<password required to edit the room, mandatory if configured in the room>"
    }

A successful request will result in a `success` response:

    {
            "videoroom" : "success"
    }

Other attendees in the same room as the remote publishers will be notified accordingly, exactly as it happens when a local publisher goes aeay or close their PeerConnection.

For what concerns the source instance (from where the publisher is remotized to a different VideoRoom instance), the `publish_remotely` request is used, which must be formatted as follows:

    {
            "request" : "publish_remotely",
            "room" : <unique ID of the room the local publisher to remotize is in>,
            "publisher_id" : <unique ID of the local publisher to remotize>,
            "remote_id" : "<unique ID to associate to this remotization; this has nothing to do with the ID the publisher will have in the remote instance, and is only used to address this specific remotization on the source instance>",
            "secret" : "<password required to edit the room, mandatory if configured in the room>",
            "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>",
            "port" : <port to forward the packets to>,
            "rtcp_port" : <port to contact to receive RTCP feedback from the recipient; optional, and only for RTP streams, not data>,
            "srtp_suite" : <length of authentication tag (32 or 80); optional>,
            "srtp_crypto" : "<key to use as crypto (base64 encoded key as in SDES); optional>"
    }

A successful request will result in a `success` response:

    {
            "videoroom" : "success",
            "room" : <same as request>,
            "id" : <unique ID of the local publisher>,
            "remote_id" : "<unique ID of this remotization (needed for unpublish_remotely)>"
    }

Notice that, as explained before, `publish_remotely` expects a remote publisher ready to receive their media, which is why `add_remote_publisher` must be sent on the target Janus instance first: the info returned by that request (IP and ports) are what you then feed to `publish_remotely` .

The `publish_remotely` request can be used multiple times for the same local publisher, e.g., to make the same publisher available on more than one remote Janus/VideoRoom instance. This is why `remote_id` is needed to be able to individually address each specific remotization, in case you want to, e.g., stop making a specific publisher available on a specific Janus instance, but keep it available on others.

To disable a specific remotization of a local publisher, the `unpublish_remotely` request is used, which must be formatted as follows:

    {
            "request" : "unpublish_remotely",
            "room" : <unique ID of the room the local publisher is in>,
            "publisher_id" : <unique ID of the local publisher>,
            "remote_id" : "<unique ID to associate to this remotization of the local publisher>",
            "secret" : "<password required to edit the room, mandatory if configured in the room>"
    }

A successful request will result in a `success` response:

    {
            "videoroom" : "success",
            "room" : <same as request>,
            "id" : <unique ID of the local publisher>
    }

Notice that removing a remotization from the source instance only stops the delivery of RTP packets to the target of the remotization: it does NOT also remove the remote publisher from the remote instance. It's up to you to notify the target instance with `remove_remote_publisher` .

You can list all the remotizations for a local publisher using `list_remotes`, which must be formatted as follows:

    {
            "request" : "list_remotes",
            "room" : <unique ID of the room the local publisher is in>,
            "publisher_id" : <unique ID of the local publisher>,
            "secret" : "<password required to edit the room, mandatory if configured in the room>"
    }

A successful request will result in a `success` response:

    {
            "videoroom" : "success",
            "room" : <same as request>,
            "id" : <unique ID of the local publisher>,
            "list" : [
                    {
                            "remote_id" : "<unique ID of this remotization of this local publisher">,
                            "host" : "<address all RTP packets are being sent to">,
                            "port" : "port all RTP packets are being sent to>
                            "rtcp_port" : "RTCP port, if enabled>
                    },
                    // Other remotizations, if any
            ]
    }