Browser client
One peer connection per room. The browser fixes its media shape up front,
sends a single offer, and never renegotiates. examples/client.html
in the repository is the reference.
Build the offer
// no iceServers: the answer carries the SFU's own address
const pc = new RTCPeerConnection();
// what you send: the mic now, a video sender kept for the camera later
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
pc.addTransceiver(mic.getAudioTracks()[0], { direction: 'sendonly' });
const camTx = pc.addTransceiver('video', { direction: 'sendonly' });
// what you receive: these counts must not exceed audio_slots / video_slots
for (let i = 0; i < 8; i++) pc.addTransceiver('audio', { direction: 'recvonly' });
for (let i = 0; i < 4; i++) pc.addTransceiver('video', { direction: 'recvonly' });
await pc.setLocalDescription(await pc.createOffer());
const res = await fetch(SFU + '/v1/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, sdp_offer: pc.localDescription.sdp, peers }),
});
const { session_id, sdp_answer } = await res.json();
await pc.setRemoteDescription({ type: 'answer', sdp: sdp_answer });Receive
Every slot fires ontrack once, at setup. Tracks then go live
or silent as the SFU binds speakers to them. Attach audio straight away
and let it idle; show a video tile on unmute and hide it on
mute.
pc.ontrack = (ev) => {
if (ev.track.kind === 'audio') {
const a = new Audio();
a.autoplay = true;
a.srcObject = new MediaStream([ev.track]);
} else {
ev.track.onunmute = () => showTile(ev.track);
ev.track.onmute = () => hideTile(ev.track);
}
};
To put a name on a tile, ask POST /v1/sessions/:id/slots
which user is on each mid; see the API.
Turn the camera on later
No renegotiation: give the reserved sender a track.
const cam = await navigator.mediaDevices.getUserMedia({ video: { width: 640 } });
await camTx.sender.replaceTrack(cam.getVideoTracks()[0]);Cap your bitrate
The SFU does no congestion control, so the browser must not send more than listeners can take.
const p = camTx.sender.getParameters();
p.encodings = [{ maxBitrate: 600_000 }]; // about 600 kbps for video
await camTx.sender.setParameters(p);Leave
- Cleanly:
DELETE /v1/sessions/:id, thenpc.close(). - Tab closed or crashed: nothing to do. ICE notices within seconds and the server reaps the session.
- Reconnect: post a fresh offer. The old session for the same user and room is replaced.