VideoRoom
VideoRoom is an SFU: a conferencing room where each participant publishes their audio, video or data once and the server forwards it to everyone who subscribes. Use it for a video conference, a webinar (one publisher, many watchers) or a screen-share relay. Publishing and subscribing use different PeerConnections, so a participant uses one handle to join and publish and one or more further handles to subscribe.
Subscribing does not work yet
The Rust port does not send the SDP offer to a subscriber, so a browser can publish but cannot receive anyone's media. The attached event comes back with no jsep. Everything else on this page works. Details in Known gaps.
Enable and configure
VideoRoom is not in the default build. Build janus with --features plugin-videoroom (the webapp dev server builds with every plugin). Rooms come from janus.plugin.videoroom.jcfg, or from the create request at run time:
general: {
#admin_key = "supersecret" # if set, create (and rtp_forward, if you ask) need it
}
room-1234: {
description = "Demo Room"
secret = "adminpwd" # needed to edit, destroy, kick
publishers = 6 # how many can publish at once
bitrate = 512000 # cap on each publisher's video, in bits per second
fir_freq = 10 # ask publishers for a keyframe every 10 s
record = false
}The full list of room keys, with defaults, is in the plugin files reference. The create request takes the same keys as the config file, spelled the same way.
The model in one table
| Handle | Created by | Used for |
|---|---|---|
| Publisher handle | join with ptype: "publisher" | Joining the room, publishing, muting, leaving, and receiving room events (who joined, who left, who started publishing). It can join without ever publishing. |
| Subscriber handle | join with ptype: "subscriber" | One recvonly PeerConnection that receives one or more feeds. It is not a participant: it cannot publish or leave the room, only leave the subscription. |
A publisher is identified by its id (you may choose it) and, for the room's own bookkeeping, a private_id that the joined event returns. A feed is a publisher that is publishing. Subscribers ask for feed ids, and optionally single mids.
Try it
Manage rooms
These requests are synchronous: the reply to your POST is the answer. First open a session and attach a handle to janus.plugin.videoroom (walkthrough). The room used here, 1234, comes from the dev configuration.
curl -s -X POST http://127.0.0.1:8088/janus/$SESSION/$HANDLE \
-d '{"janus":"message","transaction":"t1","body":{"request":"list"}}'// VideoRoom room management. Every request below is synchronous: the reply to the POST is the result.
// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary, with the dev configuration (room 1234 comes from the config file).
// → POST /janus
{
"janus": "create",
"transaction": "t-1"
}
// ← 200 OK
{
"janus": "success",
"transaction": "t-1",
"data": {
"id": 9873482636819065
}
}
// → POST /janus/{session_id}
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "t-2"
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-2",
"data": {
"id": 9728245592380508
}
}
// list and exists
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-3",
"body": {
"request": "list"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-3",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"list": [
{
"room": 1234,
"description": "Demo Room",
"pin_required": false,
"is_private": false,
"max_publishers": 6,
"bitrate": 512000,
"fir_freq": 10,
"require_pvtid": false,
"require_e2ee": false,
"dummy_publisher": false,
"notify_joining": false,
"audiocodec": "opus",
"videocodec": "vp8",
"opus_fec": true,
"record": false,
"lock_record": false,
"num_participants": 0,
"audiolevel_ext": true,
"audiolevel_event": false,
"videoorient_ext": true,
"playoutdelay_ext": true,
"transport_wide_cc_ext": true
}
]
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-4",
"body": {
"request": "exists",
"room": 1234
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-4",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"room": 1234,
"exists": true
}
}
}
// create a room (not permanent: it is gone after a restart)
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-5",
"body": {
"request": "create",
"room": 4321,
"description": "Docs room",
"secret": "s3cret",
"pin": "1234",
"publishers": 4,
"permanent": false
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-5",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "created",
"room": 4321,
"permanent": false
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-6",
"body": {
"request": "create",
"room": 4321
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-6",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 427,
"error": "Room 4321 already exists"
}
}
}
// the new room shows up in list, with pin_required true
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-7",
"body": {
"request": "list"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-7",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"list": [
{
"room": 1234,
"description": "Demo Room",
"pin_required": false,
"is_private": false,
"max_publishers": 6,
"bitrate": 512000,
"fir_freq": 10,
"require_pvtid": false,
"require_e2ee": false,
"dummy_publisher": false,
"notify_joining": false,
"audiocodec": "opus",
"videocodec": "vp8",
"opus_fec": true,
"record": false,
"lock_record": false,
"num_participants": 0,
"audiolevel_ext": true,
"audiolevel_event": false,
"videoorient_ext": true,
"playoutdelay_ext": true,
"transport_wide_cc_ext": true
},
{
"room": 4321,
"description": "Docs room",
"pin_required": true,
"is_private": false,
"max_publishers": 4,
"bitrate": 0,
"fir_freq": 0,
"require_pvtid": false,
"require_e2ee": false,
"dummy_publisher": false,
"notify_joining": false,
"audiocodec": "opus",
"videocodec": "vp8",
"opus_fec": true,
"record": false,
"lock_record": false,
"num_participants": 0,
"audiolevel_ext": true,
"audiolevel_event": false,
"videoorient_ext": true,
"playoutdelay_ext": true,
"transport_wide_cc_ext": true
}
]
}
}
}
// edit needs the secret
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-8",
"body": {
"request": "edit",
"room": 4321,
"secret": "wrong",
"new_description": "x"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-8",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 433,
"error": "Unauthorized (wrong secret)"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-9",
"body": {
"request": "edit",
"room": 4321,
"secret": "s3cret",
"new_description": "Renamed room",
"new_publishers": 8
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-9",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "edited",
"room": 4321,
"permanent": false
}
}
}
// allowed: enable the token list, add and remove tokens, disable it again
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-10",
"body": {
"request": "allowed",
"room": 4321,
"secret": "s3cret",
"action": "enable"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-10",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"room": 4321,
"allowed": []
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-11",
"body": {
"request": "allowed",
"room": 4321,
"secret": "s3cret",
"action": "add",
"allowed": [
"tok-a",
"tok-b"
]
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-11",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"room": 4321,
"allowed": [
"tok-a",
"tok-b"
]
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-12",
"body": {
"request": "allowed",
"room": 4321,
"secret": "s3cret",
"action": "remove",
"allowed": [
"tok-a"
]
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-12",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"room": 4321,
"allowed": [
"tok-b"
]
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-13",
"body": {
"request": "allowed",
"room": 4321,
"secret": "s3cret",
"action": "disable"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-13",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"room": 4321
}
}
}
// listparticipants of an empty room, then kick a participant that is not there
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-14",
"body": {
"request": "listparticipants",
"room": 4321
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-14",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "participants",
"room": 4321,
"participants": []
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-15",
"body": {
"request": "kick",
"room": 4321,
"secret": "s3cret",
"id": 99
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-15",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 428,
"error": "No such user 99 in room 4321"
}
}
}
// destroy, then check
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-16",
"body": {
"request": "destroy",
"room": 4321,
"secret": "s3cret"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-16",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "destroyed",
"room": 4321,
"permanent": false
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-17",
"body": {
"request": "exists",
"room": 4321
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-17",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"room": 4321,
"exists": false
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-18",
"body": {
"request": "destroy",
"room": 4321,
"secret": "s3cret"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-18",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 426,
"error": "No such room (4321)"
}
}
}
// a room without an id gets a random one; unknown and malformed requests
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-19",
"body": {
"request": "create",
"description": "Auto id"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-19",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "created",
"room": 4661856518455979,
"permanent": false
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-20",
"body": {
"request": "bogus"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-20",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 423,
"error": "Unknown request 'bogus'"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-21",
"body": {
"request": "exists"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-21",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 429,
"error": "Missing mandatory element (room)"
}
}
}
// → POST /janus/{session_id}
{
"janus": "destroy",
"transaction": "t-22"
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-22"
}Two details from that run that the C documentation does not show: edited and destroyed replies include permanent, and the list of allowed tokens comes back in no fixed order.
Join, list and kick
Joining and the other participant requests are asynchronous: the POST gets an ack and the result is an event. The transcript also shows a subscriber asking for a feed that is not publishing (error 428), and an admin kicking a participant.
// VideoRoom participants: join, listparticipants, kick, and what a subscriber gets when the feed is not publishing.
// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary. alice and admin are two sessions; admin uses the room secret.
// → POST /janus
{
"janus": "create",
"transaction": "admin-1"
}
// ← 200 OK
{
"janus": "success",
"transaction": "admin-1",
"data": {
"id": 9873482636819065
}
}
// → POST /janus/{session_id}
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "admin-2"
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-2",
"data": {
"id": 9728245592380508
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-3",
"body": {
"request": "create",
"room": 4321,
"secret": "s",
"pin": "p",
"publishers": 1,
"notify_joining": true
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-3",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "created",
"room": 4321,
"permanent": false
}
}
}
// → POST /janus
{
"janus": "create",
"transaction": "alice-1"
}
// ← 200 OK
{
"janus": "success",
"transaction": "alice-1",
"data": {
"id": 4661856518455979
}
}
// → POST /janus/{session_id}
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "alice-2"
}
// ← 200 OK
{
"janus": "success",
"session_id": 4661856518455979,
"transaction": "alice-2",
"data": {
"id": 5783672383433022
}
}
// join is asynchronous: the POST gets an ack, the result is an event. This room has a PIN.
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "alice-3",
"body": {
"request": "join",
"ptype": "publisher",
"room": 4321,
"display": "alice",
"id": 1111
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 4661856518455979,
"transaction": "alice-3"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 4661856518455979,
"transaction": "alice-3",
"sender": 5783672383433022,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 429,
"error": "Missing mandatory element (pin)"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "alice-4",
"body": {
"request": "join",
"ptype": "publisher",
"room": 4321,
"display": "alice",
"id": 1111,
"pin": "p"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 4661856518455979,
"transaction": "alice-4"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 4661856518455979,
"transaction": "alice-4",
"sender": 5783672383433022,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "joined",
"room": 4321,
"description": "Room 4321",
"id": 1111,
"private_id": 9150406700,
"publishers": [],
"attendees": []
}
}
}
// alice has joined but not published: publisher is false until her media is up.
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-4",
"body": {
"request": "listparticipants",
"room": 4321
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-4",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "participants",
"room": 4321,
"participants": [
{
"id": 1111,
"display": "alice",
"publisher": false
}
]
}
}
}
// A subscriber that asks for a feed that is not publishing gets error 428.
// → POST /janus
{
"janus": "create",
"transaction": "bob-1"
}
// ← 200 OK
{
"janus": "success",
"transaction": "bob-1",
"data": {
"id": 3194067608492722
}
}
// → POST /janus/{session_id}
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "bob-2"
}
// ← 200 OK
{
"janus": "success",
"session_id": 3194067608492722,
"transaction": "bob-2",
"data": {
"id": 3952167449224036
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "bob-3",
"body": {
"request": "join",
"ptype": "subscriber",
"room": 4321,
"pin": "p",
"streams": [
{
"feed": 1111
}
]
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 3194067608492722,
"transaction": "bob-3"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 3194067608492722,
"transaction": "bob-3",
"sender": 3952167449224036,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 428,
"error": "No such feed (1111)"
}
}
}
// kick: admin gets success, alice gets a leaving event with reason kicked.
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-5",
"body": {
"request": "kick",
"room": 4321,
"secret": "s",
"id": 1111
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-5",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success"
}
}
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 4661856518455979,
"sender": 5783672383433022,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"room": 4321,
"leaving": "ok",
"reason": "kicked"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-6",
"body": {
"request": "moderate",
"room": 4321,
"secret": "s",
"id": 1111,
"mid": "0",
"mute": true
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-6",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 428,
"error": "No such user 1111 in room 4321"
}
}
}
// enable_recording and listforwarders
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-7",
"body": {
"request": "enable_recording",
"room": 4321,
"secret": "s",
"record": true
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-7",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "success",
"record": true
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-8",
"body": {
"request": "listforwarders",
"room": 4321,
"secret": "s"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-8",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "forwarders",
"room": 4321,
"publishers": []
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "admin-9",
"body": {
"request": "destroy",
"room": 4321,
"secret": "s"
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "admin-9",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "destroyed",
"room": 4321,
"permanent": false
}
}
}Publish
Publishing is join as publisher, then publish with an SDP offer. The server answers in a configured event with the answer in jsep. Then your browser trickles its ICE candidates and, once DTLS finishes, the server sends webrtcup and the participant shows publisher: true in listparticipants.
The first transcript is the signalling with a hand-made offer, so it never connects. The second is the full flow from Chrome, recorded in a Janus Bench run (videoroom.publish-subscribe, publisher side).
// Publishing into room 1234: join, then publish with an offer. The SDP is shortened and hand-made, so no media flows.
// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary.
// → POST /janus
{
"janus": "create",
"transaction": "pub-1"
}
// ← 200 OK
{
"janus": "success",
"transaction": "pub-1",
"data": {
"id": 9873482636819065
}
}
// → POST /janus/{session_id}
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "pub-2"
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "pub-2",
"data": {
"id": 9728245592380508
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "pub-3",
"body": {
"request": "join",
"ptype": "publisher",
"room": 1234,
"display": "alice",
"id": 1111
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "pub-3"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "pub-3",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "joined",
"room": 1234,
"description": "Demo Room",
"id": 1111,
"private_id": 4661856518,
"publishers": []
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "pub-4",
"body": {
"request": "publish",
"audio": true,
"video": true,
"descriptions": [
{
"mid": "1",
"description": "Front camera"
}
]
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 5559794783672383433 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "pub-4"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "pub-4",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"room": 1234,
"configured": "ok",
"audio_codec": "opus",
"video_codec": "vp8",
"streams": [
{
"type": "audio",
"mindex": 0,
"mid": "0",
"codec": "opus",
"fec": true
},
{
"type": "video",
"mindex": 1,
"mid": "1",
"description": "Front camera",
"codec": "vp8"
}
]
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 5559794783672383433 2 IN IP4 192.168.1.10\r\ns=VideoRoom 1234\r\n…"
}
}
// The publisher is not 'publisher: true' in listparticipants until ICE and DTLS finish (no browser here).
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "pub-5",
"body": {
"request": "listparticipants",
"room": 1234
}
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "pub-5",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "participants",
"room": 1234,
"participants": [
{
"id": 1111,
"display": "alice",
"publisher": false
}
]
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "pub-6",
"body": {
"request": "unpublish"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "pub-6"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "pub-6",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"error_code": 435,
"error": "Can't unpublish, not published"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "pub-7",
"body": {
"request": "leave"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "pub-7"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "pub-7",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"room": 1234,
"leaving": "ok"
}
}
}// Publishing from Chrome: join, publish with a real offer, trickle ICE, webrtcup. Cut from a Janus Bench run (videoroom.publish-subscribe, publisher side).
// → sent
{
"janus": "create",
"transaction": "bench-mw29jxq7-14"
}
// ← received
{
"janus": "success",
"transaction": "bench-mw29jxq7-14",
"data": {
"id": 9873482636819065
}
}
// → sent
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "bench-mw29jxq9-15",
"session_id": 9873482636819065
}
// ← received
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxq9-15",
"data": {
"id": 9728245592380508
}
}
// → sent
{
"janus": "message",
"handle_id": 9728245592380508,
"body": {
"request": "join",
"ptype": "publisher",
"room": 1234,
"display": "bench-pub",
"id": 610640
},
"transaction": "bench-mw29jxqd-16",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxqd-16"
}
// ← received
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxqd-16",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "joined",
"room": 1234,
"description": "Demo Room",
"id": 610640,
"private_id": 466185651,
"publishers": []
}
}
}
// → sent
{
"janus": "message",
"handle_id": 9728245592380508,
"body": {
"request": "publish",
"audio": true,
"video": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 8455979478367238343 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
},
"transaction": "bench-mw29jxqn-17",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxqn-17"
}
// ← received
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxqn-17",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "event",
"room": 1234,
"configured": "ok",
"audio_codec": "opus",
"video_codec": "vp8",
"streams": [
{
"type": "audio",
"mindex": 0,
"mid": "0",
"codec": "opus",
"fec": true,
"audiolevel_ext_id": 1
},
{
"type": "video",
"mindex": 1,
"mid": "1",
"codec": "vp8",
"videoorient_ext_id": 13,
"playoutdelay_ext_id": 5
}
]
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 8455979478367238343 2 IN IP4 192.168.1.10\r\ns=VideoRoom 1234\r\n…"
}
}
// → sent
{
"janus": "trickle",
"handle_id": 9728245592380508,
"candidate": {
"candidate": "candidate:402291504 1 udp 1670031940 192.168.1.10 49807 typ host generation 0 ufrag nC2u network-cost 999",
"sdpMid": "0",
"sdpMLineIndex": 0
},
"transaction": "bench-mw29jxqu-18",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxqu-18"
}
// (1 more trickle ICE candidate message(s) and their acks omitted)
// → sent
{
"janus": "trickle",
"handle_id": 9728245592380508,
"candidate": {
"completed": true
},
"transaction": "bench-mw29jxu7-1a",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jxu7-1a"
}
// ⇠ event pushed by the server
{
"janus": "webrtcup",
"session_id": 9873482636819065,
"sender": 9728245592380508
}
// → sent
{
"janus": "message",
"handle_id": 9728245592380508,
"body": {
"request": "listparticipants",
"room": 1234
},
"transaction": "bench-mw29jygf-1b",
"session_id": 9873482636819065
}
// ⇠ event pushed by the server
{
"janus": "media",
"session_id": 9873482636819065,
"sender": 9728245592380508,
"mid": "1",
"type": "video",
"receiving": true
}
// ← received
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "bench-mw29jygf-1b",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "participants",
"room": 1234,
"participants": [
{
"id": 610640,
"display": "bench-pub",
"publisher": true,
"talking": false
}
]
}
}
}
// ⇠ event pushed by the server
{
"janus": "media",
"session_id": 9873482636819065,
"sender": 9728245592380508,
"mid": "0",
"type": "audio",
"receiving": true
}Subscribe (the gap)
A subscriber is a second handle that joins with ptype: "subscriber" and streams: [{"feed": <publisher id>}]. In C the reply is an attached event carrying an SDP offer; the client answers with start and its answer. This is what the Rust server sends today, recorded in the same Bench run:
// KNOWN GAP. The subscriber side of the same Bench run: a second session joins as subscriber and asks for the feed of the publisher in videoroom-publish-webrtc, whose media is up.
// The attached event comes back with no jsep, so the browser has no offer to answer and the check times out. C sends the offer here.
// → sent
{
"janus": "create",
"transaction": "bench-mw29jygm-1c"
}
// ← received
{
"janus": "success",
"transaction": "bench-mw29jygm-1c",
"data": {
"id": 987348263681906
}
}
// → sent [subscriber]
{
"janus": "attach",
"plugin": "janus.plugin.videoroom",
"transaction": "bench-mw29jygr-1d",
"session_id": 987348263681906
}
// ← received [subscriber]
{
"janus": "success",
"session_id": 987348263681906,
"transaction": "bench-mw29jygr-1d",
"data": {
"id": 6972824559238050
}
}
// → sent [subscriber]
{
"janus": "message",
"handle_id": 6972824559238050,
"body": {
"request": "join",
"ptype": "subscriber",
"room": 1234,
"streams": [
{
"feed": 610640
}
]
},
"transaction": "bench-mw29jygw-1e",
"session_id": 987348263681906
}
// ← received [subscriber]
{
"janus": "ack",
"session_id": 987348263681906,
"transaction": "bench-mw29jygw-1e"
}
// ← received [subscriber]
{
"janus": "event",
"session_id": 987348263681906,
"transaction": "bench-mw29jygw-1e",
"sender": 6972824559238050,
"plugindata": {
"plugin": "janus.plugin.videoroom",
"data": {
"videoroom": "attached",
"room": 1234,
"streams": [
{
"type": "audio",
"active": true,
"mindex": 0,
"mid": "0",
"ready": false,
"send": true,
"feed_id": 610640,
"feed_display": "bench-pub",
"feed_mid": "0",
"codec": "opus"
},
{
"type": "video",
"active": true,
"mindex": 1,
"mid": "1",
"ready": false,
"send": true,
"feed_id": 610640,
"feed_display": "bench-pub",
"feed_mid": "1",
"codec": "vp8"
}
]
}
}
}The attached event lists the right streams (audio and video of the feed, with codecs) but no jsep follows, so the client has nothing to answer.
Reference
Every request has "request": "<name>" in the body. Errors come back as {"videoroom":"event","error_code":N,"error":"…"}; see the table below. Parameters marked required fail with 429 (missing) or 430 (wrong type) when absent. The sections below are from the C documentation, checked against the binary where noted.
Synchronous requests
| Request | Parameters | Reply videoroom | Verified |
|---|---|---|---|
create | room (integer or string, chosen by the plugin if absent), description, secret, pin, is_private, allowed (array of tokens), permanent, admin_key (required when configured), and every other room key from the config file | created, with room and permanent | verified |
edit | room and secret (required if the room has one), new_description, new_secret, new_pin, new_is_private, new_require_pvtid, new_bitrate, new_fir_freq, new_publishers, new_lock_record, new_rec_dir, permanent | edited, with room and permanent | verified |
destroy | room, secret, permanent | destroyed, with room and permanent. Participants get a destroyed event. | verified |
exists | room (required) | success, with room and exists | verified |
list | admin_key (optional: also lists private rooms) | success, with list: one object per room (room, description, pin_required, is_private, max_publishers, bitrate, fir_freq, require_pvtid, require_e2ee, dummy_publisher, notify_joining, audiocodec, videocodec, opus_fec, record, lock_record, num_participants, and the extension flags) | verified |
allowed | room, secret, action: enable, disable, add or remove; allowed (array of tokens, for add and remove) | success, with room and the current allowed array (not for disable) | verified |
kick | room, secret, id (the participant) | success. The kicked handle gets {"videoroom":"event","room":R,"leaving":"ok","reason":"kicked"}. | verified |
moderate | room, secret, id, mid, mute (boolean) | success | error path only |
listparticipants | room (required) | participants, with participants: id, display, metadata, publisher (true once the media is up), talking | verified |
enable_recording | room, secret, record (boolean) | success, with record | verified |
listforwarders | room, secret | forwarders, with publishers, each with its forwarders | empty list only |
rtp_forward, stop_rtp_forward | room, publisher_id, host, streams (per medium: mid, port, host, ssrc, pt, rtcp_port, simulcast, port_2, port_3, …), secret (when the room has one), srtp_suite, srtp_crypto; stop takes stream_id | rtp_forward with forwarders, and stop_rtp_forward | written from the C source; not exercised here |
add_remote_publisher, update_remote_publisher, remove_remote_publisher, publish_remotely, unpublish_remotely, listremotes | Room cascading between two Janus instances: see the C documentation. | success | written from the C source; not exercised here |
The C documentation names list_remotes; the server answers listremotes
The doxygen text says list_remotes, but the C code, the Rust port and every real client use listremotes. Sending list_remotes gives error 423 "Unknown request" in the Rust server (verified).
Asynchronous requests
Each returns an ack; the result is an event on the same handle with the same transaction.
| Request | On which handle | Parameters | Result |
|---|---|---|---|
join | any | ptype (publisher or subscriber), room, pin (if the room has one), token (if the room has an allowed list). Publisher: id, display, metadata. Subscriber: streams (array of {feed, mid, crossrefid, …}), private_id, use_msid, autoupdate. | joined (publisher) or attached (subscriber) |
joinandconfigure | publisher | join and publish in one request, with an offer in jsep | joined, with the answer in jsep |
publish | publisher | audiocodec, videocodec, bitrate, record, filename, display, metadata, descriptions (array of {mid, description}), audio_level_average, audio_active_packets, update and restart (force a renegotiation or an ICE restart; with configure); deprecated audio, video, data flags (use streams with mid and send in configure instead); offer in jsep (required) | configured, with the answer in jsep; the room gets a publishers event |
configure | publisher | The fields of publish, plus keyframe, streams (per mid: send, keyframe, min_delay, max_delay). With an offer in jsep it renegotiates. | configured |
configure | subscriber | streams (per mid: send, substream, temporal, fallback, spatial_layer, temporal_layer, min_delay, max_delay), restart (ICE restart) | configured: "ok"; by the C documentation restart also brings a new offer (not exercised here) |
unpublish | publisher | none | unpublished: "ok"; the room gets unpublished with the id |
leave | either | none | leaving: "ok" (publisher) or left: "ok" (subscriber); the room gets leaving |
subscribe, unsubscribe, update | subscriber | streams, or subscribe and unsubscribe arrays for update | updated, with the full streams layout and, if the SDP changes, a new offer |
start | subscriber | none; with an answer in jsep it completes the first negotiation or a renegotiation | started: "ok" |
pause | subscriber | none | paused: "ok" |
switch | subscriber | streams: {feed, mid, sub_mid} entries | switched: "ok", with changes and streams |
Events the room sends
| Event body | Sent to | When |
|---|---|---|
{"videoroom":"joined","room":R,"description":D,"id":I,"private_id":P,"publishers":[…],"attendees":[…]} | the joiner | Reply to join as publisher. attendees only if the room has notify_joining. |
{"videoroom":"event","room":R,"publishers":[{id,display,streams,…}]} | all participants | A publisher's media came up. |
{"videoroom":"event","room":R,"joining":{id,display}} | all participants | Someone joined (only with notify_joining). |
{"videoroom":"event","room":R,"unpublished":ID} | all participants | A publisher stopped publishing or left. |
{"videoroom":"event","room":R,"leaving":ID} | all participants | A participant left. |
{"videoroom":"event","room":R,"configured":"ok","audio_codec":…,"video_codec":…,"streams":[…]} | the publisher | Reply to publish or configure. The Rust server (and C) include room, the negotiated codecs and per-stream info, which the C documentation does not show. |
{"videoroom":"destroyed","room":R} | all participants | The room was destroyed. |
{"videoroom":"talking"|"stopped-talking","room":R,"id":I,"audio-level-dBov-avg":N} | publishers | Only with audiolevel_ext and audiolevel_event. |
{"videoroom":"attached","room":R,"streams":[…]} | the subscriber | Reply to join as subscriber. Should carry an offer in jsep: see the gap above. |
Error codes
| Code | Meaning | Code | Meaning |
|---|---|---|---|
| 421 | No message | 431 | Invalid SDP type |
| 422 | Invalid JSON | 432 | Publishers full |
| 423 | Invalid request (unknown request) | 433 | Unauthorized (wrong secret, PIN or token) |
| 424 | Join first | 434 | Already published |
| 425 | Already joined | 435 | Not published |
| 426 | No such room | 436 | ID exists |
| 427 | Room exists | 437 | Invalid SDP |
| 428 | No such feed or user | 438 | Invalid feed |
| 429 | Missing mandatory element | 499 | Unknown error |
| 430 | Invalid element |
Behaviour you may not expect
- A
createwithoutroomgets a random id (a large number: see the lastcreatein the first transcript). A room withoutdescriptionis described asRoom <id>(verified).createof an id that exists is 427. publisherinlistparticipantsis false for a participant who has joined but whose WebRTC connection is not up, even right afterpublish(verified:publish, no browser,publisher: false).unpublishin that state is error 435.- A subscriber whose feed is not publishing yet gets error 428
No such feed (<id>), not anattachedevent. Subscribe after you see thepublishersevent orwebrtcup. - Join into a room with a
pinwithout one: 429Missing mandatory element (pin). A wrong PIN is 433Unauthorized (wrong pin)(verified). Joining twice on one handle is 425Already in as a publisher on this handle. edit,destroy,kick,allowed,moderate,enable_recordingandlistforwarderschecksecretonly when the room has one.- The
allowedarray is returned in hash-table order, as in C. requestvalues are matched case-insensitively:LISTworks (verified).
Known gaps
| Gap | What happens | Evidence |
|---|---|---|
| Subscriber offer is missing | join as subscriber returns attached with no jsep. The subscriber never gets an offer, so it cannot answer, ICE never starts and no media is delivered. This blocks the whole subscribe path in a browser. | Bench videoroom.publish-subscribe and videoroom.datachannel fail at "wait for the subscriber offer". The note is at the top of plugins/videoroom/src/lib.rs. The only offer the Rust server makes is the renegotiation after switch fills an inactive slot. |
| Simulcast and SVC | The layer selection (substream, temporal, spatial_layer) is ported and unit tested, but it only matters to subscribers, which cannot connect. Publishing simulcast has not been tried with a browser. | parity/status.json; Bench has no simulcast check yet. |
| RTP forwarders, remote publishers, recording, dummy publisher | Ported and covered by unit tests; the requests are accepted, but none was run against live media here. | parity/status.json, notes for plugin-videoroom. |
| Data channels in a room | A publisher can open a data channel, but the subscriber side cannot be reached. | Bench videoroom.datachannel. |
Publishing works end to end: join, publish, trickle ICE, DTLS and RTP reaching the server (the publisher's handle_info shows packets in). This is what the Bench videoroom.rooms check and the publisher half of videoroom.publish-subscribe verify.
Test it in Janus Bench
The VideoRoom page has the room list, join, publish and subscribe actions. The checks are videoroom.rooms (passes), videoroom.publish-subscribe (fails at the subscriber offer) and videoroom.datachannel (fails for the same reason). See Janus Bench.