Detecting a Frozen or Static Video Feed Automatically
9 min read · updated August 11, 2026
The RTSP session is established. The player is not buffering. FFmpeg prints an increasing frame= counter and no errors. The timestamps advance normally. And the picture on screen has been the same image for ten minutes. Nothing in the stack considers this an error, because at every layer it is not one.
The symptom, and why nothing errors
A frozen feed is invisible to every health check that asks whether data is flowing, because data is flowing. A video codec has an efficient representation for “this frame is the same as the last one” — a predicted frame consisting almost entirely of skipped macroblocks with zero motion and zero residual — so an encoder handed the same image sixty times a second emits sixty perfectly valid frames. The container is well-formed, the timestamps are monotonic, the decoder is happy.
There are several distinct causes and they matter later when you try to fix it rather than merely detect it.
- Camera or firmware hang. The sensor pipeline stops producing new images but the network stack keeps serving the last one it has. Common on cheap IP cameras after long uptime.
- Capture device repeating. A capture card or SDI receiver that loses input often holds the last valid frame rather than signalling loss, which is a deliberate design choice that makes this failure silent.
- Encoder or transcoder stall. The input stopped but the encoding pipeline continues emitting from its last buffer, or a frame-rate converter duplicates to maintain output cadence.
- Upstream source freeze. The freeze happened before you and everything on your side is faithfully relaying it.
- A genuinely still scene. A locked-off camera on an empty corridor. Not a fault, and it is why this problem is harder than it looks.
One cheap signal separates the first four from the fifth before you decode anything: bitrate. A frozen 1080p H.264 feed at a nominal 5 Mbit/s collapses to a few kilobits per second, because all-skip frames are almost free to encode. A genuinely still but live scene does not collapse nearly as far, since sensor noise is real changing content that the encoder must spend bits on. A bitrate alarm at a small fraction of nominal is the least expensive freeze detector you can build, and it needs no decoding at all.
Sensor noise is the discriminator
The naive detector compares consecutive decoded frames and calls it frozen when the difference is small. That gets the test almost right and the threshold badly wrong, and the reason is worth being precise about.
A real camera pointed at a perfectly static scene does not produce identical frames. Photon arrival is a Poisson process, so shot noise differs frame to frame; sensor read noise and dark current add more; amplifier gain in low light multiplies all of it. The mean absolute difference between consecutive frames from a live camera on a static scene is small but reliably nonzero, and it rises sharply in low light as gain increases.
A duplicated frame has none of that. It is bit-identical before encoding, so after decoding the difference is exactly zero, or so close to zero that it is below anything noise can produce. That is the real test:
NOT this: mean_abs_diff(frame_t, frame_t-1) < small_threshold
BUT this: mean_abs_diff(frame_t, frame_t-1) is at or below the
NOISE FLOOR of this camera, for a sustained duration
live camera, bright static scene MAD ~ 0.3 - 1.5 (8-bit levels)
live camera, dark high-gain scene MAD ~ 2 - 8
duplicated frame MAD = 0.0 exactly
duplicated frame, re-encoded MAD ~ 0 to 0.2 (quantisation only)
the useful separation is between "exactly zero, or nearly" and
"small but alive", not between "small" and "large".The re-encoded case is why the threshold is not literally zero. If the frozen stream has been through a lossy transcode after freezing, two decoded copies of the same source frame may differ in the lowest bits from independent quantisation decisions. A threshold expressed in decibels below full scale rather than as an absolute level handles this cleanly across bit depths.
The check, two ways
FFmpeg implements exactly this and it is the fastest route to a working detector. The freezedetect filter, documented in the FFmpeg filters reference, takes a noise tolerance n and a minimum duration d, and sets frame metadata when the picture has not changed significantly for at least that long.
# monitor a live feed, log freezes, decode nothing to disk ffmpeg -hide_banner -i rtsp://camera.local/stream1 \ -vf freezedetect=n=-60dB:d=5 -map 0:v -f null - 2>&1 \ | grep -E 'freeze_(start|duration|end)' # metadata keys it sets on the frame: # lavfi.freezedetect.freeze_start timestamp the freeze began # lavfi.freezedetect.freeze_duration how long it lasted # lavfi.freezedetect.freeze_end timestamp it ended # n is the noise tolerance: -60dB is strict, -40dB is permissive. # d is the minimum duration before it reports. Below about 2 s you # will fire on ordinary still moments in real footage.
When you need the check inside an application rather than at the command line, the same logic is a short loop: decode to greyscale, downscale — a 64×64 reduction is ample and makes the comparison nearly free — take the mean absolute difference against the previous frame, and maintain a counter of consecutive frames below the noise floor. Fire when that counter exceeds the frame rate multiplied by your duration threshold, and clear it on any frame above the floor.
Two implementation details decide whether this works in production. Downscale before differencing, not after: it removes the isolated hot pixels that would otherwise keep the difference nonzero on a frozen feed with a noisy sensor. And compare in the decoded domain rather than on compressed frame sizes if you want reliability — frame size is a good screening signal but a variable-bitrate encoder produces small frames for plenty of live content too.
Finding which hop froze
Detection tells you there is a freeze. Fixing it needs to know where it entered, and the answer is almost always visible in bitrate at each hop.
- Check at the camera directly. Pull the RTSP or device stream at source, bypassing your ingest entirely, and run the same freeze check. If it is frozen here, everything downstream is faithfully relaying a broken source and no amount of pipeline work helps.
- Compare bitrate at ingest against nominal. If the camera is fine but ingest shows a collapsed bitrate, the loss is in transport or in the sending device’s encoder. If ingest shows full bitrate, the source is sending real content and the freeze is yours.
- Check the transcoder input and output separately. Full bitrate in and collapsed bitrate out localises the fault to the transcoder — commonly a stalled pipeline, or a frame-rate converter duplicating because its input stopped delivering.
- Check the packager or CDN output. A freeze that appears only at the delivered stream and not at any earlier hop is usually a stale segment being served, which is a cache problem rather than a video problem and will show as a segment sequence that stops advancing.
- Restart the narrowest component that reproduces it. The whole point of the previous four steps is to avoid restarting the entire chain and losing the evidence. Capture a short sample of the frozen stream first — a frozen feed is a few kilobits per second, so a minute of it costs nothing to keep and is the only artefact you will have afterwards.
Pair this with a timestamp check while you are there. A feed that is frozen and whose presentation timestamps have stopped advancing is a stalled pipeline; a feed that is frozen with timestamps advancing normally is a duplicating source. The two look identical on screen and have entirely different causes, and it is the same distinction that makes a latency budget go wrong in one case and not the other.
The false positives you will get
A freeze detector deployed across a fleet will alarm on legitimate content, and knowing which cases in advance is the difference between a useful alert and one that gets muted.
- Slates, colour bars and holding cards. A channel showing a static graphic between programmes is genuinely frozen by any pixel test. Either whitelist those sources or combine with the audio check: a holding card usually has music under it, a frozen feed usually has silence or frozen audio too.
- Aggressive temporal denoising. Strong noise reduction on a static scene removes exactly the sensor noise the detector relies on, pushing a live feed to near-zero difference. Raise the duration threshold on those sources, or test upstream of the denoiser.
- Screen shares and presentations. A slide left up for four minutes is indistinguishable from a freeze and is completely normal. This is a content class to exclude, not a threshold to tune.
- Very low frame rate sources. A camera configured at 1 fps sends genuinely repeated frames between captures on some pipelines. Check the configured rate before believing the alarm.
- Black frames. A feed that has gone entirely black is also frozen, and will fire both this detector and a black-frame test. FFmpeg’s
blackdetectis the companion filter, and treating “frozen and black” as its own alert class avoids two pages for one fault.
The general defence is a duration threshold long enough to clear ordinary stillness — five to ten seconds for surveillance, longer for presentation content — combined with a second signal, whether that is bitrate collapse, audio silence, or timestamp behaviour. A single-signal detector on a large fleet produces enough noise that people stop reading it, at which point it detects nothing at all.