zam@zsbahtiar:~$

cat ~/notes/janus-webrtc-sfu.md

janus for 1:1 webrtc calls: why i put an sfu in the middle

[id]

janus for 1:1 webrtc calls: why i put an sfu in the middle

A 1:1 video call can connect both clients directly. Mine still goes through a server.

The reason is not video quality or room size. The system needs server-side recording, centralized call control, and a reliable media path across difficult networks.

janus handles that media path. My backend decides what a call is allowed to do. Janus moves and records the packets.

That separation works well, but it also means owning TURN, UDP networking, a recording pipeline, and an egress bill.

What exactly is janus?

Janus is a general-purpose WebRTC server. It is not a complete calling product and does not own application state. Instead, it exposes plugins for different media patterns:

  • videoroom for multiparty audio and video, which is the plugin I use
  • streaming for broadcasting one source to many viewers
  • SIP, NoSIP, textroom, recordplay, and several others

Applications use HTTP or WebSocket for signaling and a separate admin API for management. The media travels as RTP over UDP.

The useful mental model is a programmable media pipe.

My backend decides who can call, creates the room, and owns the call lifecycle. Janus receives streams, forwards them, and writes recordings.

Why put a server between two people?

For two participants, plain peer-to-peer is the obvious baseline. It has the shortest media path and almost no server egress.

The three common topologies make the trade-off clearer:

three WebRTC topologies: mesh connects every participant directly, an SFU forwards streams, and an MCU mixes streams

  • mesh / P2P connects participants directly. It is efficient for 1:1, but there is no central media path for server-side recording or call enforcement.
  • MCU decodes every stream, mixes them, re-encodes the result, and sends it back. Client bandwidth is small, but server CPU cost is high. That is unnecessary for two people.
  • SFU receives each stream and forwards it without decoding or re-encoding. This is the janus videoroom model.

I chose an SFU because the server had to remain in the media path. Recording and call control made that a system requirement, not a scaling optimization.

Two planes, one call

A call uses separate signaling and media planes:

architecture: the client talks to nginx and a Go signaling API for control, media flows as RTP directly to janus or through coturn, and janus records to encrypted object storage

The signaling plane carries control traffic. Clients connect to my backend over HTTPS or WSS. The backend authorizes the call, creates a janus room, joins both participants, relays SDP offers and answers, and exchanges ICE candidates.

Rooms are created per call. The backend owns state transitions, notifications, and the rest of the lifecycle. Janus does not need to know any product-level meaning.

The media plane carries RTP over UDP. Once signaling finishes, most clients send audio and video directly to janus.

Some networks use symmetric NAT or block the direct path. For those clients, coturn provides a TURN relay. Without TURN, a meaningful fraction of calls will fail to connect even when signaling succeeds.

Because every stream already passes through janus, recording uses the same media path. Janus writes each stream to disk. A worker muxes the files into MP4, encrypts the result, and uploads it to object storage.

No client-side recorder and no second media path to synchronize.

The operational parts

Host networking changes service discovery

An SFU needs a large UDP port range for RTP. That range does not fit neatly behind a container overlay network, so janus runs directly on the host network outside the orchestrator.

This changes name resolution. Janus cannot resolve service names that only exist inside the overlay network.

When call-state webhooks stopped arriving, the target was an internal service name that the host could not resolve. Pointing the webhook to 127.0.0.1 fixed the path.

ICE-lite needs explicit NAT mapping

The server has a stable public IP and does not need to gather its own candidates dynamically. Janus runs in ICE-lite mode with nat_1_1_mapping set to that external IP.

Without the mapping, clients can receive a candidate that describes the host incorrectly.

TURN cannot hide behind an HTTP proxy

The signaling API can sit behind a CDN proxy. TURN cannot.

WebRTC media uses UDP, which a normal HTTP reverse proxy does not carry. TURN DNS records need to resolve directly to the relay host.

A janus recording is not a video file

Janus records one raw file per media stream in its own format. Those files are not directly playable.

ffmpeg has to mux them into MP4 in a separate worker. It will use every available CPU core unless its threads are capped, so the worker has explicit limits to avoid starving active calls.

Recording is a pipeline, not a checkbox.

Egress is part of the architecture

Every byte that enters an SFU leaves it again. Cloud providers bill that outgoing traffic as egress.

the egress bill scales with concurrent calls, minutes, and bitrate, while a hard per-call bitrate cap controls the total

The rough cost shape is:

concurrent calls x minutes x bitrate x 2

Each participant sends a stream to janus, then janus forwards it to the other participant. A P2P call avoids this server-side traffic because the media never crosses the SFU.

The strongest cost control is a hard bitrate cap per room. I cap audio and video server-side so clients cannot negotiate beyond it. Reducing the video cap lowers egress proportionally, while the quality difference at call resolution is smaller than the bandwidth difference.

Another option is routing calls that do not need server-side media through a P2P path. That brings egress close to zero, but recording has to move to the client.

The right topology depends on which requirement keeps the server in the middle.

Would I choose janus again?

Yes, when recording and call control require a server-side media path.

Janus stays focused on WebRTC and leaves product logic in the application. The real complexity sits around it: host networking, NAT mapping, TURN, recording, and egress.

If I were starting again, I would model those five concerns before the first production call. The videoroom plugin itself is the straightforward part.


← back to blog