feat(cctv): add Quebec, Ottawa, and World feeds

This commit is contained in:
InoukT committed 2026-11-02 22:57:22 -05:00
1 parent 72ceeb2fce
commit fa4ce4b2a5
59 files changed
+40815 -88

No files matched your search

+24 -9
View File
@@ -1,8 +1,9 @@
# CCTV integration workflow
Use this workflow when adding a camera provider or repairing an existing feed.
The CCTV layer gets its catalog and media through the local server; keep
provider-specific network access and URL policy on the server side.
The CCTV catalog comes through the local server; media normally does too. Keep
provider-specific URL policy on the server, including any validated browser-only
media URL permitted by the exception below.
## 1. Check the source before coding
@@ -90,11 +91,23 @@ Catalog and media loaders must also:
- avoid persistent frame storage unless the provider's terms explicitly allow
it and the design has been reviewed.
For live media, use the existing media proxy and supported feed type (`mjpeg`,
`mp4`, `webm`, or `hls`) rather than adding a direct browser stream. Validate
the upstream content type and bound or cancel streams according to the
existing proxy behavior. Read [SECURITY.md](../SECURITY.md) before changing
URL validation, redirects, or proxy behavior.
For live media, prefer the existing proxy and supported feed type (`mjpeg`,
`mp4`, `webm`, or `hls`). A direct browser-only video is a narrow fallback for
a source whose terms allow embedding when the browser can play it but the app
server cannot fetch it. Build its URL from a validated provider ID on the
server; never accept a browser-supplied media URL. Render it in a native DOM
`<video>` without `crossOrigin`, and do not read its pixels into canvas, WebGL,
or a Cesium texture. If a map preview is needed, a second video request may be
paused as a DOM overlay at the camera location; label it as an approximate
still and scale it with map distance. If terms disallow embedding, use a source
link instead. Read [SECURITY.md](../SECURITY.md) before changing URL
validation, redirects, or proxy behavior.
When an image provider sets a minimum refresh interval, enforce it at the
server frame boundary as well as in the client. A bounded in-memory cache and
single-flight request per camera prevent projection, panel, and re-selection
paths from multiplying upstream requests; do not persist frames unless the
provider's terms explicitly allow it.
## 4. Add focused regression coverage
@@ -109,8 +122,10 @@ and fixtures. Cover at least:
- the frame or live-media content type the provider actually serves.
Use `src/data/cctvProxy.test.mjs` for shared proxy/media behavior and
`src/data/cctvCatalogCap.test.mjs` for shared catalog allocation. Run the
focused provider and proxy tests, then the standard checks from
`src/data/cctvCatalogCap.test.mjs` for shared catalog allocation. For a
browser-only source, verify native playback, denied canvas pixel access, the
paused second-player overlay, and its cleanup separately from proxy tests. Run
the focused provider and proxy tests, then the standard checks from
[CONTRIBUTING.md](../CONTRIBUTING.md) that apply to the change. For runtime
verification, inspect `/api/cctv/sources`, `/api/cctv/health`, and the camera's
`/api/cctv/frame/:id` or `/api/cctv/stream/:id` response locally. Confirm a
+61 -7
View File
@@ -1322,9 +1322,10 @@ from the payload. There is no heading in the feed, so gizmos use the shared
id-hash fallback at low confidence.
Once enabled, the 250 cameras nearest the five borough cores load.
`CCTV_NYC_MAX_SOURCES` sets the pack cap (8–400). The catalog-wide ceiling stays at 4,000 and is shared
round-robin: no region is dropped outright, but when the packs together exceed
the ceiling, adding a pack trims cameras from the largest packs.
`CCTV_NYC_MAX_SOURCES` sets the pack cap (8–400). The default catalog limit is
7,516: 4,000 shared round-robin slots, 1,120 reserved Québec locations, and
2,396 reserved World image sources. Lower explicit `CCTV_MAX_SOURCES` values
are honored.
## City of Toronto RESCU CCTV source pack
@@ -1340,8 +1341,9 @@ cameras return a 200 "Image Unavailable" placeholder frame instead of an error.
By default all ~336 cameras load, nearest downtown first.
`CCTV_TORONTO_MAX_SOURCES` sets the pack cap (8–500) and
`CCTV_TORONTO_ENABLED=0` turns the pack off. The pack shares the 4,000
catalog-wide ceiling round-robin with the other packs.
`CCTV_TORONTO_ENABLED=0` turns the pack off. The pack shares the default
7,516-source catalog limit with the other packs; Québec and the World image
pack have reserved slots and the remaining sources use round-robin allocation.
## Lithuania CCTV source pack
@@ -5019,8 +5021,9 @@ silently demoting every later lookup for the session.
kill switch `CCTV_FINTRAFFIC_ENABLED=0`) are the fourth pack: one keyless GeoJSON station list
covering the whole country, where one _preset_ (a station's fixed view) is one camera — 806
GATHERING stations carry 2,256 in-collection presets, prioritized to 300 against seven anchors
on the main road spine. `CCTV_MAX_SOURCES` is a 4,000 catalog-wide ceiling shared round-robin
across packs, so a lower global cap thins every region instead of starving the last pack.
on the main road spine. `CCTV_MAX_SOURCES` defaults to 7,516: 4,000 shared
round-robin slots plus 3,516 reserved Québec and World image slots. Lower
caps fill reservations first; at 3,516 or less, no shared slots remain.
~1,100 cameras total, all RAW PRIOR poses, stills-first. The catalog lives for 15 minutes; once
it expires, `/api/cctv` keeps answering from it while one background refresh reloads every
pack, so only the very first load waits for the slowest pack (a live first load took about
@@ -6567,3 +6570,54 @@ the container healthy. Checks time out after 10 seconds, with a 30-second
startup grace period and three consecutive failures before reporting unhealthy.
This reports app HTTP availability, not individual provider health, and does not
configure automatic restarts.
## Québec CCTV integration
The regular CCTV source loader reads `public/quebec-cameras.json` by default
when `CCTV_QUEBEC_ENABLED` is not `0`. The pack contains 1,120 records: 680
MTQ highway cameras, 426 Montréal OSM camera locations, and 14 scenic webcams.
The default catalog limit is 7,516: it reserves all 1,120 Québec locations and
2,396 World image feeds, and keeps 4,000 slots for the existing round-robin
camera packs. A lower explicit `CCTV_MAX_SOURCES` value is still honored and
can trim the reserved packs if it is below their combined size.
Selecting an MTQ camera opens the official Québec 511 MP4 in a floating native
browser player and requests a second video for the paused map preview. The
official feed is a five-second clip refreshed by Québec 511 every 2–4 minutes,
not continuous live video; the browser player reloads every two minutes. The
map preview is a DOM overlay attached to the camera's coordinates and scales
down with camera distance. Neither video is read into a canvas or Cesium texture:
Québec 511 does not grant localhost pixel access, and its MP4 is not available
to the app's server proxy. Montréal and scenic entries are regular CCTV camera
locations with per-camera source-page links, but no direct frame URLs; those
show a location label and open the official/operator page. MTQ positions carry
CC BY 4.0 attribution, Montréal positions are attributed to OpenStreetMap
contributors under ODbL, and scenic rows retain their position accuracy and
provenance. The one-camera `/quebec511-camera-preview.html` page remains for
source review; Québec cameras now load through the normal CCTV layer.
## World camera image feeds
The default `world-cameras` pack reads only the checked-in, curated
`config/cctv_sources.world-image-cameras.json` file. It adds 166 Vegagerðin,
1,176 Oregon TripCheck, and 1,054 MLIT river cameras. Duplicate Canadian feeds
are excluded because DriveBC, Ottawa, and Québec already have app packs. NZTA's
249 rows are excluded because they were collected from a Journey Planner web
cache whose current terms prohibit automated scraping; an authorized API feed
or permission is required before adding them. The wider discovery export is
not shipped with the app.
The app validates and pins each still-image URL to its provider directory
before the server proxy uses it. Iceland and Oregon frames refresh no faster
than every five minutes; MLIT river frames refresh every ten minutes to reduce
requests. Images are not persisted; the proxy reuses frames in a bounded 32 MB
in-memory cache until their provider cadence. Set
`CCTV_WORLD_CAMERAS_ENABLED=0` to disable the pack.
Only the curated image-backed rows are bundled. Camera images are fetched from
their registered providers at request time and are not included in this data
file. The additive `/api/cctv/sources` metadata includes provider source links,
browser-only media links, refresh cadence, and location provenance where
available. When MLIT serves its recognized adjustment image, `/api/cctv/frame`
returns a God’s Eye maintenance card and `/api/cctv/health` reports the camera
as degraded with the provider-adjustment reason.