WebSockets transport
The WebSockets transport carries the Janus API over one long-lived socket. Requests go out as text frames, replies and events come back as text frames on the same socket, so there is no long poll. Use it for browsers (janus.js has a WebSocket mode) and for server-side clients. The Admin API has its own WebSocket listener on another port.
Enable and configure
The transport reads janus.transport.websockets.jcfg. The Janus API listener starts when general.ws is true, the Admin API listener when admin.admin_ws is true.
general: {
json = "indented" # indented, plain or compact (the default)
ws = true
ws_port = 8188
ws_ip = "127.0.0.1" # leave out to listen on every address
pingpong_trigger = 30 # seconds idle before Janus pings; set both or neither
pingpong_timeout = 10 # seconds more before Janus gives up on a silent client
}
admin: {
admin_ws = true # off by default
admin_ws_port = 7188
admin_ws_ip = "127.0.0.1"
}Secure WebSockets (wss, wss_port, admin_wss, ...), ws_acl, cors and certificates are in the transport files reference. The two APIs cannot share a port: they are separate listeners with separate subprotocols.
Try it
Janus API
The client below is Node's global WebSocket (Node 22 or later), the same class a browser has. The second argument of the constructor is the subprotocol, and it is required.
const ws = new WebSocket("ws://localhost:8188", "janus-protocol");
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.onopen = () => ws.send(JSON.stringify({ janus: "create", transaction: "c1" }));This is a full exchange, recorded with that class against the Rust binary. Events are frames you did not ask for; the transcript marks them with ⇠.
// new WebSocket("ws://localhost:8188", "janus-protocol"): open, subprotocol negotiated: janus-protocol
// There is no path on a WebSocket, so session_id and handle_id go in the JSON. No long poll: events are pushed on the same socket.
// → send
{
"janus": "create",
"transaction": "c1"
}
// ← receive
{
"janus": "success",
"transaction": "c1",
"data": {
"id": 4682622949715682
}
}
// → send
{
"janus": "attach",
"session_id": 4682622949715682,
"plugin": "janus.plugin.echotest",
"transaction": "a1"
}
// ← receive
{
"janus": "success",
"session_id": 4682622949715682,
"transaction": "a1",
"data": {
"id": 8888459584431263
}
}
// The plugin message gets an ack right away and the plugin's event follows as its own frame.
// → send
{
"janus": "message",
"session_id": 4682622949715682,
"handle_id": 8888459584431263,
"transaction": "m1",
"body": {
"audio": true
}
}
// ← receive
{
"janus": "ack",
"session_id": 4682622949715682,
"transaction": "m1",
"hint": "I'm taking my time!"
}
// ⇠ receive (pushed, no request)
{
"janus": "event",
"session_id": 4682622949715682,
"transaction": "m1",
"sender": 8888459584431263,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
}
}
// → send
{
"janus": "keepalive",
"session_id": 4682622949715682,
"transaction": "k1"
}
// ← receive
{
"janus": "ack",
"session_id": 4682622949715682,
"transaction": "k1"
}
// → send
{
"janus": "ping",
"transaction": "p1"
}
// ← receive
{
"janus": "pong",
"transaction": "p1"
}
// Closing the socket destroys the sessions it created (reclaim_session_timeout is 0 here).
// ← Admin API list_sessions after the close
{
"janus": "success",
"transaction": "dvla3h44av",
"sessions": []
}The handshake, and what is refused
A raw client shows what a browser hides.
// A raw client shows what the browser hides. Janus API port, subprotocol janus-protocol:
// → GET / HTTP/1.1
// Upgrade: websocket
// Sec-WebSocket-Protocol: janus-protocol
// ← HTTP/1.1 101 Switching Protocols
// connection: Upgrade
// upgrade: websocket
// sec-websocket-accept: <hash of the key>
// sec-websocket-protocol: janus-protocol
// access-control-allow-origin: *
// No subprotocol: the server closes the connection after the handshake request. The same for the Admin subprotocol on the Janus port.
// → GET / HTTP/1.1
// Upgrade: websocket
// (no Sec-WebSocket-Protocol)
// ← connection closed, no reply
// → GET / HTTP/1.1
// Upgrade: websocket
// Sec-WebSocket-Protocol: janus-admin-protocol
// ← connection closed, no reply
// A plain HTTP request on the WebSocket port:
// → GET / HTTP/1.1
// ← 403
// Errors on an open socket are JSON frames, as on HTTP:
// → send
not json
// ← receive
{
"janus": "error",
"error": {
"code": 454,
"reason": "Invalid Janus API request - <string>(1,3): '[' or '{' expected near 'not'"
}
}
// → send
{
"janus": "create"
}
// ← receive
{
"janus": "error",
"error": {
"code": 456,
"reason": "Missing mandatory element (transaction)"
}
}
// → send
{
"janus": "keepalive",
"session_id": 5,
"transaction": "k"
}
// ← receive
{
"janus": "error",
"session_id": 5,
"transaction": "k",
"error": {
"code": 458,
"reason": "No such session 5"
}
}Admin API
// new WebSocket("ws://localhost:7188", "janus-admin-protocol"): open, subprotocol: janus-admin-protocol
// → send
{
"janus": "ping",
"transaction": "a1"
}
// ← receive
{
"janus": "pong",
"transaction": "a1"
}
// → send
{
"janus": "list_sessions",
"transaction": "a2"
}
// ← receive
{
"janus": "error",
"transaction": "a2",
"error": {
"code": 403,
"reason": "Unauthorized request (wrong or missing secret/token)"
}
}
// → send
{
"janus": "list_sessions",
"transaction": "a3",
"admin_secret": "janusoverlord"
}
// ← receive
{
"janus": "success",
"transaction": "a3",
"sessions": []
}
// → send
{
"janus": "get_status",
"transaction": "a4",
"admin_secret": "janusoverlord"
}
// ← receive
{
"janus": "success",
"transaction": "a4",
"status": {
"token_auth": false,
"session_timeout": 60,
"reclaim_session_timeout": 0,
"candidates_timeout": 45,
"log_level": 4,
"log_timestamps": false,
"log_colors": true,
"log_rotate_sig": 0,
"locking_debug": false,
"refcount_debug": false,
"min_nack_queue": 200,
"nack-optimizations": false,
"no_media_timer": 1,
"slowlink_threshold": 0
}
}Ping and pong
// Config: pingpong_trigger = 2, pingpong_timeout = 2 (the sample file has them commented out). Janus pings after 2 s of silence. A client that answers keeps the connection; one that does not is dropped after the timeout.
// ⇠ Client that answers pings (6.5 s)
+2.0 s server sends ping
+2.0 s client answers pong
+4.1 s server sends ping
+4.1 s client answers pong
+6.1 s server sends ping
+6.1 s client answers pong
// ⇠ Client that does not answer
+2.0 s server sends ping (client stays silent)
+4.1 s connection closed
// The client can ping too. Janus answers with a pong carrying the same payload.
// ⇠ Janus answers a client ping with payload "hello"
opcode 10: "hello"Reference
Subprotocols
| Listener | Subprotocol |
|---|---|
Janus API (ws_port) | janus-protocol |
Admin API (admin_ws_port) | janus-admin-protocol |
A client that sends no Sec-WebSocket-Protocol, or only the other listener's name, is refused: the server closes the connection after reading the upgrade request and sends no HTTP response. A client that offers several names gets the first one the listener knows. The upgrade response carries Access-Control-Allow-Origin (see CORS below).
Frames
| Direction | Frame | Meaning |
|---|---|---|
| client to Janus | text | One JSON request. Put session_id and, for handle requests, handle_id in the JSON: there is no path. |
| Janus to client | text | A reply (it has the transaction of the request) or an event pushed later. The first reply to a message is ack. |
| both | ping, pong | Keepalive. Janus answers a client ping with a pong carrying the same payload. |
| either | close | Closing the socket ends the sessions it created (see below). |
Binary frames are not part of the protocol.
A request that is not JSON gets a JSON error frame (code 454), and the socket stays open. A request with no transaction gets error 456.
Sessions follow the socket
A session created on a WebSocket is tied to that socket: its events arrive there with no GET, and no request is needed to subscribe. When the socket closes, the sessions end (the transcript shows list_sessions empty afterwards). reclaim_session_timeout in janus.jcfg can keep them for a window; a new socket takes them over with the claim request.
Ping and pong
If pingpong_trigger and pingpong_timeout are both above 0, Janus pings a socket that has been idle for pingpong_trigger seconds. If nothing valid arrives in pingpong_trigger + pingpong_timeout seconds, it closes the socket. The recorded run used 2 and 2: a client that answered the pings stayed connected, one that stayed silent was closed at 4 seconds. If only one of the two is set, Janus warns and turns the keepalive off. With neither set, Janus never pings; the client must keep the session alive with keepalive requests or traffic.
Browsers answer ping frames by themselves, so a browser client needs nothing. A script that uses a library without automatic pongs must answer them.
CORS and the Origin check
On an upgrade Janus adds Access-Control-Allow-Origin to the response: *, or the cors.allow_origin value. With cors.enforce_cors = true and an allow_origin, an upgrade whose Origin does not start with that value is refused. (From the source: not exercised here.) A plain HTTP request on the WebSocket port is answered 403.
Events to handlers
general.events (default true) makes the transport report connected and disconnected to the event handlers.
Behaviour you may not expect
- verified A client without a subprotocol gets nothing back, not an HTTP error. A
curlof the port gets a plain403. - From the source: a connection handles its frames in order, so a plugin message that takes long holds up the later frames of the same connection and no other.
- verified An error for a request that is not JSON has no
transaction, since none could be read. - Events are queued and written by the connection's own loop, within about 100 ms of the plugin raising them.
Known gaps
- gap
ws_unix,wss_unix,admin_ws_unixandadmin_wss_unix(WebSockets over a Unix socket) are ignored. The listener does not fail: it starts on TCP port 8188 instead, becausews_portis missing. Check what the server logs at start (WebSockets server started (port ...)) when you rely on a Unix socket. - gap
ws_logging(libwebsockets log levels) is read and not used; there is no libwebsockets. - gap Only
Sec-WebSocket-Version: 13has been checked.parity/status.jsonlists versions 8 and 12 as open. - gap
ws_aclandadmin_ws_acltake CIDR ranges ("127.0.0.0/8") here, not the address prefixes of the C sample ("127.,192.168.0."). A prefix list stops Janus at start:general.ws_acl in the WebSockets transport config is not a valid access list: invalid ACL address '127.'. Checked. The HTTPaclstill takes prefixes. A client that the list refuses gets the upgrade, then the close. - gap The
connectedanddisconnectedtransport events of one connection carry different ids, andsession createdevents do not carry thetransportobject. See event types. - gap
query_transportanswers error 490.
Test it in Janus Bench
On the Threshold page set Transport for plugins to ws, or pass --transport ws to the headless runner. core.info.ws and core.session.ws on Run all checks use the Janus WebSocket; the Admin API page uses janus-admin-protocol.