Skip to content

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.

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

  1. Send the first message with your SDP offer in jsep. The plugin answers in the ok event's jsep.
  2. Send more messages without jsep to change settings while the call runs.
  3. 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.

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

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

FieldTypeMeaning
audiobooleanSend incoming audio back (true) or drop it (false).
videobooleanThe same for video.
bitratepositive integerCap, 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.
recordbooleanStart or stop recording what the plugin receives. One .mjr file per negotiated medium.
filenamestringBase path for the recording. The plugin adds -audio.mjr, -video.mjr, -data.mjr.
audiocodec, videocodecstringCodec to prefer when negotiating a new PeerConnection (for example vp9). Only used with an offer.
videoprofilestringCodec profile for VP9 (0, 2) or H.264 (for example 42e01f). Only used with an offer.
opusredbooleanNegotiate RED for Opus audio, if offered.
substream, temporal, fallbackintegerSimulcast: 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_layerintegerThe same for SVC: layer 0 to 2.
svcbooleanType-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_delayintegerPlayout 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 ​

EventWhenNotes
{"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).

CodeMeaning
411No message.
412The body is not valid JSON.
413Invalid element: wrong type, a layer above 2, or no recognised field.
414Invalid 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: false and video: false stop 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: false before the offer is still audio_active: false after it). record: true before 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 hint is I'm taking my time!, the same text the C server uses.
  • Over handle_info (Admin API) the plugin reports audio_active, video_active, audio_codec, video_codec, bitrate, peer-bitrate, the recording file names and slowlink_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 (e2ee in jsep) sets the flag and marks the recordings, as C does; it changes no relay. Not exercised with a browser.
  • The recording file names and .mjr layout follow record.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).

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