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

  1. Log the selected candidate pair. If it is never relay on a cellular failure, you need TURN before you debug anything else.
  2. Probe the relay in two steps: STUN on the control port, then a real allocation. It separates "port unreachable" from "allocation refused".
  3. Confirm the relay media port range is open, not just the control port.
  4. Set the public address explicitly on any host whose interface is private.
  5. Trigger an ICE restart on a freeze, and fall back to a full rejoin, with jittered backoff.
  6. 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.

Next: the Pion ReplaceTrack guide -> All guides