Remote video support that survives a technician's phone network
Remote visual assistance demos well on office Wi-Fi and fails the first time a technician climbs into a van. The failure is almost never the codec. It is the network underneath it: the phone changes address mid-session, it sits behind a carrier NAT that no direct candidate can reach, and the server does not know its own public address. This guide separates those causes by the symptom you actually see.
It works in the office and fails on cellular: no relay candidate
ICE tries to find a working path by gathering candidates. A host candidate is the
device's own address. An srflx candidate is the address a STUN server sees after
NAT, which works for cone NATs because the mapping is reusable. A relay candidate
is an address on a TURN server that both sides send to when nothing direct works.
Consumer Wi-Fi is usually cone NAT, so two peers discover each other with STUN alone and the
session connects with no infrastructure. A phone on cellular is frequently behind a
carrier-grade NAT whose mapping is not reusable. The srflx candidate is still
gathered and still looks valid, but every packet sent to it is dropped, because the mapping
was opened for a different destination. Nothing errors. ICE simply sits in
checking and eventually fails.
The symptom is specific: on Wi-Fi the connection establishes in under a second; on cellular
it never leaves checking, and the selected candidate pair never shows
relay. If you log the selected pair and it is host or
srflx when the call fails, the case for running a relay is settled. The fix is a
TURN service, reachable on the public internet, with credentials the server can hand to the
browsers.
The relay port answers, but the allocation is refused
"No video through the relay" has two causes that look identical from the browser: the UDP port is unreachable, or the port is fine and the relay declined the allocation. They need different fixes, so test them separately.
A STUN probe to the same host and port as the TURN server answers that first question. If the
probe returns a srflx candidate, the port is open and something is listening. A
relay allocation using the real credentials answers the second. A relay candidate coming back
means TURN is working and the problem is elsewhere; a refusal with the port open means a
credential or allocation-range problem, most often a 401 in the coturn log, or a relay port
range that was never opened even though the control port was.
# two probes, one browser tab
# 1. STUN on turn:host:3478 -> srflx candidate means the port is reachable
# 2. relay with credentials -> relay candidate means the allocation succeeded
# coturn: open the control port AND the relay range
listening-port=3478
min-port=49152
max-port=65535
# a cloud VM needs to be told its own public address
external-ip=203.0.113.10
The relay range is the part people forget. The control port on 3478 can answer, the credentials can be perfect, and the allocation still fails because the media ports coturn wanted to use are closed in the security group.
Video freezes when the technician walks to the van
A WebRTC transport is bound to a 5-tuple: source address, source port, destination address, destination port, protocol. When a phone moves from Wi-Fi to cellular it gets a new source address, and the 5-tuple the session was using is gone. The old candidate pair cannot be revived. On the screen this looks like a freeze with no error, because the connection object has not given up yet.
The recovery is an ICE restart, which renegotiates connectivity on the existing peer connection instead of tearing the session down. A restart is not always enough, especially after a long outage or a carrier change, so the practical chain is: try an ICE restart first, fall back to a full rejoin if the restart does not recover, and space the attempts with jittered backoff so a whole fleet of devices does not hammer the server at once when a tower comes back up.
Freeze detection that only watches the connection state will miss this. A connection can read
connected while frames have stopped arriving. Watch the frame callbacks instead:
if no new frame arrives within a timeout, treat it as frozen and start the restart chain, even
though ICE still reports connected.
ICE connects, but no frame ever decodes
On a cloud VM the network interface usually has a private address, and the public address is
attached by the provider's NAT. If the server gathers candidates from its own interface, it
advertises something like 172.16.x.x in SDP, and the remote browser dutifully
tries a private address it can never reach. ICE may even report connected on a local path
while the media never leaves the host.
The server has to be told its public address, or the relay has to be the only path offered.
In a coturn deployment that is external-ip; in a Go server it is a
-public-ip flag, or forcing the relay policy so that only relay candidates are
used. If a session connects instantly between two machines on the same office network and
fails from outside, this is the first thing to check.
The tab says connected but nothing arrives
Signaling runs over a WebSocket, and reverse proxies reap idle WebSockets. A default idle timeout around 60 seconds is common. The socket closes, the application does not notice, and the UI keeps saying connected because nothing told it otherwise.
The fix is a protocol-level ping from the server, not a JavaScript heartbeat. Background tabs throttle timers aggressively, so a client-side timer that fires every 20 seconds in the foreground can fire far less often when the tab is hidden, which is exactly when a technician has put the phone in a pocket. A server-side ping keeps the connection alive regardless of what the tab is doing.
Check order that saves time
- Log the selected candidate pair. If it is never
relayon a cellular failure, you need TURN before you debug anything else. - Probe the relay in two steps: STUN on the control port, then a real allocation. It separates "port unreachable" from "allocation refused".
- Confirm the relay media port range is open, not just the control port.
- Set the public address explicitly on any host whose interface is private.
- Trigger an ICE restart on a freeze, and fall back to a full rejoin, with jittered backoff.
- Keep the signaling socket alive from the server, not from a client timer.
None of this is exotic once it is written down, and all of it is invisible in a demo on good Wi-Fi. The failure modes only appear in the field, which is where the buyers of a field-assist product actually run it.
The Remote Field Assist Kit ships this as working source: a Go signaling server and SFU, the browser publisher and viewer, the ICE-restart and rejoin chain, the diagnostics panel with the selected candidate path, the coturn configuration and the two-probe relay test, so you can prove each layer on your own server instead of inferring it from a black box.