opencctv.org

How we check 146,113 public cameras, fetch by fetch

· updated 2026-09-30 · all posts

The camera at Capital Cir NW and Hartsfield Rd in Tallahassee refreshes every 30 seconds. At 13:09 UTC today it answered with a 42,299-byte JPEG whose md5 is 173184649261774bf7fbff1e51ec031e. That md5 is the liveness test. Not the status code, not the content type, the bytes.

Ask the same Florida site for an id that belongs to no camera and it answers HTTP 200, image/png, 15,136 bytes, md5 9c2d059e65a23b43a5b481c16918fed4. Nothing about that response says "this camera does not exist". A status check passes it. A hash does not, which is why every image feed we list gets one.

What one check does

The fingerprint pass fetches a camera's feed URL with the headers that source needs, some operators gate on Referer and some publish a templated URL that has to be resolved first, requires HTTP 200 and a content-type of image/*, then takes a plain MD5 of the body with no normalisation and stores it as that camera's last_image_hash. In the 24 hours to the nightly digest for 2026-09-26, which ran at 11:18 UTC today, the pass made 73,605 checks and 65.1% of them passed. Over the last 49 days the daily series sits between 65,018 and 89,701 checks, median 76,920. The public version of all this is the paragraph on how we source and verify cameras; the numbers come off our own health_checks and metric_daily tables at 13:05 UTC on 2026-09-27.

A third of checks failing sounds worse than it is. One failure is usually a timeout or a 429, and on its own it does nothing: a camera has to fail three of its last five checks before it comes off the index, and it returns by itself the moment its feed serves real frames again. Yesterday that arithmetic took 961 cameras off the map rather than a third of the fleet.

The reasons are mundane and worth knowing if you fetch other people's cameras for a living. Yesterday's tally is led by socket timeouts at 2,831, then 2,113 feeds that answered with a web page where a JPEG used to be, 1,718 plain 404s and 1,540 cameras where the operator is asking us to slow down with a 429. Expired certificates, 502s, and one origin returning an empty body make up the tail.

Two states, one card

Back to that 15,136-byte PNG, because the interesting part is where else it turns up. Asking Georgia DOT's 511 site for camera 18551, a real id on that system, returned the same 15,136 bytes with the same md5 at 13:08 UTC on 2026-09-27. Two state 511 sites, one vendor's "camera unavailable" card, byte for byte.

That is the detection signal, and it pays to be precise about it: one hash across many distinct feed URLs. Distinct URLs, not rows, because the same camera listed twice is a duplicate rather than a placard. A live camera shows a scene nobody else shows, so byte-identical images across hundreds of different URLs can only be one static card. The low end is noise, where two URLs share a hash because they are twin installs or two black night frames; when we swept this on 2026-06-07, the 136 hashes at five or more distinct URLs covered 99% of the cameras affected, all of them cards like the Florida one.

Confirmed cards go into a curated table of placeholder hashes, and a camera whose fingerprint matches one is deactivated on the spot without waiting out three failures in five. I like that mechanism more than most things here, because it is self-healing: a camera matches only whilst it is actually serving the card, so anything that recovers comes back on its next check and nothing needs un-deciding by hand.

What the nightly review reads

The review job writes the numbers I read in the morning, and this morning's set for 2026-09-26 is a fair sample. Fourteen shared-frame groups of 20 cameras or more, covering 2,280 cameras between them. Forty-five sources in quarantine after repeated ingest failures, 47 carrying an ingest error, 29 re-ingested automatically once their source came back. Two cameras retired outright because the agency stopped listing them, which is a different thing from a dark feed and gets its own path through the lifecycle.

A group of 20 or more is flagged for a person rather than pulled by a rule, and that is deliberate. A vendor placard and an agency whose cameras all went dark together for one night look identical in a hash table. I would rather read fourteen groups with a coffee than have a rule take 2,280 listings off the map and get a few hundred of them wrong.

The fleet, by what kind of feed it is

Counted from our cameras table at 13:05 UTC on 2026-09-27, 146,113 cameras were live across 385 sources and 160 countries, and that is what the map was serving as I wrote this. 117,917 of them are plain image feeds, which is exactly the population the md5 pass covers. The rest are video or embeds: 19,988 HLS streams, 5,926 iframes, 1,388 mjpeg and 894 mp4.

A stream gets a different check, because there is no single frame to hash. We read the playlist and require it to be a live one rather than a recording, which is why invalid-m3u8-content and a playlist carrying EXT-X-ENDLIST, 394 and 39 cameras respectively, appear in yesterday's reasons at all. A stream that plays perfectly whilst pointing at a parked scene passes that check, and for those the signal we trust is the camera's absence from the operator's own list rather than anything in the picture.

What we are building next

The Tallahassee camera taught me something whilst I was writing this. Fetched three times between 13:09 and 13:11 UTC, it returned the same 42,299 bytes each time, and it is a 30-second camera on a cache that clearly does not hurry. So "the bytes changed" is not a test you can run inside a minute. Liveness has to be a streak rather than a snapshot, and when a source answers 200 with a static image the honest method is a refetch about 75 seconds later and a byte comparison, repeated.

The next thing to build is the mirror case: one card that comes back with different bytes on every request because the origin re-encodes it. Hashing cannot see that by construction, and the group-by finds nothing, so the plan is to group by source and content length across distinct feed URLs instead. A byte size shared by hundreds of cameras in one source is the same signal one level up. If you have done near-duplicate image detection at this scale for less than a perceptual hash over a hundred thousand images, I would genuinely like to know how, because today the cards we catch are the ones lazy enough to be identical.

Corrections to [email protected].