EchoTest
EchoTest sends back whatever the browser sends it: audio, video and data channel messages. It is the smallest plugin and the first one to try. If an echo works, signalling, ICE, DTLS, SRTP and the plugin relay all work on your setup, so use it to check a new deployment, a firewall or a client library before you touch anything bigger. You can also switch audio or video off, cap the bitrate and record what the plugin received.
Enable and configure
EchoTest is in the default build (plugin-echotest is a default feature of the janus crate) and starts without any configuration. To leave it out at run time, name it in plugins.disable in janus.jcfg.
general: {
#events = false # true|false: send events to event handlers (default true)
}events is the only key. The full table is in the plugin files reference.
Try it
EchoTest has one request, with no name: you send a body of optional fields. It is asynchronous: the POST returns an ack and the result comes as an event with the same transaction. Attach to janus.plugin.echotest first (see the Janus API walkthrough), then:
- Send the first message with your SDP offer in
jsep. The plugin answers in theokevent'sjsep. - Send more messages without
jsepto change settings while the call runs. - Send your ICE candidates with
trickle.
This transcript is the signalling a real client sends, against the Rust binary. The SDP is a shortened, hand-made offer and nobody sends media, so ICE never completes here; it shows exactly what the server sends back for each request, including the errors.
// EchoTest: open a session, attach, negotiate with an offer, change settings, record, and the errors.
// Produced by docs-site/scripts/record-plugins-core.mjs against the Rust binary. The SDP is a shortened, hand-made offer; no browser is attached, so ICE and DTLS never complete.
// → 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.echotest",
"transaction": "t-2"
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-2",
"data": {
"id": 9728245592380508
}
}
// 1. Send the first message together with the offer. The reply to the POST is an ack; the answer comes as an event.
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-3",
"body": {
"audio": true,
"video": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 4661856518455979478 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-3",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-3",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 4661856518455979478 2 IN IP4 192.168.1.10\r\ns=-\r\n…"
}
}
// 2. Change settings while the session runs: no jsep, only the fields you want to change.
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-4",
"body": {
"audio": false,
"bitrate": 128000
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-4",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-4",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
}
}
// 3. Start and stop a recording (one .mjr file per negotiated medium).
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-5",
"body": {
"record": true,
"filename": "/tmp/janus/echo-demo"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-5",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-5",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-6",
"body": {
"record": false
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-6",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-6",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
}
}
// 4. Errors arrive as events too, with the same transaction.
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-7",
"body": {
"audio": "yes"
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-7",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-7",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"error_code": 413,
"error": "Invalid element type (audio should be a boolean)"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-8",
"body": {
"substream": 5
}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-8",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-8",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"error_code": 413,
"error": "Invalid value (substream should be 0, 1 or 2)"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-9",
"body": {}
}
// ← 200 OK
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "t-9",
"hint": "I'm taking my time!"
}
// ⇠ event, pushed by the server (long poll of GET /janus/{session_id})
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "t-9",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"error_code": 413,
"error": "Message error: no supported attributes found"
}
}
}
// → POST /janus/{session_id}/{handle_id}
{
"janus": "message",
"transaction": "t-10",
"body": {
"audio": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\nnot an sdp"
}
}
// ← 200 OK
{
"janus": "error",
"session_id": 9873482636819065,
"transaction": "t-10",
"error": {
"code": 465,
"reason": "Invalid line (2nd char is not '='): not an sdp"
}
}
// → POST /janus/{session_id}
{
"janus": "destroy",
"transaction": "t-11"
}
// ← 200 OK
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "t-11"
}The next transcript is what happens after the answer, recorded from Chrome in a Janus Bench run (the echotest.media check): trickle ICE, webrtcup, one media event per medium, a configure message, hangup. These frames were not produced by the script's own client.
// EchoTest in Chrome: what happens after the answer. Cut from a Janus Bench run (echotest.media); not produced by this script's own client.
// Candidates, the SDP bodies and the ids are shortened. Over HTTP the JSON below goes in the POST body or comes from the long poll; the Bench shows it as plain frames.
// → sent
{
"janus": "create",
"transaction": "bench-mw29jtbh-1"
}
// ← received
{
"janus": "success",
"transaction": "bench-mw29jtbh-1",
"data": {
"id": 9873482636819065
}
}
// → sent
{
"janus": "attach",
"plugin": "janus.plugin.echotest",
"transaction": "bench-mw29jtbx-2",
"session_id": 9873482636819065
}
// ← received
{
"janus": "success",
"session_id": 9873482636819065,
"transaction": "bench-mw29jtbx-2",
"data": {
"id": 9728245592380508
}
}
// → sent
{
"janus": "message",
"handle_id": 9728245592380508,
"body": {
"audio": true,
"video": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 466185651845597947 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
},
"transaction": "bench-mw29jti4-3",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jti4-3",
"hint": "I'm taking my time!"
}
// ← received
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "bench-mw29jti4-3",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 466185651845597947 2 IN IP4 192.168.1.10\r\ns=-\r\n…"
}
}
// → sent
{
"janus": "trickle",
"handle_id": 9728245592380508,
"candidate": {
"candidate": "candidate:9367238343 1 udp 4022915040 192.168.1.10 52584 typ host generation 0 ufrag d7qL network-cost 999",
"sdpMid": "0",
"sdpMLineIndex": 0
},
"transaction": "bench-mw29jtig-4",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jtig-4"
}
// (1 more trickle ICE candidate message(s) and their acks omitted)
// → sent
{
"janus": "trickle",
"handle_id": 9728245592380508,
"candidate": {
"completed": true
},
"transaction": "bench-mw29jtlh-6",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jtlh-6"
}
// ⇠ event pushed by the server
{
"janus": "webrtcup",
"session_id": 9873482636819065,
"sender": 9728245592380508
}
// ⇠ event pushed by the server
{
"janus": "media",
"session_id": 9873482636819065,
"sender": 9728245592380508,
"mid": "1",
"type": "video",
"receiving": true
}
// ⇠ event pushed by the server
{
"janus": "media",
"session_id": 9873482636819065,
"sender": 9728245592380508,
"mid": "0",
"type": "audio",
"receiving": true
}
// → sent
{
"janus": "message",
"handle_id": 9728245592380508,
"body": {
"audio": false,
"bitrate": 128000
},
"transaction": "bench-mw29jum0-7",
"session_id": 9873482636819065
}
// ← received
{
"janus": "ack",
"session_id": 9873482636819065,
"transaction": "bench-mw29jum0-7",
"hint": "I'm taking my time!"
}
// ← received
{
"janus": "event",
"session_id": 9873482636819065,
"transaction": "bench-mw29jum0-7",
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
}
}
// → sent
{
"janus": "hangup",
"handle_id": 9728245592380508,
"transaction": "bench-mw29jum3-8",
"session_id": 9873482636819065
}
// ⇠ event pushed by the server
{
"janus": "event",
"session_id": 9873482636819065,
"sender": 9728245592380508,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "done"
}
}
}
// ⇠ event pushed by the server
{
"janus": "hangup",
"session_id": 9873482636819065,
"sender": 9728245592380508,
"reason": "Janus API"
}TIP
Over HTTP the ack is the reply to your POST and the event arrives on the long poll, so a client may see them in either order in its own log. The server writes the ack first. With WebSockets they arrive as two frames in that order.
Reference
Request
All fields are optional, but a message with no recognised field is an error (413). Types are checked in this order and the first bad field is reported.
| Field | Type | Meaning |
|---|---|---|
audio | boolean | Send incoming audio back (true) or drop it (false). |
video | boolean | The same for video. |
bitrate | positive integer | Cap, in bits per second, that the plugin sends to the browser as REMB (for example 128000). 0, the default, means no cap: the plugin sends a REMB of 10 Mbit/s. |
record | boolean | Start or stop recording what the plugin receives. One .mjr file per negotiated medium. |
filename | string | Base path for the recording. The plugin adds -audio.mjr, -video.mjr, -data.mjr. |
audiocodec, videocodec | string | Codec to prefer when negotiating a new PeerConnection (for example vp9). Only used with an offer. |
videoprofile | string | Codec profile for VP9 (0, 2) or H.264 (for example 42e01f). Only used with an offer. |
opusred | boolean | Negotiate RED for Opus audio, if offered. |
substream, temporal, fallback | integer | Simulcast: the substream (0 to 2) and temporal layer (0 to 2) to send back, and microseconds without packets before falling back to a lower substream. |
spatial_layer, temporal_layer | integer | The same for SVC: layer 0 to 2. |
svc | boolean | Type-checked only. SVC and simulcast are switched on by the svc and simulcast members of the jsep object, not by this request. |
min_delay, max_delay | integer | Playout delay to write into the echoed RTP extension, in 10 ms blocks. |
The C documentation lists audio, video, bitrate, record, filename, the codec fields and the layer fields. fallback, opusred, min_delay and max_delay are in the C source's validation table but not in its documentation; the port reads them too.
Add jsep ({"type":"offer","sdp":"…"}) to negotiate a PeerConnection, or to renegotiate one (a new medium, an ICE restart).
Events
| Event | When | Notes |
|---|---|---|
{"echotest":"event","result":"ok"} | The request worked. | Carries the answer as jsep when the request had an offer. |
{"echotest":"event","error_code":N,"error":"…"} | The request failed. | See the codes below. |
{"echotest":"event","result":"done"} | The PeerConnection went away. | Followed by the core's hangup event. |
Besides these, the core pushes webrtcup, media, slowlink and hangup for the handle (WebRTC from a browser).
| Code | Meaning |
|---|---|
| 411 | No message. |
| 412 | The body is not valid JSON. |
| 413 | Invalid element: wrong type, a layer above 2, or no recognised field. |
| 414 | Invalid SDP. |
An SDP that does not parse is refused by the core before the plugin sees it, with a janus: "error" reply and code 465 (see the last request in the transcript).
Behaviour you may not expect
audio: falseandvideo: falsestop the echo only. The browser keeps sending, and Janus keeps receiving and counting those packets.- A setting sent before the first offer is stored and applied when the PeerConnection is made (verified:
audio: falsebefore the offer is stillaudio_active: falseafter it).record: truebefore an offer records nothing, because no medium is negotiated yet: no file is created. - Data channel text comes back with the prefix
Janus EchoTest here! You wrote:in front. Binary messages come back unchanged. - The ack's
hintisI'm taking my time!, the same text the C server uses. - Over
handle_info(Admin API) the plugin reportsaudio_active,video_active,audio_codec,video_codec,bitrate,peer-bitrate, the recording file names andslowlink_count. Recorded in this run:"recording":{"audio":"echo-demo-audio.mjr","video":"echo-demo-video.mjr"}.
Known gaps
EchoTest is the most complete plugin in the port. The Bench checks echotest.media, echotest.datachannel and echotest.record all pass in headless Chrome.
- Simulcast and SVC selection are ported and covered by unit tests, but no browser run has exercised them yet (gap).
- E2EE (
e2eeinjsep) sets the flag and marks the recordings, as C does; it changes no relay. Not exercised with a browser. - The recording file names and
.mjrlayout followrecord.c, checked by tests rather than by a run against the C server.
Test it in Janus Bench
Open the EchoTest page, or run the checks echotest.media, echotest.datachannel and echotest.record from Run all checks (see Janus Bench).