soli-sfu

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, then pc.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.