Event handlers
An event handler sends what happens inside Janus to another system. Sessions starting and ending, plugins attaching, ICE and DTLS changes, media statistics, plugin events such as "a participant joined the room": each is one JSON document, and a handler forwards it over HTTP, a WebSocket, a message queue, MQTT, nanomsg or GELF. Use event handlers for monitoring, billing, call-detail records and dashboards. They are separate from the API a client uses: a client never sees them.
The shape of every event is on the event types page. This page is about turning handlers on and receiving the events.
Turn them on
Two things must be true: event handlers must be switched on in janus.jcfg, and the handler you want must be enabled in its own file.
events: {
broadcast = true # load event handlers at all (default: off)
#disable = "libjanus_sampleevh.so" # leave some out (comma separated library names)
#stats_period = 5 # seconds between media statistics per handle (default 1, 0 = off)
#combine_media_stats = true # one event per handle with all media in an array
}general: {
enabled = true
events = "all" # or a list: sessions, handles, jsep, webrtc, media, plugins, transports, core, external
grouping = true # several events in one POST (a JSON array)
json = "indented"
backend = "http://127.0.0.1:9000/events"
}The same effect from the command line: janus -e (--event-handlers) turns event handlers on even when events.broadcast is false.
This is what the start-up log says in each case (the first handler of the list has an enabled config here):
// events.broadcast = true (the dev config) and sampleevh enabled
// ⇠ janus log (event handler lines)
Event handler plugins folder: /path/to/events
Loading event handler plugin 'libjanus_sampleevh.so'...
Loading event handler plugin 'libjanus_rabbitmqevh.so'...
Loading event handler plugin 'libjanus_wsevh.so'...
Loading event handler plugin 'libjanus_mqttevh.so'...
Loading event handler plugin 'libjanus_nanomsgevh.so'...
Loading event handler plugin 'libjanus_gelfevh.so'...
janus-rs 0.1.0 sample event handler started
// ← GET /janus/info (event_handlers and events)
{
"event_handlers": false,
"events": {}
}
// events.broadcast = false: handlers are not loaded at all
// ⇠ janus log (event handler lines)
Event handlers support disabled
// ← GET /janus/info (event_handlers and events)
{
"event_handlers": false,
"events": {}
}
// events.broadcast = false but janus is started with -e
// ⇠ janus log (event handler lines)
Event handler plugins folder: /path/to/events
Loading event handler plugin 'libjanus_sampleevh.so'...
Loading event handler plugin 'libjanus_rabbitmqevh.so'...
Loading event handler plugin 'libjanus_wsevh.so'...
Loading event handler plugin 'libjanus_mqttevh.so'...
Loading event handler plugin 'libjanus_nanomsgevh.so'...
Loading event handler plugin 'libjanus_gelfevh.so'...
janus-rs 0.1.0 sample event handler started
// ← GET /janus/info (event_handlers and events)
{
"event_handlers": false,
"events": {}
}
// events.broadcast = true, disable = "libjanus_sampleevh.so,libjanus_gelfevh.so"
// ⇠ janus log (event handler lines)
Event handler plugins folder: /path/to/events
[WARN] Event handler plugin 'libjanus_sampleevh.so' has been disabled, skipping...
Loading event handler plugin 'libjanus_rabbitmqevh.so'...
Loading event handler plugin 'libjanus_wsevh.so'...
Loading event handler plugin 'libjanus_mqttevh.so'...
Loading event handler plugin 'libjanus_nanomsgevh.so'...
[WARN] Event handler plugin 'libjanus_gelfevh.so' has been disabled, skipping...
// ← GET /janus/info (event_handlers and events)
{
"event_handlers": false,
"events": {}
}How to read it:
- With
broadcast = false(or noeventssection) Janus logsEvent handlers support disabledand loads nothing. - With it on, Janus lists all six handlers as
Loading event handler plugin 'libjanus_...so'.... This does not mean they run: a handler whose own file is missing or hasenabled = falsedoes nothing. A handler that starts logs its own line (janus-rs 0.1.0 sample event handler started). disablenames handlers by the library name the C server uses, even though this port has no shared libraries:libjanus_sampleevh.so,libjanus_rabbitmqevh.so,libjanus_wsevh.so,libjanus_mqttevh.so,libjanus_nanomsgevh.so,libjanus_gelfevh.so.
info does not show event handlers
GET /janus/info reports "event_handlers": false and "events": {} even when a handler runs, as the last lines of the transcript show. The C server reports true and lists each handler. Do not use info to check whether handlers are up: read the start-up log.
Choosing events
events is a comma separated list of labels: sessions, handles, external, jsep, webrtc, media, plugins, transports, core. all selects everything and none nothing. The comparison ignores case and a space after a comma is fine. Every event has a type number; the numbers and labels are in the event types table. You cannot choose a subtype: webrtc brings all of ICE, DTLS and candidates.
Change a handler while Janus runs
The Admin API request query_eventhandler passes a request to one handler. In the C server every handler answers tweak, which changes the settings that make sense at run time (the event mask, grouping, the backend). I ran it against the sample handler only. This changed its mask, with the Admin API on port 7088:
{"janus": "query_eventhandler", "handler": "janus.eventhandler.sampleevh",
"request": {"request": "tweak", "events": "sessions"}, "admin_secret": "janusoverlord", "transaction": "t1"}{"janus": "success", "transaction": "t1", "response": {"result": 200}}A handler name that is not loaded gets error 460 Invalid event handler; an unknown request gets the handler's own error (code 411 for the sample handler: Unknown request 'info'). Both replies were seen; the WebSocket handler also answers info, which was not run here.
Your own events
The Admin API request custom_event puts an event of type 4 (external) into the stream, so a script can mark moments (a deploy, a test run) on the same timeline: {"janus": "custom_event", "schema": "docs.example", "data": {...}}. It needs schema and data (error 456 if one is missing). The event types page shows what a handler receives.
The handlers
| Handler | Config file | Janus is | Sends | Receive it with |
|---|---|---|---|---|
| Sample HTTP | janus.eventhandler.sampleevh.jcfg | HTTP client | POST of JSON | any web server |
| WebSocket | janus.eventhandler.wsevh.jcfg | WebSocket client | one text frame per message | any WebSocket server |
| GELF | janus.eventhandler.gelfevh.jcfg | UDP or TCP client | GELF messages | Graylog or any GELF input |
| MQTT | janus.eventhandler.mqttevh.jcfg | MQTT client | one message per event, a topic per type | any MQTT subscriber |
| RabbitMQ | janus.eventhandler.rabbitmqevh.jcfg | AMQP client | one message per batch | a consumer of the queue |
| nanomsg | janus.eventhandler.nanomsgevh.jcfg | NN_PUB publisher | one SP message | an NN_SUB socket |
All six read enabled and events. json is read by every handler except GELF, and grouping by the sample, WebSocket, RabbitMQ and nanomsg handlers. The key tables are in the event handler files reference.
I received the events of every handler in a real run. The WebSocket and nanomsg servers are written by hand in the recording script (no library): an RFC 6455 server on node:http and node:crypto, and a 60-line SP peer. Details are at each handler.
Sample HTTP
Janus POSTs the event, with Content-Type: application/json, to backend. With grouping = true (the default) events that are ready together go in one request as a JSON array; otherwise each event is its own object.
Extra keys: backend_user and backend_pwd (basic authentication), max_retransmissions (default 5) and retransmissions_backoff (ms, default 100, doubling), compress and compression (gzip, with Content-Encoding: gzip). A request that fails is sent again that many times, then the event is lost; while it retries, later events queue behind it.
A local Node server was the backend. This is the first POST that held a session event, from a run that created a session, attached a handle, sent a message and destroyed the session:
// The handler sends to http://127.0.0.1:PORT/events. A local Node server prints what it receives.
// ← POST /events
// Content-Type: application/json
[
{
"emitter": "MyJanusInstance",
"type": 128,
"timestamp": 1793808463047519,
"event": {
"transport": "janus.transport.http",
"id": "2",
"data": {
"event": "request",
"admin_api": false,
"ip": "127.0.0.1",
"port": 65501
}
}
},
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808463047649,
"session_id": 1372779827942998,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
}
]
// The whole run made 4 POSTs, carrying 1, 1, 2, 4 events. grouping is on by default, so events that are queued together share one POST, as a JSON array.The first event is a transport event (HTTP saw a request), then the session event. The run made several POSTs; the note at the end says how many events each carried.
grouping = false is ignored
Set grouping = false and the handler still sends arrays, with several events per request. The transcript below shows it: the config said no grouping and the second POST has three events. In the C server each event is its own POST with a single object. The source of the Rust port reads the key group_events for this handler, not grouping, so the value from your file never arrives. Parse the body as "an array, or one object" until this is fixed.
// Config: grouping = false, events = "sessions,handles". Expected (C): one POST per event, each body a single object.
// ← POST /events
[
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808465235778,
"session_id": 2324111410325289,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
}
]
// ← POST /events
[
{
"emitter": "MyJanusInstance",
"type": 2,
"timestamp": 1793808465236352,
"session_id": 2324111410325289,
"handle_id": 5375966110689118,
"event": {
"name": "attached",
"plugin": "janus.plugin.echotest"
}
},
{
"emitter": "MyJanusInstance",
"type": 2,
"timestamp": 1793808465236807,
"session_id": 2324111410325289,
"handle_id": 5375966110689118,
"event": {
"name": "detached",
"plugin": "janus.plugin.echotest"
}
},
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808465236820,
"session_id": 2324111410325289,
"event": {
"name": "destroyed"
}
}
]https:// backends are supported (rustls; per parity/status.json, not run here). In the runs here the sample handler also lost the shutdown core event: when Janus stopped, the last event did not reach it, while GELF delivered it in the same run. Use another handler if you need that event.
WebSocket
Janus connects to backend (ws://, or wss://) as a client, with the subprotocol you name in subprotocol (or none), and sends each event as a text frame. It reconnects with a growing delay. While it is disconnected events pile up in memory; cap that with events_cap_on_reconnect (events kept while reconnecting) and events_max_queue (an absolute limit), both off by default.
The server below accepted the upgrade and printed the frames:
// Janus is the WebSocket client. It connects when it starts; the server below is a minimal RFC 6455 server in this script (node:http upgrade and node:crypto).
// → Upgrade request received
// path: /events
// Sec-WebSocket-Protocol: janus-events
// ⇠ text frame received
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808467414114,
"session_id": 2083157126904228,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
}
// ⇠ text frame received
{
"emitter": "MyJanusInstance",
"type": 2,
"timestamp": 1793808467414668,
"session_id": 2083157126904228,
"handle_id": 1796490975420706,
"event": {
"name": "attached",
"plugin": "janus.plugin.echotest"
}
}
// ⇠ text frame received
{
"emitter": "MyJanusInstance",
"type": 2,
"timestamp": 1793808467415459,
"session_id": 2083157126904228,
"handle_id": 1796490975420706,
"event": {
"name": "detached",
"plugin": "janus.plugin.echotest"
}
}
// ⇠ text frame received
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808467415482,
"session_id": 2083157126904228,
"event": {
"name": "destroyed"
}
}Janus sent a close frame when it stopped. ws_logging (the libwebsockets log levels) is accepted and ignored.
GELF
GELF is the Graylog log format. Janus wraps each event as a GELF message and sends it over UDP or TCP to backend and port (protocol = "udp" or "tcp").
| GELF field | Value |
|---|---|
version | "1.1" |
host | the emitter of the event (the server name) |
short_message | the name of the event type: Sessions, Handles, WebRTC, Media, Plugins, Transports, Core, Jsep, External |
level | the numeric event type |
timestamp | seconds, with microseconds as decimals |
full_message | the whole event |
Over UDP, a message larger than max_message_len (default 1024) is split into GELF chunks: 1e 0f, an 8-byte message id, the chunk number, the chunk count, then the data; Graylog puts them back together. compress = true (UDP only) compresses each message with zlib. Over TCP each message ends with a NUL byte.
UDP:
// Config: protocol = "udp", events = "sessions". Each datagram is one JSON document.
// ⇠ UDP message, 326 bytes
{
"timestamp": 1793808470.168378,
"host": "MyJanusInstance",
"version": "1.1",
"level": 1,
"short_message": "Sessions",
"full_message": {
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808470168378,
"session_id": 6594759896122534,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
}
}
// ⇠ UDP message, 252 bytes
{
"timestamp": 1793808470.168877,
"host": "MyJanusInstance",
"version": "1.1",
"level": 1,
"short_message": "Sessions",
"full_message": {
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808470168877,
"session_id": 6594759896122534,
"event": {
"name": "destroyed"
}
}
}TCP:
// Config: protocol = "tcp", events = "sessions". Each message ends with a NUL byte (shown here after the split).
// ⇠ TCP message, 326 bytes
{
"timestamp": 1793808471.842676,
"host": "MyJanusInstance",
"version": "1.1",
"level": 1,
"short_message": "Sessions",
"full_message": {
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808471842676,
"session_id": 5408321047095100,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
}
}
// ⇠ TCP message, 252 bytes
{
"timestamp": 1793808471.843346,
"host": "MyJanusInstance",
"version": "1.1",
"level": 1,
"short_message": "Sessions",
"full_message": {
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808471843346,
"session_id": 5408321047095100,
"event": {
"name": "destroyed"
}
}
}The startup event is big. The next run asked for core events only, to show chunking, and it ends with the shutdown event, sent when Janus stops:
// events = "core". At start Janus sends one big event (the server info): it does not fit in max_message_len, so it goes out as GELF chunks. Each chunk starts with 1e 0f, an 8-byte message id, the chunk number and the chunk count.
// ⇠ 5 chunked datagrams
chunk 0 of 5: 1036 bytes, starts 1e 0f 36 76 41 74 6c 71 59 6b 00 05
chunk 1 of 5: 1036 bytes, starts 1e 0f 36 76 41 74 6c 71 59 6b 01 05
chunk 2 of 5: 1036 bytes, starts 1e 0f 36 76 41 74 6c 71 59 6b 02 05
chunk 3 of 5: 1036 bytes, starts 1e 0f 36 76 41 74 6c 71 59 6b 03 05
chunk 4 of 5: 374 bytes, starts 1e 0f 36 76 41 74 6c 71 59 6b 04 05
// A small event is sent whole. At stop (SIGTERM) Janus sends the shutdown event.
// ⇠ UDP message
{
"timestamp": 1793808476.523481,
"host": "MyJanusInstance",
"version": "1.1",
"level": 256,
"short_message": "Core",
"full_message": {
"emitter": "MyJanusInstance",
"type": 256,
"subtype": 2,
"timestamp": 1793808476523481,
"event": {
"status": "shutdown",
"signum": 15
}
}
}The compressed bytes the port produces differ from zlib's (a different deflate implementation) but decompress to the same JSON.
MQTT
Janus connects to the broker as a client and publishes each event on topic (default /janus/events), plus the event type label when addevent is on (the default): /janus/events/sessions, /janus/events/handles, /janus/events/webrtc and so on. Every event also gets an eventtype key with the readable name (Sessions). connect_status and disconnect_status are published on <topic>/status, and will_enabled sets the last will.
The keys are url, client_id, username, password, qos, retain, mqtt_version (3.1, 3.1.1 or 5, and add_user_properties for 5), the tls_* keys, and keep_alive_interval, cleansession, max_inflight, max_buffered, disconnect_timeout. They are in the reference.
The broker was mosquitto 2 in Docker, and mosquitto_sub -v -t '#' printed what it saw. The topic was janus/events:
// Config: topic = "janus/events", addevent on (the default), events = "sessions,handles". mosquitto_sub -v -t "#" printed one line per message: the topic, then the event.
// ⇠ topic janus/events/status
{
"event": "connected",
"eventhandler": "janus.eventhandler.mqttevh"
}
// ⇠ topic janus/events/sessions
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808481178756,
"session_id": 6505430838492586,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
},
"eventtype": "Sessions"
}
// ⇠ topic janus/events/handles
{
"emitter": "MyJanusInstance",
"type": 2,
"timestamp": 1793808481179571,
"session_id": 6505430838492586,
"handle_id": 5815717739184433,
"event": {
"name": "attached",
"plugin": "janus.plugin.echotest"
},
"eventtype": "Handles"
}
// ⇠ topic janus/events/handles
{
"emitter": "MyJanusInstance",
"type": 2,
"timestamp": 1793808481179988,
"session_id": 6505430838492586,
"handle_id": 5815717739184433,
"event": {
"name": "detached",
"plugin": "janus.plugin.echotest"
},
"eventtype": "Handles"
}
// ⇠ topic janus/events/sessions
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808481180002,
"session_id": 6505430838492586,
"event": {
"name": "destroyed"
},
"eventtype": "Sessions"
}addevent = false does not turn the topic suffix off here, and does not in the C server either: only true is read, and the default is on.
RabbitMQ
Janus publishes the events to exchange (empty means the default exchange) with the routing key route_key, and declares a queue of that name. exchange_type is fanout, direct, topic or headers. With grouping on, events that are ready together share one message, as a JSON array. host, port, username, password, vhost, heartbeat, retry_interval, declare_outgoing_queue and ssl_* work as for the transport.
The broker was RabbitMQ 3.13 in Docker; the messages were read from the queue with the management API:
// Config: route_key = "janus-events" and no exchange, so Janus publishes to the default exchange with that routing key. It declares a queue of the same name. events = "sessions", grouping left at its default (on), so events that are ready together share one message, as a JSON array.
// ⇠ message on queue janus-events
// routing_key: janus-events
// exchange:
[
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808490430504,
"session_id": 426261730346739,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
},
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808490431816,
"session_id": 426261730346739,
"event": {
"name": "destroyed"
}
}
]RabbitMQ 4 refuses the transient queue the handler declares by default: set declare_outgoing_queue = "no" and create the queue yourself (from the status notes; not run on version 4).
nanomsg
The handler is an NN_PUB socket. It publishes to address with mode = "bind" (Janus listens, subscribers connect) or mode = "connect" (the default for this handler: Janus dials a subscriber that listens). Subscribers do their own filtering; on the wire Janus sends everything.
The subscriber below is a hand-written SP peer (handshake with NN_SUB, then length-prefixed messages), not libnanomsg:
// Config: mode = "bind", address = "tcp://127.0.0.1:5555", grouping = false, events = "sessions". The subscriber below is a hand-written SP peer (protocol NN_SUB, 0x0021).
// ⇠ SP handshake: we send 00 53 50 00 00 21 00 00 (NN_SUB); Janus answers (NN_PUB, 0x0020)
00 53 50 00 00 20 00 00
// ⇠ message received
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808492929784,
"session_id": 5302617955395476,
"event": {
"name": "created",
"transport": {
"transport": "janus.transport.http",
"id": "6238072747940578789"
}
}
}
// ⇠ message received
{
"emitter": "MyJanusInstance",
"type": 1,
"timestamp": 1793808492930912,
"session_id": 5302617955395476,
"event": {
"name": "destroyed"
}
}Behaviour you may not expect
- Each handler has its own queue: a slow backend does not slow Janus, and a dead one only delays its own events (from the C design notes in
eventhandler.h; I did not stall a backend here). - verified Two handlers can run at once, each with its own mask; the sample handler and a GELF handler ran together here.
- verified A startup event (type 256) is sent when Janus starts and every 15 seconds afterwards with
"status": "update"and the counts of sessions, handles and PeerConnections. - The
timestampof an event is in microseconds since the Unix epoch (wall clock), soDate(timestamp / 1000)works in JavaScript. The C header describes it as monotonic time; the C code and this port both write the real time.
Known gaps
- gap The sample HTTP handler ignores
grouping = false. - gap
inforeportsevent_handlers: falseand no handlers, whatever runs. - gap The sample HTTP handler dropped the core
shutdownevent in the recorded run. - gap Sessions created over WebSockets, Unix sockets and nanomsg send
session createdevents without thetransportobject. See event types. - gap The
rttof a media statistics event is far too large in the recorded run: see event types. - gap
ws_loggingon the WebSocket handler is read and not used. - gap TLS for MQTT, RabbitMQ, WebSocket (
wss://) and the sample handler (https://) is ported and compared with the C server in the harness. I did not run any of them over TLS. - gap A SUB peer that sends data to the nanomsg handler: libnanomsg aborts, the port ignores it.
Test it in Janus Bench
Janus Bench does not run an event handler. It reads info only.