opencctv.org

An empty boardwalk at 09:30, and how its video gets to your browser

· all posts

At 09:30 this morning the fibreglass shark over the Ocean City arcade had the boardwalk to itself. Dolle's Boardwalk Cam looks north along the planks from the corner by the taffy shop: shutters down across the arcade fronts, three bins and an empty bench, and the low October sun coming straight off the Atlantic and down the boards until the far end of the frame goes white. Not a person in it. The season shut weeks ago and the camera is better for it.

That picture reaches a browser 1.53 seconds at a time, and whether it reaches one at all comes down to a division I can do on the back of an envelope.

What the player is doing while you watch

An HLS camera does not send you video. It publishes a text manifest listing the last few seconds of itself as separate files, each a second or ten long, and the player's job is to keep fetching the next one before the last one runs out. On a camera page the browser gets handed that manifest and hls.js does the fetching, with the settings written into the page itself: a buffer of up to 120 seconds, a live edge held six segments back, which on Dolle's is about 9 seconds behind the shark.

The rest of that block is failure handling, because a public camera fails all the time. Eight network errors are retried with a backoff that climbs 2, 4, 6 seconds to a ceiling of 16, and on the ninth the player is thrown away and rebuilt from scratch five seconds later. A watchdog looks at the video clock every 3 seconds, and if it has not moved for two ticks it stops the loader and starts it again at the live edge. If the manifest has not parsed within 20 seconds, same thing. iPhones skip all of it and get the native player, because Safari plays HLS itself and does it better than we would.

One setting is there for the camera rather than the viewer: video pauses after 30 minutes of an idle tab, stills after an hour. A boardwalk cam does not need to push 3 Mbps at a laptop that was shut at lunchtime.

The sum that decides whether a camera can play at all

Take any segment, divide its size in bytes by the duration the manifest gives it, and you have the rate the origin must sustain for the stream to play. Measured from this sandbox on 2026-10-28, with the segment and its EXTINF read at the same moment:

The ratio is the whole story. Above 1 the buffer fills faster than it drains and a stall is a blip you wait out. Below 1 it drains whatever you do, and no buffer size, retry count or patience will fix it, because the next ten seconds of video take longer than ten seconds to arrive. The player keeps trying, the picture stays black, and every reachability check says the camera is fine, because the manifest is there and the segments return 200.

Worth a look at the stork nest this week whilst the arithmetic is fresh: the birds left for Africa in August, so what the camera shows is an empty platform of sticks the size of a cartwheel, a lime tree behind it in full yellow, and the Biele Karpaty ridge beyond the village roofs.

What we built for the streams that lose that sum

The case that made us build it was Arlington, Virginia, where one Wowza server publishes the county's traffic cameras across three ports. Measured on 2026-07-23 by that same division: about 5.1 MB per 10 second segment, so 513 KB/s required, against 180 KB/s delivered on a single connection and 300 KB/s with as many parallel connections as the host would allow, identical from a datacentre and from a home line. There was no lower-bitrate rendition to drop to and no still-image endpoint to fall back on. The Arlington hub carries 270 live cameras today.

So we taught the site to show a frame instead of a film. A converted camera keeps its stream URL but wears it inside out: feed_url becomes hlsframe:<the original manifest> and feed_type becomes image, which means every image path in the site already works on it and the change is undone by deleting nine characters. The extractor downloads only the first ~600 KB of a segment, enough to carry a keyframe, pipes it through ffmpeg -frames:v 1 and aborts the download the moment a frame is out, about three seconds in all. A frame is only refreshed while somebody is looking, with a 30 second freshness window, so a snapshot camera nobody opens costs nothing. The health check validates one of these by the liveness of the manifest hiding inside the URL rather than by fingerprinting the JPEG, and a re-ingest re-wraps the row so a rotated stream URL is picked up instead of reverting to video.

An aside on scale: Dolle's whole 1.53 second segment is 580 KB, so on a camera like that the keyframe prefix is nearly the entire file.

The cheaper answer, found second

Two days later, on 2026-07-25, the rule changed before most of it was needed. If a throttled host serves its manifest and its segments with Access-Control-Allow-Origin: *, the browser can fetch them itself, and the site now routes that host's cameras direct instead of converting them. Our throughput number describes our route, not yours: a host that crawls from a datacentre in Virginia may be quick from a flat two miles from the camera, and when the viewer's browser goes straight to the origin our proxy is not in the path to be slow. Arlington qualified on that test, and so did mudik, and both went direct rather than to stills. For the same reason a camera already pinned direct is never probed or converted at all, because our measurement would be describing a path nobody uses.

Both hosts behind the three cameras above pass that test today. I asked each for a manifest and a segment with an Origin of https://opencctv.org and got Access-Control-Allow-Origin: * back every time.

That leaves snapshot mode as the fallback for a narrower camera than the one it was built for: an origin too slow through our proxy that also cannot be handed to the browser, because it sends no CORS headers or checks the referer. The monitor that spots those is deliberately slow to pull the trigger. It probes a few random HLS cameras a tick, confirms a suspect host against several distinct cameras on it, and converts only on five throttled samples across three different cameras, with three healthy samples enough to put the host back to video. Hosts are keyed by name with the port stripped and matched on domain boundaries, so one origin on three ports converts and reverts as one thing.

The number I want next

Of the 133,051 cameras live on the site this afternoon, as the live cameras page counts them, 18,875 are HLS, counted from our own table at 13:22 UTC today with feed_type = 'm3u8' and the row active. What I cannot tell you is how many of those sit on an origin that is both too slow through the proxy and closed to the browser, which is the population this whole mechanism exists for. I suspect it is small, in the low hundreds at most. Finding out means probing throughput on a sample of 18,875 streams belonging to other people, politely enough that it reads as a viewer rather than a scrape, and I have not worked out the shape of that yet.

Corrections to [email protected].