Voice Debugging
Finding out why voice will not connect
Voice that works for you and for nobody else is nearly always ICE, which in practice means UDP reachability. Signalling runs over the same WebSocket as the rest of the app, so signalling working tells you nothing about whether media can get through.
Start here
Is the SFU actually ready?
curl -s http://localhost:5005/healthThree answers matter:
| Response | What it means |
|---|---|
{"status":"healthy",…} | The SFU is up and its outbound UDP works |
{"status":"starting","detail":"verifying UDP connectivity"}, HTTP 503 | Still probing. It sends a STUN Binding Request and retries up to eight times, backing off to five seconds |
| Connection refused | Not running, or the UDP mux port was already taken and it refused to start |
That last one is worth knowing about. The SFU binds ICE_UDP_MUX_PORT at
startup and exits if something else holds it, rather than coming up and quietly
handing out candidates on ports nobody opened. Check the logs for
failed to create ICE UDP mux on port.
Is the one UDP port open?
All media goes over ICE_UDP_MUX_PORT — 3478 unless you set it. That single
port has to be reachable from wherever your participants are, and if the SFU is
in a container it has to be published:
ports:
- "3478:3478/udp"Cloudflare Tunnel does not carry UDP. If your signalling goes through a tunnel, the media port still has to be exposed directly.
The failure that looks like a configuration problem
Users outside your network reach the server, see the channel, and sit on "connecting" forever.
The usual cause is a NAT between the SFU's UDP socket and the internet — a Docker bridge, a cloud VPC — that rewrites the source port. Look for the candidate lines in the SFU log:
ICE candidate for <peer>: type=host protocol=udp address=203.0.113.10:3478
ICE candidate for <peer>: type=srflx protocol=udp address=203.0.113.10:57599The host candidate says 3478 because that is what the SFU bound. The srflx
candidate says 57599 because that is what the outside world actually saw. When
those differ, external peers need the second one, and they only get it if STUN
is on.
So: keep DISABLE_STUN=false, which is the default. Turning STUN off is only
safe when the SFU has a direct, port-preserving path out — host networking, bare
metal, or a 1:1 NAT. Docker and most cloud networking are neither.
If the SFU is behind NAT or has several interfaces and you know the address it should be advertising:
ICE_ADVERTISE_IP=203.0.113.10Comma-separate it for a machine that should be reachable over both a LAN and the internet. Clients are given every candidate and use whichever answers first.
Reading the logs
docker logs <sfu-container> -fIn order, a working join produces: a server connection and
Server … registered room … successfully, then a client connection, then
Client … validated for room, then peer connection state changes. Where it
stops tells you which side to look at.
Two rejections have specific messages:
Join validation failed— the room is not registered, or the server credentials do not match. That is the Gryt server's side, not the client's.there are no seats left in this voice server (n/200)—MAX_PEERS. It counts every peer across every room on this SFU, not per server.
DEBUG is on unless you set it to false. VERBOSE_LOG=true adds per-packet
RTP detail and is genuinely too noisy for anything but a short capture.
From the client
Everything the client knows about the connection is in Settings → Advanced, in the latency panel. It only fills in while you are connected to voice:
| Field | What to look for |
|---|---|
| SFU endpoint | Which SFU you actually reached. Confirms LAN versus WAN path |
| ICE remote | The address media is really going to |
| ICE local | Your side of the pair |
| Candidate type | host on a LAN, srflx through NAT |
| Round-trip time | Yours, next to the other participants' |
| Jitter buffer | Highlighted above 80 ms |
If you are on the same network as the SFU and ICE remote shows a 192.168.x.x
address at near-zero latency, the LAN path won. A public address at 20 ms means
traffic is going out and back.
The same screen has toggles for two floating overlays, one for the microphone and one for video, showing live capture, codec, resolution and bitrate. Both are off by default and are separate from the latency panel. There is no third overlay for the connection itself; the latency panel is where that lives.
Checks in the browser
Microphone permission:
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
console.log("granted", stream.getAudioTracks());
stream.getTracks().forEach(t => t.stop());
})
.catch(err => console.error("denied", err));Whether the browser can form candidates at all:
const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
pc.createDataChannel("x");
pc.onicecandidate = e => e.candidate && console.log(e.candidate.candidate);
await pc.setLocalDescription(await pc.createOffer());No srflx line means STUN is not getting out from the client either, which is a
different problem from the server side of the same symptom.
What to include when asking for help
- The last ~50 lines of SFU logs, and the same from the Gryt server
- Browser console errors
ICE_UDP_MUX_PORT, whether it is published, and whether it is open in the firewallDISABLE_STUNandSTUN_SERVERS- The latency panel: SFU endpoint, ICE remote and local, candidate type
- Whether it fails for everyone or only for people outside your network