Skip to content

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:

ini
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 ​

HandleCreated byUsed for
Publisher handlejoin 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 handlejoin 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.

sh
curl -s -X POST http://127.0.0.1:8088/janus/$SESSION/$HANDLE \
  -d '{"janus":"message","transaction":"t1","body":{"request":"list"}}'
jsonc
// 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.

jsonc
// 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).

jsonc
// 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"
    }
  }
}
jsonc
// 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:

jsonc
// 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 ​

RequestParametersReply videoroomVerified
createroom (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 filecreated, with room and permanentverified
editroom 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, permanentedited, with room and permanentverified
destroyroom, secret, permanentdestroyed, with room and permanent. Participants get a destroyed event.verified
existsroom (required)success, with room and existsverified
listadmin_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
allowedroom, 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
kickroom, secret, id (the participant)success. The kicked handle gets {"videoroom":"event","room":R,"leaving":"ok","reason":"kicked"}.verified
moderateroom, secret, id, mid, mute (boolean)successerror path only
listparticipantsroom (required)participants, with participants: id, display, metadata, publisher (true once the media is up), talkingverified
enable_recordingroom, secret, record (boolean)success, with recordverified
listforwardersroom, secretforwarders, with publishers, each with its forwardersempty list only
rtp_forward, stop_rtp_forwardroom, 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_idrtp_forward with forwarders, and stop_rtp_forwardwritten from the C source; not exercised here
add_remote_publisher, update_remote_publisher, remove_remote_publisher, publish_remotely, unpublish_remotely, listremotesRoom cascading between two Janus instances: see the C documentation.successwritten 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.

RequestOn which handleParametersResult
joinanyptype (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)
joinandconfigurepublisherjoin and publish in one request, with an offer in jsepjoined, with the answer in jsep
publishpublisheraudiocodec, 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
configurepublisherThe fields of publish, plus keyframe, streams (per mid: send, keyframe, min_delay, max_delay). With an offer in jsep it renegotiates.configured
configuresubscriberstreams (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)
unpublishpublishernoneunpublished: "ok"; the room gets unpublished with the id
leaveeithernoneleaving: "ok" (publisher) or left: "ok" (subscriber); the room gets leaving
subscribe, unsubscribe, updatesubscriberstreams, or subscribe and unsubscribe arrays for updateupdated, with the full streams layout and, if the SDP changes, a new offer
startsubscribernone; with an answer in jsep it completes the first negotiation or a renegotiationstarted: "ok"
pausesubscribernonepaused: "ok"
switchsubscriberstreams: {feed, mid, sub_mid} entriesswitched: "ok", with changes and streams

Events the room sends ​

Event bodySent toWhen
{"videoroom":"joined","room":R,"description":D,"id":I,"private_id":P,"publishers":[…],"attendees":[…]}the joinerReply to join as publisher. attendees only if the room has notify_joining.
{"videoroom":"event","room":R,"publishers":[{id,display,streams,…}]}all participantsA publisher's media came up.
{"videoroom":"event","room":R,"joining":{id,display}}all participantsSomeone joined (only with notify_joining).
{"videoroom":"event","room":R,"unpublished":ID}all participantsA publisher stopped publishing or left.
{"videoroom":"event","room":R,"leaving":ID}all participantsA participant left.
{"videoroom":"event","room":R,"configured":"ok","audio_codec":…,"video_codec":…,"streams":[…]}the publisherReply 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 participantsThe room was destroyed.
{"videoroom":"talking"|"stopped-talking","room":R,"id":I,"audio-level-dBov-avg":N}publishersOnly with audiolevel_ext and audiolevel_event.
{"videoroom":"attached","room":R,"streams":[…]}the subscriberReply to join as subscriber. Should carry an offer in jsep: see the gap above.

Error codes ​

CodeMeaningCodeMeaning
421No message431Invalid SDP type
422Invalid JSON432Publishers full
423Invalid request (unknown request)433Unauthorized (wrong secret, PIN or token)
424Join first434Already published
425Already joined435Not published
426No such room436ID exists
427Room exists437Invalid SDP
428No such feed or user438Invalid feed
429Missing mandatory element499Unknown error
430Invalid element

Behaviour you may not expect ​

  • A create without room gets a random id (a large number: see the last create in the first transcript). A room without description is described as Room <id> (verified). create of an id that exists is 427.
  • publisher in listparticipants is false for a participant who has joined but whose WebRTC connection is not up, even right after publish (verified: publish, no browser, publisher: false). unpublish in that state is error 435.
  • A subscriber whose feed is not publishing yet gets error 428 No such feed (<id>), not an attached event. Subscribe after you see the publishers event or webrtcup.
  • Join into a room with a pin without one: 429 Missing mandatory element (pin). A wrong PIN is 433 Unauthorized (wrong pin) (verified). Joining twice on one handle is 425 Already in as a publisher on this handle.
  • edit, destroy, kick, allowed, moderate, enable_recording and listforwarders check secret only when the room has one.
  • The allowed array is returned in hash-table order, as in C.
  • request values are matched case-insensitively: LIST works (verified).

Known gaps ​

GapWhat happensEvidence
Subscriber offer is missingjoin 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 SVCThe 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 publisherPorted 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 roomA 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.

GPL-3.0-or-later, like the Janus gateway it ports.