A frozen contribution encoder is one of the nastier failures in a live webcast, because nothing in the transport layer necessarily tells you it happened. The SRT or RIST receiver keeps acknowledging packets, the origin keeps producing segments, the CDN keeps serving them, and the player keeps advancing its buffer. The viewer sees a stream that is technically live and visually a still frame. The failure is in the picture, not the pipe, so the watchdog has to look at the picture.
This article is about building that watchdog from the detection primitives FFmpeg actually documents — blackframe, freezedetect, and signalstats — and about the design decision that matters more than any single filter: layering. A single “no change = frozen” rule will page you every time a slate, a lower-third, or bars-and-tone sits on screen. The goal is an alarm that fires on a genuinely stuck encoder and stays quiet on legitimate static content.
What the failure looks like from the outside
Start with the distinction the watchdog has to make. There are three separate questions, and they are answered by different signals:
- Is the transport alive? Are packets arriving, are frames being decoded, is the timestamp advancing?
- Is the picture alive? Are consecutive decoded frames actually different from each other?
- Is the still picture expected? Is the static content a known state — bars, a slate, a holding graphic — rather than a fault?
A frozen encoder fails question 2 while passing question 1. That is exactly why transport-level monitoring alone will not catch it. The layered design in this article treats each question as a separate stage and only escalates when the answers line up.
The documented primitives
FFmpeg’s filter documentation lists freezedetect, freezeframes, blackdetect, blackframe, and signalstats among its video filters (FFmpeg Filters Documentation). Each has a documented role. None of them is a complete freeze oracle on its own.
blackframe: narrow by design
The blackframe filter is documented as detecting frames that are “(almost) black” (FFmpeg libavfilter/vf_blackframe.c). It exposes two tunable options, and the defaults are worth knowing precisely:
amount — “percentage of the pixels that have to be below the threshold for the frame to be considered black,” default 98.
threshold — “threshold below which a pixel value is considered black,” default 32.
Both come straight from the filter’s option table in the source. The filter is also flagged AVFILTER_FLAG_METADATA_ONLY, meaning it “does not modify the frame data in any way” — it attaches metadata and passes the frame through.
That is a useful, well-defined signal: a frame that is 98% of pixels below luma 32 is a near-black frame. It is not a general freeze detector. A frozen encoder showing a bright studio shot will never trip blackframe. Use it for what it says it does — detecting near-black frames, which is often what you get when a source drops to black rather than freezing on the last good frame — and do not repurpose it as your freeze alarm.
freezedetect: the picture-liveness primitive
freezedetect is the filter whose documented purpose is closest to the problem: detecting a lack of change across consecutive frames. The retrieved documentation confirms the filter exists and is listed among the video filters, but the excerpt available here does not document its internal comparison method or its option defaults. Treat that as a boundary: you can rely on freezedetect as a picture-liveness signal, but you should read the current filter documentation for your FFmpeg build before you tune it, rather than assuming a comparison method or a noise tolerance.
The conceptual point stands regardless of internals: a freeze detector compares frames and asks whether they changed. That is the right question for a frozen encoder, and it is also the question that produces false positives on legitimate static content. Which brings us to the hard part.
signalstats: corroboration, not an oracle
signalstats is listed among FFmpeg’s video filters and emits per-frame signal statistics. It is tempting to treat “the stats are identical across a window” as a freeze detector. It is not, for the same reason freezedetect is not sufficient on its own: a legitimate still — a slate, a holding graphic, a static lower-third — also produces identical statistics across a window. Identical stats tell you the picture did not change. They do not tell you whether it was supposed to change.
Where signalstats earns its place is as corroboration. If freezedetect flags a static window and the per-frame statistics are also flat across that window, you have two independent confirmations that the picture is genuinely static rather than a detector artifact. That is a stronger basis for escalation than either signal alone.
The color-bar problem
Here is the false-positive that will bite a naive watchdog first. SMPTE color bars are a television test pattern used where the NTSC standard applies, including North America (SMPTE color bars). In the standard pattern, the top two-thirds of the picture contain seven vertical bars of 75% intensity, ordered white/gray, yellow, cyan, green, magenta, red, and blue. The pattern is static by construction.
Color bars also remain in active use well beyond analog calibration. The same source notes they “remain widely used in transmission and within modern digital television facilities,” including “television and webcast equipment.” And it describes the typical practice: a network or originator “transmits SMPTE color bars together with a continuous 1,000 Hz sine wave before sending program material,” so that downstream equipment can be adjusted.
Put those facts together and the design consequence is unavoidable. A feed that opens with bars-and-tone is a feed doing exactly what it is supposed to do. A freeze detector that alarms on any static window will alarm at the head of that feed, every time. The watchdog needs an explicit expected static state — a classification that says “this still is bars, this still is a slate, this still is a holding graphic” — and it must suppress escalation while that state is active.
Note the scope of the claim. The source describes typical television practice, not a universal rule for every webcast. Your own feeds may or may not open with bars. The point is not that bars are always present; it is that if they are, your watchdog must know it, and the only way to know it is to classify the still rather than merely detect it.
Designing the layered watchdog
Four stages, in order. Each stage answers one question and passes a verdict to the next.
Stage 1 — Transport liveness
Confirm that packets are arriving and frames are being decoded, and that presentation timestamps are advancing. This stage exists to rule out the opposite failure: a dead transport that produces no frames at all. If frames stop arriving, that is a transport alarm, not a freeze alarm, and it should route differently. A frozen encoder passes this stage, which is precisely why the next stage is necessary.
Stage 2 — Picture liveness
Run freezedetect on the decoded contribution feed and corroborate with signalstats. A static window that both filters agree on is a candidate freeze. A static window that only one flags is worth logging but not paging on. This is where the debounce window lives — see the tuning section below.
Stage 3 — Content classification
When stage 2 reports a static window, classify the still before escalating. Is it bars? Is it a known slate? Is it a holding graphic the production expects? If the still matches an expected state, suppress the alarm and record the classification. If it matches nothing expected, escalate.
Classification is where a perceptual hash becomes interesting as a design direction. A hash of the still frame, compared against hashes of known slates and bar patterns, is a plausible way to answer “is this a known still?” without hard-coding pixel coordinates. But be clear about the status of that idea: the retrieved sources document detection filters, not a hash algorithm. No specific hash function, hash size, or distance threshold is documented here, and none should be presented as established practice. If you adopt hashing, you are making an engineering choice to evaluate and validate against your own content, not implementing a documented capability.
Stage 4 — Escalation with hysteresis
Only a static window that survives stages 1 through 3 becomes an alarm. The escalation policy carries the hysteresis: how long the window must persist, how many consecutive windows must agree, and how the alarm clears. This is the stage that decides whether operators trust the watchdog.
Tuning: thresholds, debounce, and what to log
The choice of filter matters less than the timing policy around it. A short static window is normal in production — slates, stills, lower-thirds, a held graphic during a sponsor read. The escalation policy should be tuned to the content rhythm of the specific production, not to a universal number. No source here supports a specific detection-time or accuracy figure, so treat any number you adopt as a local tuning decision to be validated against your own feeds.
Two practical rules:
- Debounce before you page. Require the static window to persist across a configured duration and, ideally, across more than one detection interval before escalation. A single flagged window is a log entry; a sustained, corroborated, unclassified window is an alarm.
- Log the per-frame detection metadata, not just the alarm. Record the
freezedetect verdict, the signalstats output, and the stage-3 classification for every window, alarm or not. Post-incident review needs the metadata to tell a real frozen encoder from a false positive, and the alarm alone does not carry that information.
A worked example, clearly hypothetical
Suppose a 30 fps contribution feed goes static for four seconds. Walk it through the stages:
- Transport: packets arriving, frames decoding, PTS advancing. Pass.
- Picture:
freezedetect flags the four-second window; signalstats shows flat per-frame statistics across the same window. Corroborated static window.
- Classification: the still is compared against known states. If it matches the bars pattern the feed opened with, it is classified as expected static and the alarm is suppressed. If it matches nothing expected, it proceeds.
- Escalation: the window exceeds the debounce duration and survives classification, so the watchdog pages with the metadata attached.
The example is illustrative, not a record of a real incident. Its purpose is to show where the decision actually happens: not in the filter, but in the classification and escalation stages around it.
What this design does not do
Be honest about the boundary. The retrieved sources document detection filters and the color-bar pattern; they do not document a perceptual-hash algorithm, and they do not cover transport-specific freeze behavior for SRT, RIST, WebRTC, or QUIC. A hash-based classifier is a reasonable direction to evaluate, but it is a design option, not a documented capability, and it should be validated against your own content before it gates an alarm. The layered structure — transport liveness, picture liveness, content classification, escalation with hysteresis — is what makes the watchdog trustworthy regardless of which classifier you put in stage 3.
FAQ
Why not just alarm on any static window?
Because legitimate static content is routine. Bars-and-tone at the head of a feed, slates, and holding graphics all produce static windows. The source on SMPTE bars describes bars being sent before program material as typical practice, so a watchdog that alarms on any static window will false-positive on feeds that follow it. Classification and debounce are what separate a fault from an expected state.
Can blackframe serve as my freeze detector?
No. blackframe is documented to detect frames that are “(almost) black,” with defaults of 98% of pixels below luma 32. A frozen encoder showing a bright scene will not trip it. Use it for near-black detection, not as a general freeze alarm.
Is signalstats enough on its own?
No. Identical statistics across a window tell you the picture did not change, but legitimate stills produce identical statistics too. Use signalstats to corroborate a freezedetect verdict, not as a standalone freeze oracle.
Should I use a perceptual hash for classification?
It is a plausible design direction for answering “is this a known still?” without hard-coding coordinates, but the retrieved sources do not document a specific hash algorithm, size, or threshold. Treat it as an engineering choice to evaluate and validate against your own content, not as established practice.