Browser / Web Relay Issues

Use this guide when the browser listener at /listen is silent or fails to connect.

The browser listener path:

browser → HTTPS/WSS → mixer_web (8889) → proxy → shiloh-web-relay (8890)
                                                         │
                                            WebRTC WHEP handshake
                                                         │
                                            UDP media (ephemeral port)

Branch 1: WHEP endpoint returns 404

Symptom: Browser console shows POST /whep/main 404 or the /listen page loads but immediately errors.

Check that shiloh-web-relay is running:

systemctl --user status shiloh-web-relay
ss -lntp | grep 8890

The second command should show shiloh-web-relay listening on 127.0.0.1:8890.

Check that the proxy is working:

curl -sv http://localhost:8889/whep/main -X POST 2>&1 | head -20

Port 8889 is mixer_web which proxies /whep/* to shiloh-web-relay on
127.0.0.1:8890. A 405 or 400 (not 404 or connection refused) means the
proxy chain is up and the endpoint exists.

If shiloh-web-relay is down:

systemctl --user start shiloh-web-relay
journalctl --user -u shiloh-web-relay -n 30

Common startup failures: port 8890 already in use (check ss), or mixer_web is proxying to the wrong address (should be 127.0.0.1:8890).


Branch 2: Connection timeout / audio never starts

Symptom: Browser spinner runs indefinitely, or DevTools shows a PeerConnection that never reaches connected.

Check ICE connectivity:

Open chrome://webrtc-internals/ (Chrome) or about:webrtc (Firefox) while on the /listen page. Look at the active PeerConnection. The candidatePair section shows whether ICE succeeded.

  • inbound-rtp with packetsReceived climbing ~50/s: audio is flowing normally.
  • bytesReceived stuck at 0 after ICE connected: DTLS or codec mismatch.
  • ICE state stuck at checking: NAT/firewall issue.

Common causes:

Symptom Likely cause Fix
No ICE candidates gathered Browser blocked WebRTC (non-HTTPS) See iOS/Safari section below
ICE checks all fail Firewall blocking UDP ephemeral ports Open UDP 10000–65535 from server to client, or add a TURN server
ICE connects but no audio Wrong SSRC / codec negotiation Check shiloh-web-relay logs

Check shiloh-web-relay logs for the PeerConnection state:

journalctl --user -u shiloh-web-relay | grep "PC state"

connected = ICE worked. failed = NAT problem, needs TURN.


Branch 3: Audio delay > 500 ms

The browser listener uses WebRTC which targets 100–500 ms latency. Delay beyond 500 ms is usually caused by the browser’s jitter buffer growing due to inconsistent packet delivery.

Check network jitter:

ping -i 0.01 -c 500 <MIXER_HOST> | tail -3

High mdev (> 10 ms) causes the jitter buffer to expand.

WebRTC jitter buffer tuning (Chrome):

Chrome exposes jitter buffer delay in chrome://webrtc-internals/ under Stats graphs → jitterBufferDelay. There is no direct user-facing knob — improving network stability is the fix.

Reduce upstream source latency:

The relay fan-out adds one JACK buffer period of latency before audio reaches the web relay. With a 1024-frame JACK buffer at 48 kHz this is ~21 ms. Reducing the JACK buffer (at the cost of more xrun risk) reduces this floor. See Performance Tuning.


Branch 4: iOS / Safari — HTTPS required

Symptom: Safari on iOS or macOS opens /listen but WebRTC never starts, or the browser reports a security error.

Root cause: Safari requires HTTPS for WebRTC (getUserMedia and WHEP both require a secure context). HTTP connections are rejected by the browser before any ICE negotiation starts.

Fix — put mixer_web behind a TLS reverse proxy:

Option A — nginx with Let’s Encrypt:

server {
    listen 443 ssl;
    server_name mixer.example.com;

    ssl_certificate     /etc/letsencrypt/live/mixer.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mixer.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8889;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Option B — Caddy (auto-HTTPS):

mixer.example.com {
    reverse_proxy localhost:8889
}

After adding TLS, update PHX_HOST in the mixer_web.service environment to the public HTTPS hostname so LiveView websockets use wss://.

Local testing with self-signed cert:

iOS rejects self-signed certificates even when manually trusted for HTTPS — you must use a valid CA-signed cert for WebRTC on Safari/iOS. Use a domain with Let’s Encrypt, or Tailscale’s HTTPS certificates for private networks.