A few lines of config. Calls connect through corporate firewalls and carrier-grade NAT.
config optionsimple-peer accepts a config object that's forwarded to the underlying RTCPeerConnection. That's where the TURN credentials go.
// npm install simple-peer
import Peer from 'simple-peer';
const turnConfig = {
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'
}
]
};
// Initiator
const peer1 = new Peer({ initiator: true, trickle: true, config: turnConfig });
// Responder
const peer2 = new Peer({ trickle: true, config: turnConfig });
peer1.on('signal', data => peer2.signal(data));
peer2.on('signal', data => peer1.signal(data));
peer1.on('connect', () => peer1.send('hello'));
peer2.on('data', d => console.log('got:', d.toString()));
Adding a stream option doesn't change the TURN config — same iceServers array works for data, audio, and video.
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
const peer = new Peer({ initiator: true, stream, config: turnConfig });
peer.on('stream', remoteStream => document.querySelector('video').srcObject = remoteStream);
const peer = new Peer({
initiator: true,
config: { ...turnConfig, iceTransportPolicy: 'relay' }
});
trickle: false mode waits for all candidates before signaling. Works but slower; leave trickle on for production.turns:relay1.expressturn.com:443 you'll fail behind strict corporate firewalls.Related: PeerJS recipe · mediasoup recipe