Using ExpressTURN with mediasoup

Pass iceServers to your sendTransport / recvTransport. Done.

Where TURN goes in a mediasoup pipeline

mediasoup is an SFU. Each browser client opens a sendTransport (to push its camera/mic) and a recvTransport (to pull other producers). Both transports are RTCPeerConnections under the hood, so both need TURN configured for users on restrictive networks.

Client-side (mediasoup-client)

// npm install mediasoup-client
import { Device } from 'mediasoup-client';

const device = new Device();
await device.load({ routerRtpCapabilities });

const 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'
  }
];

const sendTransport = device.createSendTransport({
  id, iceParameters, iceCandidates, dtlsParameters,
  iceServers,
  iceTransportPolicy: 'all'
});

const recvTransport = device.createRecvTransport({
  id, iceParameters, iceCandidates, dtlsParameters,
  iceServers
});

Force-relay testing

// Validate the TURN path actually works
const sendTransport = device.createSendTransport({
  /* ... */, iceServers, iceTransportPolicy: 'relay'
});

Server-side mediasoup notes

mediasoup the server does not connect through TURN — only clients do. The mediasoup worker listens on a public IP/port pair you configure with announcedIp. Clients reach the worker either directly (UDP), via TCP fallback (mediasoup supports protocol: 'tcp' in webRtcTransport listenIps), or via TURN (relayed). All three should be available so ICE picks whichever works.

Premium: per-user shared-secret credentials

For production at scale, mint per-session credentials server-side instead of shipping a static password in your client bundle. See shared-secret examples. In a Node mediasoup signaling server:

import crypto from 'crypto';

function makeTurnCreds(secret) {
  const ttl = 3600; // 1 hour
  const username = `${Math.floor(Date.now()/1000) + ttl}:user`;
  const credential = crypto.createHmac('sha1', secret).update(username).digest('base64');
  return { username, credential };
}

// In your join-room handler, send to client alongside transport params:
socket.emit('joined', { iceServers: [{ urls: [...], ...makeTurnCreds(EXPRESSTURN_SECRET) }] });

Common pitfalls

  • Forgetting recvTransport: Easy to add iceServers to send only and have viewers fail.
  • announcedIp wrong: If your mediasoup worker can't be reached directly, ICE falls back to TURN every time and you burn bandwidth.
  • Simulcast bandwidth: 3-layer simulcast pushes 3× the bandwidth through TURN per relayed user. Plan accordingly.

Related: LiveKit recipe · Janus recipe · TURN for live streaming

Done.

Get Free TURN Credentials