WebRTC connections were getting stuck in the "connecting" state with the following symptoms:
- ICE Connection State:
new => checking => connected(stuck in checking) - STUN Binding Timeout:
STUN binding request timed out(error code 701) - IPv6 Issues: Browser attempting to use IPv6 addresses that timeout
- Missing ICE Candidate: Local candidate showing as "(not set)"
The WebRTC client code was missing the onicecandidate event handler. This meant:
- ICE candidates generated by the browser were not being logged or sent
- The connection couldn't properly establish NAT traversal
- No visibility into ICE candidate gathering process
- Only using a single STUN server (stun.l.google.com:19302)
- No redundancy if the primary STUN server fails or times out
- IPv6 STUN requests timing out without fallback
- No visibility into ICE gathering state changes
- No timeout detection for stuck connections
- Limited debugging information for connection failures
- Incorrect RTCPeerConnection configuration properties
- No connection timeout handling
- Missing detailed logging at critical connection stages
// Handle ICE candidates - critical for NAT traversal
pc.onicecandidate = (event) => {
if (event.candidate) {
console.log(`ICE candidate for stream ${stream.name}:`, event.candidate.candidate);
// Candidates are included in SDP for go2rtc
} else {
console.log(`ICE gathering complete for stream ${stream.name}`);
}
};Why this matters: ICE candidates are essential for establishing peer-to-peer connections through NAT/firewalls. Without proper handling, connections will fail.
pc.onicegatheringstatechange = () => {
console.log(`ICE gathering state for stream ${stream.name}: ${pc.iceGatheringState}`);
};Why this matters: Provides visibility into the ICE gathering process, helping diagnose when and why gathering fails.
const connectionTimeout = setTimeout(() => {
if (peerConnectionRef.current &&
peerConnectionRef.current.iceConnectionState !== 'connected' &&
peerConnectionRef.current.iceConnectionState !== 'completed') {
console.error(`WebRTC connection timeout for stream ${stream.name}`);
setError('Connection timeout. Check network/firewall settings.');
setIsLoading(false);
}
}, 30000); // 30 second timeoutWhy this matters: Prevents connections from hanging indefinitely, provides clear error messages to users.
Before:
const pc = new RTCPeerConnection({
iceTransports: 'all', // Wrong property name
bundlePolicy: 'balanced',
rtcpCnameCpn: 'LightNVR', // Non-standard
rtcpCnameRtp: 'LightNVR', // Non-standard
iceCandidatePoolSize: 0,
iceServers: [...]
});After:
const pc = new RTCPeerConnection({
iceTransportPolicy: 'all', // Correct property name
bundlePolicy: 'balanced',
rtcpMuxPolicy: 'require', // Standard property
iceCandidatePoolSize: 0,
iceServers: [...]
});Why this matters: Using correct WebRTC API properties ensures proper browser behavior.
Before:
ice_servers:
- urls:
- "stun:stun.l.google.com:19302"
- "stun:stun1.l.google.com:19302"After:
ice_servers:
- urls:
- "stun:stun.l.google.com:19302"
- "stun:stun1.l.google.com:19302"
- "stun:stun2.l.google.com:19302"
- "stun:stun3.l.google.com:19302"
- "stun:stun4.l.google.com:19302"Why this matters: Multiple STUN servers provide redundancy. If one times out (especially on IPv6), others can succeed.
The /api/webrtc/config endpoint now returns all STUN servers to the client:
{
"iceServers": [{
"urls": [
"stun:stun.l.google.com:19302",
"stun:stun1.l.google.com:19302",
"stun:stun2.l.google.com:19302",
"stun:stun3.l.google.com:19302",
"stun:stun4.l.google.com:19302"
]
}]
}Added detailed logging at each stage:
- Offer creation
- Local description setting
- ICE gathering state changes
- Remote description setting
- Connection state transitions
After rebuilding, restart the service:
sudo systemctl restart lightnvr
# or if running manually:
sudo killall lightnvr
sudo ./lightnvrOpen the browser console (F12) and look for:
Initializing WebRTC connection for stream <name>
Created offer for stream <name>
Set local description for stream <name>, waiting for ICE gathering...
ICE gathering state for stream <name>: gathering
ICE candidate for stream <name>: candidate:...
ICE gathering state for stream <name>: complete
Sending offer to server for stream <name>
Received answer from server for stream <name>
Set remote description for stream <name>, ICE state: checking
ICE connection state for stream <name>: connected
Track received for stream <name>
Video playing for stream <name>
Watch for these specific issues:
- STUN timeout: Should now try multiple servers
- Connection timeout: Will show clear error after 30 seconds
- ICE gathering stuck: Will be visible in logs
For WebRTC to work properly, ensure:
- UDP Port 8555 is accessible (WebRTC media)
- TCP Port 1984 is accessible (go2rtc API)
- TCP Port 8080 is accessible (lightNVR web interface)
If only using on local network, you can disable STUN in config/lightnvr.ini:
[go2rtc]
webrtc_enabled = true
webrtc_listen_port = 8555
stun_enabled = falseIf accessing from outside your network or behind complex NAT:
[go2rtc]
webrtc_enabled = true
webrtc_listen_port = 8555
stun_enabled = true
stun_server = stun.l.google.com:19302
# Optional: Specify your external IP if auto-detection fails
external_ip = YOUR.EXTERNAL.IP.ADDRESSCause: STUN server unreachable (firewall, IPv6 issues, network problems)
Solutions:
- Check firewall allows UDP traffic to STUN servers
- Try disabling IPv6 in browser if IPv6 connectivity is poor
- Use custom STUN server on your network
- For local-only use, disable STUN entirely
Cause: ICE candidates not being exchanged properly
Solutions:
- Check browser console for ICE candidate logs
- Verify port 8555 is accessible
- Check go2rtc logs:
journalctl -u lightnvr -f - Verify go2rtc config:
cat /etc/lightnvr/go2rtc/go2rtc.yaml
Cause: Cannot establish connection within timeout period
Solutions:
- Check network connectivity
- Verify all required ports are open
- Check if go2rtc is running:
ps aux | grep go2rtc - Review go2rtc logs for errors
- Try local network first before external access
Cause: Browser prefers IPv6 but IPv6 connectivity is poor
Solutions:
- Disable IPv6 in browser (chrome://flags/#disable-ipv6)
- Configure router to prefer IPv4
- Use IPv4-only STUN servers
- Set explicit external IPv4 address in config
For the most reliable connections through restrictive NATs/firewalls, consider adding a TURN server:
[go2rtc]
ice_servers = stun:stun.l.google.com:19302,turn:your-turn-server:3478Free TURN servers:
- https://www.metered.ca/tools/openrelay/ (free tier available)
- https://numb.viagenie.ca/ (free STUN/TURN)
The code includes connection quality monitoring. Watch for:
- Packet loss
- Jitter
- Bitrate fluctuations
For detailed troubleshooting, enable debug logging in config/lightnvr.ini:
[general]
log_level = 3 ; 0=ERROR, 1=WARN, 2=INFO, 3=DEBUGweb/js/components/preact/WebRTCVideoCell.jsx- Client-side WebRTC handlingsrc/video/go2rtc/go2rtc_process.c- go2rtc configuration generationsrc/web/api_handlers_go2rtc_proxy.c- WebRTC config API endpoint
- Rebuild and restart lightNVR (already done)
- Test connections from browser console
- Monitor logs for any remaining issues
- Adjust configuration based on your network setup
- Consider TURN server if issues persist through restrictive NATs