Using ExpressTURN with Janus

Set ICE servers either in the Janus server config or per-client.

Server-side: janus.jcfg

Janus's WebRTC settings live in conf/janus.jcfg. Add a turn_* block under the nat section. Janus uses these to issue ICE candidates server-side, but most teams prefer to set ICE servers client-side so each browser learns its own credentials.

nat: {
  stun_server = "stun.expressturn.com"
  stun_port = 3478
  full_trickle = true

  turn_server = "relay1.expressturn.com"
  turn_port = 3478
  turn_type = "udp"
  turn_user = "YOUR_EXPRESSTURN_USERNAME"
  turn_pwd = "YOUR_EXPRESSTURN_PASSWORD"

  ice_lite = false
  ice_tcp = true
}

Client-side (janus.js — recommended)

// Pass iceServers when attaching to a Janus plugin
Janus.init({
  debug: 'all',
  callback: () => {
    const janus = new Janus({
      server: 'wss://your-janus-server/ws',
      iceServers: [
        { urls: 'stun:stun.expressturn.com:3478' },
        {
          urls: [
            'turn:relay1.expressturn.com:3478?transport=udp',
            'turn:relay1.expressturn.com:3478?transport=tcp',
            'turns:relay1.expressturn.com:443?transport=tcp'
          ],
          username: 'YOUR_EXPRESSTURN_USERNAME',
          credential: 'YOUR_EXPRESSTURN_PASSWORD'
        }
      ],
      success: () => { /* attach to plugin */ }
    });
  }
});

Per-handle override

Some Janus plugins (VideoRoom, Streaming, AudioBridge) can take iceServers per-handle when you call janus.attach(). Useful when you want different relay configs for publishers vs subscribers.

Common pitfalls

  • Janus configured for ICE-Lite: If ice_lite = true, Janus will not use TURN itself. The browser side still can.
  • Local-only NAT detection: Set nat_1_1_mapping in janus.jcfg if Janus runs behind a 1:1 NAT (cloud VM with public IP).
  • HTTPS / WSS required: Modern browsers reject WebRTC from non-HTTPS pages. Make sure your Janus signaling is WSS.

Related: mediasoup recipe · LiveKit recipe · TURN for streaming

Done.

Get Free TURN Credentials