WebRTC from a browser
How a browser gets media in and out of the gateway, step by step, with the messages of a real session. The recording below is headless Chrome running the Bench check echotest.media against the Rust binary.
The steps
- Create a session and a handle, as in the walkthrough.
- Make an offer in the browser. Add your audio and video tracks, call
createOffer()andsetLocalDescription(). - Send it. A
messageto the handle with the plugin's request inbodyand the offer asjsep: {type: "offer", sdp}. The reply isack. - Apply the answer. The plugin's
eventarrives withjsep: {type: "answer", sdp}. CallsetRemoteDescription()with it. - Trickle candidates. Send each
icecandidatethe browser gathers as{"janus":"trickle","candidate":{candidate,sdpMid,sdpMLineIndex}}, and{"completed":true}at the end. Send them after step 3, not before: the server needs the offer first. The Bench queues candidates until the offer has gone out. - Wait for
webrtcup. ICE and DTLS are done and SRTP keys are set. Media flows. - Watch for
media,slowlinkandhangup.mediasays audio or video started or stopped arriving;slowlinksays packets are being lost;hangupsays the PeerConnection closed, with areason. - End it.
hangupto close the PeerConnection and keep the handle,detachto drop the handle,destroyto drop the session.
What the server sends you
jsonc
// Recorded from headless Chrome running the Bench check echotest.media against the Rust binary. SDP bodies are shortened.
// → http request: create
{
"janus": "create",
"transaction": "bench-mw29lqgs-1"
}
// ← http reply: success
{
"janus": "success",
"transaction": "bench-mw29lqgs-1",
"data": {
"id": 8200833398347707
}
}
// → http request: attach
{
"janus": "attach",
"plugin": "janus.plugin.echotest",
"transaction": "bench-mw29lqh0-2",
"session_id": 8200833398347707
}
// ← http reply: success
{
"janus": "success",
"session_id": 8200833398347707,
"transaction": "bench-mw29lqh0-2",
"data": {
"id": 1087002850853293
}
}
// → http request: message
{
"janus": "message",
"handle_id": 1087002850853293,
"body": {
"audio": true,
"video": true
},
"jsep": {
"type": "offer",
"sdp": "v=0\r\no=- 4758997107471883516 2 IN IP4 127.0.0.1\r\ns=-\r\n…"
},
"transaction": "bench-mw29lqmv-3",
"session_id": 8200833398347707
}
// ← http reply: ack
{
"janus": "ack",
"session_id": 8200833398347707,
"transaction": "bench-mw29lqmv-3",
"hint": "I'm taking my time!"
}
// ← http reply: event
{
"janus": "event",
"session_id": 8200833398347707,
"transaction": "bench-mw29lqmv-3",
"sender": 1087002850853293,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
},
"jsep": {
"type": "answer",
"sdp": "v=0\r\no=- 4758997107471883516 2 IN IP4 192.0.2.10\r\ns=-\r\n…"
}
}
// → http request: trickle
{
"janus": "trickle",
"handle_id": 1087002850853293,
"candidate": {
"candidate": "candidate:246734890 1 udp 2113937151 192.0.2.10 56463 typ host generation 0 ufrag MVSd network-cost 999",
"sdpMid": "0",
"sdpMLineIndex": 0
},
"transaction": "bench-mw29lqn3-4",
"session_id": 8200833398347707
}
// ← http reply: ack
{
"janus": "ack",
"session_id": 8200833398347707,
"transaction": "bench-mw29lqn3-4"
}
// ← http reply: webrtcup
{
"janus": "webrtcup",
"session_id": 8200833398347707,
"sender": 1087002850853293
}
// ← http reply: media (video)
{
"janus": "media",
"session_id": 8200833398347707,
"sender": 1087002850853293,
"mid": "1",
"type": "video",
"receiving": true
}
// ← http reply: media (audio)
{
"janus": "media",
"session_id": 8200833398347707,
"sender": 1087002850853293,
"mid": "0",
"type": "audio",
"receiving": true
}
// → http request: message
{
"janus": "message",
"handle_id": 1087002850853293,
"body": {
"audio": false,
"bitrate": 128000
},
"transaction": "bench-mw29lrqg-7",
"session_id": 8200833398347707
}
// ← http reply: ack
{
"janus": "ack",
"session_id": 8200833398347707,
"transaction": "bench-mw29lrqg-7",
"hint": "I'm taking my time!"
}
// ← http reply: event
{
"janus": "event",
"session_id": 8200833398347707,
"transaction": "bench-mw29lrqg-7",
"sender": 1087002850853293,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "ok"
}
}
}
// → http request: hangup
{
"janus": "hangup",
"handle_id": 1087002850853293,
"transaction": "bench-mw29lrql-8",
"session_id": 8200833398347707
}
// ← http reply: success
{
"janus": "success",
"session_id": 8200833398347707,
"transaction": "bench-mw29lrql-8"
}
// ← http reply: event
{
"janus": "event",
"session_id": 8200833398347707,
"sender": 1087002850853293,
"plugindata": {
"plugin": "janus.plugin.echotest",
"data": {
"echotest": "event",
"result": "done"
}
}
}
// ← http reply: hangup
{
"janus": "hangup",
"session_id": 8200833398347707,
"sender": 1087002850853293,
"reason": "Janus API"
}
// → http request: destroy
{
"janus": "destroy",
"transaction": "bench-mw29lrqs-9",
"session_id": 8200833398347707
}
// ← http reply: success
{
"janus": "success",
"session_id": 8200833398347707,
"transaction": "bench-mw29lrqs-9"
}
// (the browser sent three trickle requests, one per candidate and a final {completed:true}; only the first is shown)| Event | Fields | When |
|---|---|---|
webrtcup | sender | After ICE connected and the DTLS handshake finished. |
media | type (audio/video), receiving (bool), mid | When media starts to arrive, or stops for no_media_timer seconds. |
slowlink | uplink (bool), nacks | When the lost packets per second pass slowlink_threshold. |
hangup | reason | When the PeerConnection closes, from either side. |
detached | When the handle is gone. |
Candidates, mDNS and NAT
- The server gathers its candidates itself and puts them in the answer. Set
nat.stun_server,nat.nat_1_1_mappingornat.ice_enforce_listinjanus.jcfgto control which addresses it offers. - Browsers that hide their LAN address behind a
.localmDNS name do not connect yet. The Rust ICE agent never pairs.localremote candidates and does not learn them from incoming checks. Chrome sends real addresses when the page has camera or microphone permission, or when started with--disable-features=WebRtcHideLocalIpsWithMdns, which is what the headless Bench runner does. The checked symptom is a peer connection stuck inconnectingwhile ICE showsconnectedon the browser, because the server never starts its DTLS handshake. - If the server is not reachable on its candidate address (a different subnet, Docker without host networking), nothing connects: use
--nat-1-1or a STUN/TURN server as with the C server.
Data channels
Create the channel in the browser (pc.createDataChannel("JanusDataChannel")) before the offer, and send data: true in the plugin request where the plugin asks for it. For TextRoom the plugin makes the offer and the browser answers, and the browser still creates the channel itself, as janus.js does.
Try it in the Bench
Every plugin page of the Bench shows the local and remote video, the packet counters from getStats(), and the frames in the transcript. The EchoTest page is the shortest path to see all of the above at once.