Skip to content

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 ​

  1. Create a session and a handle, as in the walkthrough.
  2. Make an offer in the browser. Add your audio and video tracks, call createOffer() and setLocalDescription().
  3. Send it. A message to the handle with the plugin's request in body and the offer as jsep: {type: "offer", sdp}. The reply is ack.
  4. Apply the answer. The plugin's event arrives with jsep: {type: "answer", sdp}. Call setRemoteDescription() with it.
  5. Trickle candidates. Send each icecandidate the 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.
  6. Wait for webrtcup. ICE and DTLS are done and SRTP keys are set. Media flows.
  7. Watch for media, slowlink and hangup. media says audio or video started or stopped arriving; slowlink says packets are being lost; hangup says the PeerConnection closed, with a reason.
  8. End it. hangup to close the PeerConnection and keep the handle, detach to drop the handle, destroy to 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)
EventFieldsWhen
webrtcupsenderAfter ICE connected and the DTLS handshake finished.
mediatype (audio/video), receiving (bool), midWhen media starts to arrive, or stops for no_media_timer seconds.
slowlinkuplink (bool), nacksWhen the lost packets per second pass slowlink_threshold.
hangupreasonWhen the PeerConnection closes, from either side.
detachedWhen 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_mapping or nat.ice_enforce_list in janus.jcfg to control which addresses it offers.
  • Browsers that hide their LAN address behind a .local mDNS name do not connect yet. The Rust ICE agent never pairs .local remote 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 in connecting while ICE shows connected on 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-1 or 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.

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