Skip to content

TextRoom ​

janus.plugin.textroom is a text chat built on WebRTC data channels. Users join rooms, broadcast to a room, and whisper to one user, all over one PeerConnection that carries no audio and no video. You use it when an application needs real-time text next to Janus and you do not want another server for it.

Two kinds of request exist, and the difference matters:

  • Over the Janus API you only do setup (setup, ack) and room management (create, edit, destroy, list, exists, listparticipants, allowed, kick, announcement). The key is request. These answer at once.
  • Over the data channel you do everything, including join, leave and message. The key is textroom, and every message must carry a transaction string that the reply copies back.

You cannot join a room or send a message through the Janus API.

Enable and configure ​

Build with the plugin-textroom feature. The plugin reads janus.plugin.textroom.jcfg:

text
general: {
	json = "compact"
}
room-1234: {
	description = "Demo Room"
	secret = "adminpwd"
}

json = "compact" makes the server send each data channel message as one line of JSON. Every key, including pin, history, is_private, post, admin_key and string_ids, is in the TextRoom config reference.

Permanent rooms rewrite the file

create, edit and destroy with "permanent": true write the room into janus.plugin.textroom.jcfg (the reply says "permanent": true). The file is regenerated, so comments in it are lost.

Try it ​

Room management over the Janus API ​

No PeerConnection is needed. The transcript runs list, exists, create (with a PIN, a secret and a history of 5 messages), edit, a wrong secret, allowed, kick, destroy, and two requests the plugin refuses.

jsonc
// TextRoom requests that go through the Janus API: no PeerConnection needed.

// Here the request key is `request`; on a data channel it is `textroom` (see textroom-chat).

// Room 1234 comes from janus.plugin.textroom.jcfg (secret adminpwd).


