feat(cctv): add 316 publicly broadcast webcams - #328
Open
meijbaard wants to merge 4 commits into
Open
Conversation
|
@meijbaard is attempting to deploy a commit to the Developing Osiris' projects Team on Vercel. A member of the Team first needs to authorize it. |
These cameras are open data — every one is broadcast publicly by whoever
runs it — but there is no catalogue of them, so they had to be gathered from
a public index of such cameras. That index is a list of pages, not of feeds:
no coordinates, no stream URLs, 383 different hosts. Turning it into cameras
means reading every page it points at.
Three stages, so each can be rerun on its own: harvest reads the 16 index
pages and resolves the Wowza platform to a playlist, geocode fills the place
table, emit writes the module.
The place table is committed because the captions needed a human once.
Nominatim answers 3 in 16 of the foreign captions raw ("Kransjka Gora",
"Soi Buakhao, Pattaya Live") and 9 in 9 once the place name is pulled out of
them, so those readings live in the script and their results live here —
which also means regenerating never asks Nominatim the same question twice.
AD_WALLED_HOSTS is the other list that needed a human. Most of the Wowza
family answer an ad-blocker by replacing the camera with a notice saying the
advertising is what keeps them online. It is kept as a list rather than
probed at generation time so the decision is reviewable, and so a site that
is briefly unreachable cannot quietly promote itself back to being replayed.
The harvest file itself is ignored. It is a copy of somebody else's index,
and it regenerates in a minute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Netherlands was 26 pins: the Rijkswaterstaat motorway cameras, and nothing else. No harbours, no coast, no city centres. Meanwhile a harbourmaster, a golf club and a municipality each put a camera on the open web and left it there. This adds 229 Dutch cameras and 87 abroad — open data that had no catalogue, which is the only reason it was missing. Four shapes come out of it: 209 YouTube — the operator broadcasts on their own channel. The index links to the channel's "/streams" tab, which parseYouTubeUrl reads as nothing at all; rewritten to "/live" it becomes a live channel, and /api/cctv/resolve answers what is on air. No resolver code needed, and no video id baked: these streams run for months and come back under a new id after every restart. 10 HLS — the Wowza platform shared by a family of Dutch webcam sites, from the three of them that ask nothing of their visitors. streamlock.net sends Access-Control-Allow-Origin: *, so these play straight from the browser and need no entry in the proxy allow-list. 3 iframe — provider embed pages that stand on their own. 94 link — external_url alone, which puts the viewer into its external state: a way through to the camera rather than a copy of it. That last group is the one worth reading twice, because most of it is a decision rather than a limitation. Some are locked to the operator's page by a Wowza token that expires inside twenty minutes, or by a Referer check the browser cannot satisfy from here; baking either would ship a URL that is already dead when somebody clicks it. But 47 of them have a perfectly good token-free playlist, and link out anyway: their sites answer an ad-blocker by replacing the camera with a notice saying the advertising is what keeps them online. Replaying the playlist here does the very thing that notice objects to — takes the picture and leaves the page, and the advertising on it, unloaded. That those playlists carry no token is an accident of how each site was set up, not permission. Everything else is left out. 411 links are long-tail pages we cannot read — a sample of 30 gave 6 usable feeds — and 58 name a country but no place, mostly aggregator channels cycling through dozens of locations, which a single pin would have to lie about. external_url always points at the operator who runs the camera. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four keys rather than one, for the reason world-live.ts is split: a single worldwide key would make every bounds query fetch every camera. The boxes are drawn round what the arrays actually hold — the Canaries at 28N and Nordkapp at 71N are both European here — rather than round the continents. Registered in RAW_REGION_FETCHERS only. netherlands.ts is registered there and called from fetchEuropeCameras as well, and GET concatenates regions without deduplicating on id, so under ?region=all every Rijkswaterstaat camera currently lands on the map twice. That is a separate fix; this source just declines to repeat it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These assertions are about the generator, not about the upstream sites, and each one has already caught something: an id collision from two cameras sharing a caption, a Wowza URL baked with a token that expires in twenty minutes, and a "/streams" link left unrewritten — which looks fine until you notice the camera never plays. The YouTube check runs against parseYouTubeUrl itself rather than a copy of its rules, because what matters is not the URL shape but whether the viewer can read it. Writing the rules out by hand missed two legitimate forms. The hls check carries an allow-list of the three hosts whose streams are played in place, rather than a block-list of the sixteen that ask not to be. A regeneration that quietly starts replaying somebody else has to fail here first, which is the direction that fails safe. The live test asks for 200 or 404 from the HLS hosts: a Wowza 404 means the camera is not publishing right now, and plenty of these are daylight-only. The cameras are open data and the code is named for what they are, not for where the list came from. But the list is real work by real people, so the README says plainly that bekijkhet.nu is the basis for the whole layer and who has been keeping it since 2012. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
meijbaard
force-pushed
the
feat/cctv-public-webcams
branch
from
September 8, 2026 21:18
583732b to
957c42f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Netherlands was 26 pins on the map: the Rijkswaterstaat motorway cameras that
netherlands.tsfetches, and nothing else. No harbours, no coast, no city centres.Meanwhile a harbourmaster in Harlingen, a golf club on Ameland and the city of Maastricht each put a camera on the open web and left it there. This adds 229 Dutch cameras and 87 abroad — open data that had no catalogue, which is the only reason it was missing.
Where these come from
Every camera here is broadcast publicly by whoever runs it, on their own site or their own channel. What the web lacks is a catalogue of them, so they were gathered by reading a public index of such cameras — a list of pages, not of feeds, across 383 different hosts, with no coordinates and no stream URLs anywhere. The work is reading those pages and deciding honestly which ones OSIRIS can show, and how.
The index is credited in the README; see
## Acknowledgements.external_urlat the channel's/liveexternal_urlalonestream_url+stream_type: 'hls'The YouTube group needs no new resolver code. The index links to a channel's
/streamstab — a list of past broadcasts, whichparseYouTubeUrlreads as nothing at all, so those cameras would silently never play. Rewritten to/livethey becomelive-channel, and/api/cctv/resolvealready answers what is on air. Nothing is baked: these streams run for months and come back under a new video id after every restart, which is exactly the case that route's header describes.The HLS group needs no proxy entry. These are one Wowza platform shared by a family of Dutch webcam sites, and
streamlock.netsendsAccess-Control-Allow-Origin: *, so they play straight from the browser.ALLOWED_HOSTSinproxy/route.tsis untouched.Why 94 carry a link and no stream
Some of them have to: the playlist is locked to the operator's page by a Wowza token that expires inside twenty minutes, or by a
Referercheck the browser cannot satisfy from here. Baking either would ship a URL that is already dead when somebody clicks it.But 47 of them have a perfectly good token-free playlist and link out anyway. Sixteen of the nineteen sites in that Wowza family answer an ad-blocker by replacing the camera with a notice — the advertising is what keeps them online, please switch the blocker off. Replaying their playlist here does the very thing that notice objects to: takes the picture and leaves the page, and the advertising on it, unloaded. That those playlists happen to carry no token is an accident of how each site was set up, not permission.
So only the three sites that ask nothing of their visitors are played in place, and the rest are pins with a way through to the operator.
AD_WALLED_HOSTSin the generator records which sites and why; the test guards it from the other side, with an allow-list of the three rather than a block-list of the sixteen, so a regeneration that quietly starts replaying somebody else has to fail a test first.external_urlalways points at the operator who runs the camera.What is left out
netherlands.tsalready fetches those live.Coordinates
Resolved through Nominatim, country-filtered, cached in
scratch/public-webcams-places.jsonso regenerating never asks twice. Dutch cameras are looked up on their caption first —Sint Servaasbrug, Maastrichtputs the pin on the bridge — and fall back to the village centre.The foreign captions needed reading once by hand: Nominatim answers 3 in 16 of them raw and 9 in 9 given the place name, so
Kransjka Gora→Kranjska GoraandSoi Buakhao, Pattaya Live→Pattayalive in the generator, where they can be reviewed.Registration
Four keys in
RAW_REGION_FETCHERS, split the wayworld-live.tsis — one worldwide key would make every bounds query fetch every camera. Bounding boxes are drawn round what the arrays actually hold, so the Canaries at 28N and Nordkapp at 71N are both European here.Registered in
RAW_REGION_FETCHERSonly. Worth flagging while you are in here:netherlandsis registered there and called fromfetchEuropeCameras, andGETconcatenates regions without deduplicating onid— so every Rijkswaterstaat camera currently lands on the map twice. Measured:?region=netherlands,europereturnsRijkswaterstaat: 52, with all 26 ids appearing twice. Sent separately as #329 rather than mixed in here.Verifying
Checked against a running instance: a viewport query at Harlingen picks up
public-webcams-nlandpublic-webcams-europe,/api/cctv/resolveturns a channel link into a playable embed, and the map'scctvsource carries all 316 with their coordinates and stream types.npm run lintruns out of heap on this repo before and after this branch, so I could not use it as a gate.eslinton the changed files reports nothing new —route.tshas the same 29 pre-existing findings onmaster.🤖 Generated with Claude Code