The night before our iOS launch, one camera out of ten stopped streaming — but only on iPhone. Android played it fine. Frigate’s own UI played it fine. The iPhone showed a spinner, then “Stream not available.”
Here’s the part that made it a genuinely hard bug: every server-side check passed. And here’s why we’re writing it up — the cause turned out to be a combination Frigate’s own docs steer Reolink owners toward, so if you run Reolink cameras and anyone in your house carries an iPhone, this post is for you.
How we got there: fixing a real stutter
Rewind a few hours. That camera — a Reolink RLC-840A — had a different, older problem: live view visibly chugged. Froze for a beat, rushed to catch up, froze again, every two seconds like clockwork.
We diagnosed that one with ffprobe, and the recipe is worth stealing. Dump the presentation timestamps of every video packet coming off the camera and look at the gaps:
ffprobe -v error -rtsp_transport tcp -select_streams v:0 \
-show_entries packet=pts_time,flags -of csv=p=0 \
-i rtsp://user:pass@camera-ip:554/h264Preview_01_main
Run it for ~20 seconds and compute the deltas between successive timestamps. A healthy 25 fps stream is a flat 40 ms per packet, every packet. This camera’s RTSP output was anything but: a 135–521 ms burst at every keyframe (2-second GOP), then a scramble of short gaps after it. Players honor timestamps, so the picture freezes exactly as long as the burst — that was the chug, visible right there in the numbers. We probed a second RLC-840A and got the same bursts, so it’s the model, not the scene or the settings. An RLC-811A on identical encode settings wobbled only 80–100 ms — below what you can see.
The fix seemed clean. Reolink cameras also serve video over HTTP-FLV, and Frigate’s docs call that the most reliable transport for them. Probing the same camera over FLV: a perfect 40.0 ms per packet, zero anomalies. So we pointed that camera’s go2rtc source at its FLV URL, restarted Frigate, and the chug was gone. Smooth live view, flat timestamps, case closed.
Then the iPhone stopped rendering that camera entirely.
Everything checked out, nothing worked
The symptom made no sense at first. The stream Frigate republishes from that FLV source played everywhere except Apple’s video player:
- The HLS playlists were valid — segments listed, downloaded, no errors.
- The downloaded segments decoded cleanly in ffmpeg, every frame intact.
- The init segment declared the same HEVC video as our working cameras.
- Timestamps were monotonic — no discontinuities, nothing for a player to trip on.
- Android (ExoPlayer) played it. VLC played it. Frigate’s UI played it.
By every check we knew how to run, the stream was fine. The iPhone downloaded it all and silently rendered nothing.
The A/B that settled it
When every measurement says “fine” and the player says “no,” change one variable. We swapped that camera’s go2rtc source back from the FLV URL to RTSP and restarted Frigate: iPhone plays. Swap FLV back in: iPhone dead. One line in the config, perfectly reproducible in both directions. The FLV source was the bug.
The root cause, best we can establish: Reolink’s “enhanced FLV” carries HEVC codec parameters in a nonstandard way. go2rtc reads it and produces output that lenient decoders accept — ExoPlayer shrugs and plays it — but AVPlayer, Apple’s famously strict media stack, silently rejects it. No error surfaced anywhere; it just declines to render.
The strongest receipt: ffmpeg can’t handle the stream either. Asking it to simply remux the camera’s FLV output — copy the video, change the container, decode nothing — fails outright:
dimensions not set
Could not write header (incorrect codec parameters)
When ffmpeg refuses to even repackage a stream because the codec parameters don’t parse, AVPlayer refusing to play it stops being mysterious.
Where we landed on August 31
Our living-room camera went back on RTSP, chug and all. That was the honest trade at the time for Reolink’s 4K 8-series over HEVC:
- RTSP: plays everywhere, stutters every 2 seconds on the 840A’s bursty timestamps.
- HTTP-FLV: perfectly smooth, invisible to every iPhone in the house.
We listed the candidate proper fixes — transcoding, a firmware update, H.264, an upstream go2rtc change — and promised to update this post when one proved out. Two days later, one did. Not one from that list.
Update, September 2: smooth on RTSP, and it still plays on iPhone
The camera’s firmware is already the latest (v3.1.0.5562, and the camera’s own update check reports nothing newer), so that lever was gone. Transcoding a 4K HEVC stream on a $250 mini PC defeats the point. What we did instead: keep the RTSP source Apple’s player is happy with, and repair the timestamps on the way through.
go2rtc can wrap any source in ffmpeg, and ffmpeg has a bitstream filter called setts that rewrites packet timestamps without touching the video. So the camera’s go2rtc source in our Frigate config became one line:
go2rtc:
streams:
living_room:
- 'ffmpeg:rtsp://user:pass@CAMERA-IP:554/h264Preview_01_main#video=copy#audio=copy#raw=-max_interleave_delta 1 -bsf:v setts=ts=if(eq(PREV_OUTPTS\,NOPTS)\,PTS\,PREV_OUTPTS+clip(PTS-PREV_OUTPTS\,0.03/TB\,0.06/TB))'
Reading it left to right: pull the camera over RTSP, copy video and audio untouched, and for every video frame set its timestamp to the previous output timestamp plus the real gap, clamped between 30 and 60 ms. A 300 ms keyframe burst becomes 60 ms; the scramble of 10 ms catch-up frames after it becomes 30 ms; over any stretch longer than a second the output still tracks the camera’s true average rate, so audio stays in sync and nothing drifts. The -max_interleave_delta 1 matters too: without it ffmpeg holds each video packet until the next audio packet arrives, which added its own 100 to 280 ms clumps. The \, are escapes for ffmpeg’s filter parser, not for your shell — paste the line as-is, in single quotes, into config.yml. It only works from the config file: go2rtc’s API refuses any source containing a space.
Then docker restart frigate, and the numbers:
- Restream timestamps: 40.0 ms, every packet, where before they ranged from 10 ms to 470 ms. Over a 5-minute sample of 7,369 frames the output tracked wall-clock time to within 0.006 percent.
- Recordings: back to six 10.0-second segments per minute (this stream also feeds the record role, so playback of recordings gets the same smoothing).
- Cost: one ffmpeg process at about 3.5 percent of one core, roughly a frame of added latency.
- iPhone: plays, because the HEVC path from the camera through go2rtc is exactly the RTSP one that always worked. Android and Frigate’s own browser view: smooth.
One thing we learned along the way that changes how we read the earlier measurements. The bursty timestamps weren’t the whole story: watching arrival times rather than timestamps, the 840A also sends each keyframe roughly half a second late over RTSP, then rushes the small frames after it. Over FLV the same camera arrives evenly. So FLV was fixing two things at once, and the timestamp filter fixes one. Any player with a bit of buffer hides the late keyframe; with the timestamps repaired, that turned out to be every player we use.
Two practical gotchas if you try this:
- Reload Frigate’s web UI after the restart. When go2rtc drops during a restart, an open Frigate live view silently falls back to its jsmpeg mode, which is capped at the detect frame rate (5 fps on our install). We spent fifteen minutes convinced the fix had failed before noticing the access log said
jsmpeg. - Reolink cameras are sensitive to how many clients pull the main stream. With four simultaneous pulls (ours plus two diagnostic probes) this one dropped to 17 fps. Keep probes short, and don’t run several at once.
The takeaways worth keeping
- Probe timestamps, not just frames. The ffprobe packet-delta recipe above turns “it feels choppy” into a number, and localizes the fault — run it against the camera direct, the restream, and each transport, and compare.
- “Plays in ffmpeg” is not “plays in AVPlayer.” Apple’s stack is the strictest decoder your users own. If a stream must work on iOS, test on iOS — no server-side check we ran caught this.
- A silent failure needs an A/B, not more passing checks. Five green checkmarks were less informative than one config-line swap in each direction.
Both halves of this — the iOS symptom and the every-2-seconds stutter — live in our docs troubleshooting section in condensed form, so the next person who hits either finds the answer in two paragraphs instead of a debugging night.