// → POST /janus
{
  "janus": "create",
  "transaction": "t-create"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-create",
  "data": {
    "id": 4100001234567908
  }
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.textroom",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100002469135799
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "success",
      "list": [
        {
          "room": 1234,
          "description": "Demo Room",
          "pin_required": false,
          "num_participants": 0,
          "history": 0
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "exists",
    "room": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-2",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "success",
      "room": 1234,
      "exists": true
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-3",
  "body": {
    "request": "exists",
    "room": 99
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-3",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "success",
      "room": 99,
      "exists": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-4",
  "body": {
    "request": "listparticipants",
    "room": 1234
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-4",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "success",
      "room": 1234,
      "participants": []
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-5",
  "body": {
    "request": "create",
    "room": 5678,
    "description": "Docs room",
    "secret": "s3cret",
    "pin": "1111",
    "history": 5,
    "is_private": false
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-5",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "created",
      "room": 5678,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-6",
  "body": {
    "request": "create",
    "room": 5678
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-6",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "error",
      "error_code": 418,
      "error": "Room 5678 already exists"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-7",
  "body": {
    "request": "list"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-7",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "success",
      "list": [
        {
          "room": 1234,
          "description": "Demo Room",
          "pin_required": false,
          "num_participants": 0,
          "history": 0
        },
        {
          "room": 5678,
          "description": "Docs room",
          "pin_required": true,
          "num_participants": 0,
          "history": 5
        }
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-8",
  "body": {
    "request": "edit",
    "room": 5678,
    "secret": "s3cret",
    "new_description": "Renamed room"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-8",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "edited",
      "room": 5678,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-9",
  "body": {
    "request": "edit",
    "room": 5678,
    "secret": "wrong",
    "new_description": "x"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-9",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "error",
      "error_code": 419,
      "error": "Unauthorized (wrong secret)"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-10",
  "body": {
    "request": "allowed",
    "room": 5678,
    "secret": "s3cret",
    "action": "add",
    "allowed": [
      "token-a",
      "token-b"
    ]
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-10",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "success",
      "room": 5678,
      "allowed": [
        "token-b",
        "token-a"
      ]
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-11",
  "body": {
    "request": "kick",
    "room": 5678,
    "secret": "s3cret",
    "username": "nobody"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-11",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "error",
      "error_code": 423,
      "error": "No such user nobody in room 5678"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-12",
  "body": {
    "request": "destroy",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-12",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "destroyed",
      "room": 5678,
      "permanent": false
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-13",
  "body": {
    "request": "destroy",
    "room": 5678,
    "secret": "s3cret"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-13",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "error",
      "error_code": 417,
      "error": "No such room (5678)"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-14",
  "body": {
    "request": "join",
    "room": 1234,
    "username": "alice"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-14",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "event",
      "error_code": 415,
      "error": "Unknown request 'join'"
    }
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-15",
  "body": {
    "request": "bogus"
  }
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-15",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "event",
      "error_code": 415,
      "error": "Unknown request 'bogus'"
    }
  }
}

Note that the plugin-level answer sits in plugindata.data. A failed request is still "janus": "success" at the Janus level: read error_code in data. Some errors say "textroom": "error" and some say "textroom": "event" (an unknown request), as in the C plugin.

Chat over data channels ​

A browser follows the same steps as janus.js:

  1. Attach to janus.plugin.textroom and send {"request": "setup"}. You get an ack, then an event with "result": "ok" and a JSEP offer from the plugin.
  2. Create the PeerConnection with a data channel called JanusDataChannel, apply the offer, create the answer.
  3. Send {"request": "ack"} with the answer as jsep. When ICE and DTLS finish you get the webrtcup event and the data channel opens.
  4. Send JSON text on the data channel.

The transcript below was recorded with two real PeerConnections in headless Chrome, alice and bob. Lines marked with a double arrow are frames the server pushed. The server sends compact JSON; the transcript pretty-prints it.

jsonc
// Two users, alice and bob, each with a PeerConnection that has only a data channel.

// Room 1234 comes from janus.plugin.textroom.jcfg (secret adminpwd), json = compact.

// A double-arrow line is something the server pushed: a long-poll event or a data channel frame.


// → POST /janus
{
  "janus": "create",
  "transaction": "t-create"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "t-create",
  "data": {
    "id": 4100001234567908
  }
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.textroom",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100002469135799
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "setup"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-1"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "event",
  "session_id": 4100001234567908,
  "transaction": "t-1",
  "sender": 4100002469135799,
  "plugindata": {
    "plugin": "janus.plugin.textroom",
    "data": {
      "textroom": "event",
      "result": "ok"
    }
  },
  "jsep": {
    "type": "offer",
    "sdp": "v=0\r\no=- 4100003703703690 1 IN IP4 192.0.2.10\r\ns=Janus TextRoom plugin\r\n…"
  }
}

// → POST /janus/4100001234567908/4100002469135799
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "ack"
  },
  "jsep": {
    "type": "answer",
    "sdp": "v=0\r\no=- 6100000000001000000 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-2"
}

// ⇠ event, long poll GET /janus/<session>
{
  "janus": "webrtcup",
  "session_id": 4100001234567908,
  "sender": 4100002469135799
}

// → POST /janus/4100001234567908
{
  "janus": "attach",
  "plugin": "janus.plugin.textroom",
  "transaction": "t-attach"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 4100001234567908,
  "transaction": "t-attach",
  "data": {
    "id": 4100004938271581
  }
}

// → POST /janus/4100001234567908/4100004938271581
{
  "janus": "message",
  "transaction": "t-1",
  "body": {
    "request": "setup"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-1"
}

// → POST /janus/4100001234567908/4100004938271581
{
  "janus": "message",
  "transaction": "t-2",
  "body": {
    "request": "ack"
  },
  "jsep": {
    "type": "answer",
    "sdp": "v=0\r\no=- 6100000000002000000 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 4100001234567908,
  "transaction": "t-2"
}

// (bob did the same: attach, setup, ack, data channel open)


// → alice sends on the data channel
{
  "textroom": "join",
  "room": 1234,
  "username": "alice",
  "display": "Alice",
  "transaction": "tx1"
}

// ⇠ alice receives on the data channel
{
  "textroom": "join",
  "room": 1234,
  "username": "alice",
  "display": "Alice"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "participants": [],
  "transaction": "tx1"
}

// → bob sends on the data channel
{
  "textroom": "join",
  "room": 1234,
  "username": "bob",
  "display": "Bob",
  "transaction": "tx2"
}

// ⇠ bob receives on the data channel
{
  "textroom": "join",
  "room": 1234,
  "username": "bob",
  "display": "Bob"
}

// ⇠ alice receives on the data channel
{
  "textroom": "join",
  "room": 1234,
  "username": "bob",
  "display": "Bob"
}

// ⇠ bob receives on the data channel
{
  "textroom": "success",
  "participants": [
    {
      "username": "alice",
      "display": "Alice"
    }
  ],
  "transaction": "tx2"
}

// → bob sends on the data channel
{
  "textroom": "message",
  "room": 1234,
  "text": "hello room",
  "transaction": "tx3"
}

// ⇠ alice receives on the data channel
{
  "textroom": "message",
  "room": 1234,
  "from": "bob",
  "date": "2026-11-04T09:30:01+0000",
  "text": "hello room"
}

// ⇠ bob receives on the data channel
{
  "textroom": "message",
  "room": 1234,
  "from": "bob",
  "date": "2026-11-04T09:30:01+0000",
  "text": "hello room"
}

// ⇠ bob receives on the data channel
{
  "textroom": "success",
  "transaction": "tx3"
}

// → bob sends on the data channel
{
  "textroom": "message",
  "room": 1234,
  "to": "alice",
  "text": "psst",
  "ack": false,
  "transaction": "tx4"
}

// ⇠ alice receives on the data channel
{
  "textroom": "message",
  "room": 1234,
  "from": "bob",
  "date": "2026-11-04T09:30:01+0000",
  "text": "psst",
  "whisper": true
}

// → alice sends on the data channel
{
  "textroom": "listparticipants",
  "room": 1234,
  "transaction": "tx5"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "room": 1234,
  "participants": [
    {
      "username": "alice",
      "display": "Alice"
    },
    {
      "username": "bob",
      "display": "Bob"
    }
  ],
  "transaction": "tx5"
}

// → alice sends on the data channel
{
  "textroom": "list",
  "transaction": "tx6"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "list": [
    {
      "room": 1234,
      "description": "Demo Room",
      "pin_required": false,
      "num_participants": 2,
      "history": 0
    }
  ],
  "transaction": "tx6"
}

// → alice sends on the data channel
{
  "textroom": "announcement",
  "room": 1234,
  "secret": "adminpwd",
  "text": "Maintenance in five minutes",
  "transaction": "tx7"
}

// ⇠ alice receives on the data channel
{
  "textroom": "announcement",
  "room": 1234,
  "date": "2026-11-04T09:30:02+0000",
  "text": "Maintenance in five minutes"
}

// ⇠ bob receives on the data channel
{
  "textroom": "announcement",
  "room": 1234,
  "date": "2026-11-04T09:30:02+0000",
  "text": "Maintenance in five minutes"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "transaction": "tx7"
}

// → alice sends on the data channel
{
  "textroom": "kick",
  "room": 1234,
  "secret": "adminpwd",
  "username": "bob",
  "transaction": "tx8"
}

// ⇠ alice receives on the data channel
{
  "textroom": "kicked",
  "room": 1234,
  "username": "bob"
}

// ⇠ bob receives on the data channel
{
  "textroom": "kicked",
  "room": 1234,
  "username": "bob"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "transaction": "tx8"
}

// → bob sends on the data channel
{
  "textroom": "message",
  "room": 1234,
  "text": "can anyone hear me?",
  "transaction": "tx9"
}

// ⇠ bob receives on the data channel
{
  "textroom": "error",
  "error_code": 422,
  "error": "Not in room 1234",
  "transaction": "tx9"
}

// → alice sends on the data channel
{
  "textroom": "leave",
  "room": 1234,
  "transaction": "tx10"
}

// ⇠ alice receives on the data channel
{
  "textroom": "leave",
  "room": 1234,
  "username": "alice"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "transaction": "tx10"
}

// → alice sends on the data channel
{
  "textroom": "join",
  "room": 4321,
  "username": "alice",
  "transaction": "tx11"
}

// ⇠ alice receives on the data channel
{
  "textroom": "error",
  "error_code": 417,
  "error": "No such room (4321)",
  "transaction": "tx11"
}

// → alice sends on the data channel
{
  "textroom": "bogus",
  "transaction": "tx12"
}

// ⇠ alice receives on the data channel
{
  "textroom": "error",
  "error_code": 415,
  "error": "Unsupported request bogus",
  "transaction": "tx12"
}

What to notice:

  • A join sends a join event to everyone in the room, including the user who joined. The reply (success, with the participants that were already there) comes after.
  • A public message is delivered to everyone, including the sender. The sender also gets "success" unless it sent "ack": false.
  • A whisper (to) arrives with "whisper": true and only the recipient sees it.
  • After kick, the kicked user stays connected to the plugin but is out of the room: the next message fails with 422.
  • Errors on the data channel look like {"textroom": "error", "error_code": …, "error": …, "transaction": …}.

Rooms with a PIN and history over the data channel ​

jsonc
// Room management over the data channel: the same verbs as the Janus API, with `textroom` as the key.

// alice and bob are still connected from textroom-chat. Room 8000 keeps the last 2 messages and needs a PIN.


// → alice sends on the data channel
{
  "textroom": "create",
  "room": 8000,
  "description": "Pinned room with history",
  "pin": "4242",
  "secret": "s3cret",
  "history": 2,
  "is_private": false,
  "transaction": "tx13"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "room": 8000,
  "permanent": false,
  "transaction": "tx13"
}

// → alice sends on the data channel
{
  "textroom": "join",
  "room": 8000,
  "username": "alice",
  "pin": "wrong",
  "transaction": "tx14"
}

// ⇠ alice receives on the data channel
{
  "textroom": "error",
  "error_code": 419,
  "error": "Unauthorized (wrong pin)",
  "transaction": "tx14"
}

// → alice sends on the data channel
{
  "textroom": "join",
  "room": 8000,
  "username": "alice",
  "pin": "4242",
  "transaction": "tx15"
}

// ⇠ alice receives on the data channel
{
  "textroom": "join",
  "room": 8000,
  "username": "alice"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "participants": [],
  "transaction": "tx15"
}

// → alice sends on the data channel
{
  "textroom": "message",
  "room": 8000,
  "text": "first",
  "transaction": "tx16"
}

// ⇠ alice receives on the data channel
{
  "textroom": "message",
  "room": 8000,
  "from": "alice",
  "date": "2026-11-04T09:30:01+0000",
  "text": "first"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "transaction": "tx16"
}

// → alice sends on the data channel
{
  "textroom": "message",
  "room": 8000,
  "text": "second",
  "transaction": "tx17"
}

// ⇠ alice receives on the data channel
{
  "textroom": "message",
  "room": 8000,
  "from": "alice",
  "date": "2026-11-04T09:30:02+0000",
  "text": "second"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "transaction": "tx17"
}

// → alice sends on the data channel
{
  "textroom": "message",
  "room": 8000,
  "text": "third",
  "transaction": "tx18"
}

// ⇠ alice receives on the data channel
{
  "textroom": "message",
  "room": 8000,
  "from": "alice",
  "date": "2026-11-04T09:30:02+0000",
  "text": "third"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "transaction": "tx18"
}

// → bob sends on the data channel
{
  "textroom": "join",
  "room": 8000,
  "username": "bob",
  "pin": "4242",
  "transaction": "tx19"
}

// ⇠ bob receives on the data channel
{
  "textroom": "message",
  "room": 8000,
  "from": "alice",
  "date": "2026-11-04T09:30:02+0000",
  "text": "second"
}

// ⇠ bob receives on the data channel
{
  "textroom": "message",
  "room": 8000,
  "from": "alice",
  "date": "2026-11-04T09:30:02+0000",
  "text": "third"
}

// ⇠ bob receives on the data channel
{
  "textroom": "join",
  "room": 8000,
  "username": "bob"
}

// ⇠ bob receives on the data channel
{
  "textroom": "success",
  "participants": [
    {
      "username": "alice"
    }
  ],
  "transaction": "tx19"
}

// ⇠ alice receives on the data channel
{
  "textroom": "join",
  "room": 8000,
  "username": "bob"
}

// → alice sends on the data channel
{
  "textroom": "edit",
  "room": 8000,
  "secret": "s3cret",
  "new_description": "Renamed",
  "transaction": "tx20"
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "room": 8000,
  "permanent": false,
  "transaction": "tx20"
}

// → alice sends on the data channel
{
  "textroom": "destroy",
  "room": 8000,
  "secret": "s3cret",
  "transaction": "tx21"
}

// ⇠ alice receives on the data channel
{
  "textroom": "destroyed",
  "room": 8000
}

// ⇠ alice receives on the data channel
{
  "textroom": "destroyed",
  "room": 8000
}

// ⇠ alice receives on the data channel
{
  "textroom": "success",
  "room": 8000,
  "permanent": false,
  "transaction": "tx21"
}

// ⇠ bob receives on the data channel
{
  "textroom": "destroyed",
  "room": 8000
}

A room with history: 2 replays the last two messages to a new participant, before the join event and the reply.

Reference ​

Janus API requests ​

RequestParametersReply
setupnoneack, then event {"textroom": "event", "result": "ok"} with a JSEP offer
acknone, with the JSEP answerack
restartnoneevent with a new offer (ICE restart); answer it with ack
listadmin_key (string, optional){"textroom": "success", "list": [{room, description, pin_required, num_participants, history}]}
listparticipantsroom (number, required){"textroom": "success", "room", "participants": [{username, display}]}
existsroom (number, required){"textroom": "success", "room", "exists": true or false}
createroom (number, optional: random if missing), admin_key, description, secret, pin, is_private (default true), history (number of messages, default 0), post (HTTP backend URL), permanent (default false){"textroom": "created", "room", "permanent"}
editroom (required), secret (if the room has one), new_description, new_secret, new_pin, new_is_private, permanent{"textroom": "edited", "room", "permanent"}
destroyroom (required), secret, permanent{"textroom": "destroyed", "room", "permanent"}; the room's users get a destroyed event
allowedroom, secret, action (enable, disable, add, remove), allowed (array of tokens, for add and remove){"textroom": "success", "room", "allowed": […]}
kickroom, secret, username{"textroom": "success"}; the room gets a kicked event
announcementroom, secret, text{"textroom": "success"}; the room gets an announcement event

Over the Janus API the replies for create, edit and destroy say created, edited and destroyed. The same requests over the data channel reply success (see the transcript).

Data channel requests ​

Every request has "textroom": "<name>" and "transaction": "<string>". The management requests above work here too (list, listparticipants, exists, create, edit, destroy, allowed, kick, announcement), with textroom in place of request. In addition:

RequestParametersReply and side effects
joinroom (required), username (required, unique in the room), display, pin, token (if the room has an ACL), history (boolean, default true: replay history){"textroom": "success", "participants": […]}; everyone in the room, including you, gets a join event
leaveroom{"textroom": "success"}; the others get a leave event
messageroom (required), text (string, required), to (username) or tos (array of usernames) for a whisper, ack (default true){"textroom": "success"} if ack is true; recipients get a message event

to and tos cannot be used together. text is any string: send JSON as a string if you want structure.

Events the server pushes on the data channel ​

They have no transaction.

textroomFields
joinroom, username, display
leaveroom, username
messageroom, from, date (ISO 8601), text, whisper (only for whispers, true)
announcementroom, date, text
kickedroom, username
destroyedroom

Error codes ​

CodeMeaning
411, 412No message, invalid JSON
413, 414Missing element, invalid element
415Unknown or unsupported request
416Already set up
417, 418No such room, room exists
419Unauthorized (wrong secret, PIN or admin key)
420, 421, 422Username exists, already in the room, not in the room
423No such user
499Other error

Behaviour you may not expect ​

  • The join event reaches the joiner too, and a public message is echoed to its sender. Filter on username or from if you do not want that.
  • create without a room picks a random room id (a number of about 16 digits).
  • On the data channel an unknown request answers {"textroom": "error", "error_code": 415, "error": "Unsupported request bogus"}. Over the Janus API the text is Unknown request 'bogus', and the textroom value is event.
  • A data channel join is also refused over the Janus API: it answers 415 Unknown request 'join'.
  • Admin requests over the data channel (create, edit, destroy, allowed, kick, announcement) need a transaction like all the others.

Known gaps ​

  • HTTP backend (post). The binary POSTs room messages to http:// URLs and sends URL credentials as Basic auth. It refuses https:// (TLS is an owner decision, see plugins/textroom/src/lib.rs). Not exercised here: written from the source.
  • Not recorded here: restart (ICE restart) and allowed with a joining token. They are written from the C source and the Rust request table, not run against the binary.
  • Nothing else is known to differ from C. parity/traces/textroom* hold the C transcripts the Rust server is compared against.

Test it in Janus Bench ​

Open the TextRoom page, or run the check textroom.chat from Run all checks. It sets up two users, joins room 1234, sends a public message and a whisper, and lists the participants. Result today: verified.

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