Prompt
How do I fix WebRTC calls that fail on some browsers
Latest observation
WebRTC failures on some browsers but not others are usually caused by one of a few common issues: codec mismatch, autoplay policy, missing STUN/TURN setup, CORS/permissions, or browser-specific quirks. Here’s a practical way to debug and fix them.
1) Check the browser console and WebRTC internals first
Different browsers expose useful diagnostics:
- Chrome / Edge:
chrome://webrtc-internals - Firefox:
about:webrtc - Safari: Web Inspector → Console + Media logs
Look for:
- ICE state stuck on
new,checking, orfailed getUserMediapermission errorssetLocalDescription/setRemoteDescriptionfailuresaddIceCandidateerrors- codec negotiation issues
- DTLS/SRTP handshake failures
2) Make sure your signaling is correct
WebRTC itself does not connect peers automatically; your signaling server must reliably exchange:
- offer
- answer
- ICE candidates
Common mistakes:
- sending ICE candidates before remote description is set
- not handling glare / simultaneous offers
- dropping candidates during reconnects
- mismatched SDP due to race conditions
A safe pattern:
- set remote description first
- queue incoming ICE candidates until remote description is set
- only then call
addIceCandidate
3) Use STUN and TURN properly
A lot of “works on my browser” issues are actually NAT traversal problems.
- STUN helps peers discover public-facing addresses.
- TURN relays media when direct connection fails.
If some browsers work only on the same network or only in certain environments, TURN is often missing or misconfigured.
Checklist:
- include at least one STUN server
- deploy a TURN server for production
- ensure TURN supports UDP and TCP
- ideally support TLS on 443 for restrictive networks
Example:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{
urls: [
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp",
"turns:turn.example.com:443?transport=tcp"
],
username: "user",
credential: "pass"
}
]
});
4) Verify codec compatibility
Some browsers prefer or require different codecs.
Typical issues:
- Safari support differs from Chrome/Firefox
- H.264 vs VP8/VP9/AV1 negotiation issues
- audio codec mismatch (Opus usually safest)
To reduce problems:
- prefer Opus for audio
- support H.264 if Safari/iOS is involved
- don’t assume VP8/VP9 will work everywhere
- avoid SDP munging unless necessary
If you modify SDP manually, that can break interoperability.
5) Watch out for autoplay restrictions
A common “call connects but no sound/video plays” issue.
Browsers may block autoplay unless:
- user has interacted with the page
- media is muted initially
video.play()is called in a user gesture context
Fix:
- start remote video muted if needed
- call
element.play()after user action - handle promise rejections from
play()
Example:
remoteVideo.play().catch(err => {
console.warn("Autoplay blocked:", err);
});
6) Make sure you handle tracks correctly
Modern WebRTC uses addTrack and ontrack, not older addStream APIs.
Use:
stream.getTracks().forEach(track => pc.addTrack(track, stream));
pc.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0];
};
Browser differences often appear if one side still uses legacy APIs.
7) Check secure context requirements
Most WebRTC features require HTTPS, except localhost.
If some browsers fail in production:
- ensure the site is served over HTTPS
- avoid mixed content
- make sure permissions are granted on a secure origin
8) Fix ICE candidate handling across browsers
Browsers may emit candidates at different times, and some send an empty candidate to signal end-of-candidates.
Make sure you:
- handle
null/ empty ICE candidates - don’t treat them as errors
- serialize and forward candidates correctly
Example:
pc.onicecandidate = (event) => {
if (event.candidate) {
sendToPeer({ type: "ice-candidate", candidate: event.candidate });
}
};
9) Handle Safari-specific quirks
Safari is often the browser that “fails on some calls.”
Common Safari issues:
- stricter autoplay rules
- different handling of codecs
- issues with bundled ICE candidates in some versions
- camera/mic permission behavior
- speaker selection limitations
Try:
- updating Safari/iOS
- including H.264 support
- testing with TURN over TCP/TLS
- avoiding assumptions about
getStats()fields being identical across browsers
10) Check transceiver and SDP direction mismatches
If you use transceivers, make sure directions align:
sendrecvsendonlyrecvonlyinactive
A mismatch can cause one browser to think it should send while the other expects receive-only.
11) Inspect getUserMedia constraints
Some browsers are pickier about constraints.
If this fails:
navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 1920 }, height: { ideal: 1080 } },
audio: true
});
Try loosening constraints:
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
Then gradually add constraints back.
12) Use stats to narrow down the failure
getStats() can tell you whether the issue is:
- network connectivity
- packet loss
- codec mismatch
- RTP not flowing
- DTLS not established
Look for:
candidate-pairselectedbytesSent/bytesReceivedframesDecodedpacketsLostcurrentRoundTripTime
If selectedCandidatePair exists but media counters stay at zero, it’s often signaling/SDP/track wiring rather than network.
13) Test with a minimal example
Reduce your app to:
- one offer
- one answer
- one audio track
- one video track
- basic STUN/TURN
- no custom SDP munging
If the minimal version works, reintroduce features one by one.
Quick checklist
- HTTPS enabled
- STUN + TURN configured
- Remote description set before ICE candidates
-
ontrackused correctly - Browser autoplay handled
- Codec support includes Safari/iOS needs
- No legacy API mismatch
- Permissions granted
- Check
webrtc-internals/about:webrtc
If you want, I can help you debug a specific browser issue if you paste:
- the browser(s) that fail,
- the error from console, and
- your RTCPeerConnection config / signaling flow.