From dfe2298ad063ed4b7fe5d0f796817cf95d379568 Mon Sep 17 00:00:00 2001 From: Daniel Slay Date: Mon, 14 Sep 2026 13:57:07 +0000 Subject: [PATCH] =?UTF-8?q?cctv:=20DelDOT=20statewide=20loader=20=E2=80=94?= =?UTF-8?q?=20open=20catalog=20(tmc.deldot.gov),=20Active-only,=20rtmpt:80?= =?UTF-8?q?=20HLS,=20heading=20from=20title=20travel=20tokens;=20registere?= =?UTF-8?q?d=20in=20LIVE=5FPACKS,=20gated=20by=20CCTV=5FDELDOT=5FENABLED?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/providers/cctv/catalog.js | 6 ++ server/providers/cctv/constants.js | 8 +++ server/providers/cctv/sources.js | 93 ++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/server/providers/cctv/catalog.js b/server/providers/cctv/catalog.js index 68c5f974b..d9cea4688 100644 --- a/server/providers/cctv/catalog.js +++ b/server/providers/cctv/catalog.js @@ -16,6 +16,7 @@ import { loadTarkteeSourcesFromDatex, loadWarendorfSourcesFromCatalog, loadNswSourcesFromOpenData, + loadDelDOTSourcesFromOpenData, } from './sources.js'; /** Env kill switch: unset or anything but "0" means enabled. */ @@ -80,6 +81,11 @@ const LIVE_PACKS = [ enabled: () => envEnabled('CCTV_NSW_ENABLED'), load: loadNswSourcesFromOpenData, }, + { + name: 'deldot', + enabled: () => envEnabled('CCTV_DELDOT_ENABLED'), + load: loadDelDOTSourcesFromOpenData, + }, ]; /** * Load CCTV sources from a local JSON file (CCTV_SOURCES_FILE env or default). diff --git a/server/providers/cctv/constants.js b/server/providers/cctv/constants.js index 4c149eb5a..42b505fdc 100644 --- a/server/providers/cctv/constants.js +++ b/server/providers/cctv/constants.js @@ -220,6 +220,14 @@ export const NSW_IMAGE_USER_AGENT = * out around 120 characters. */ export const NSW_MAX_VIEW_LABEL = 140; +/** DelDOT CCTV: one keyless statewide JSON catalog; live video via RTMP-over-HTTP (rtmpt:80). */ +export const DELDOT_CCTV_URL = 'https://tmc.deldot.gov/json/videocamera.json'; +export const DEFAULT_DELDOT_MAX_SOURCES = 300; +export const DELDOT_ANCHORS = [ + { lat: 39.7459, lon: -75.5466 }, // Wilmington (New Castle) + { lat: 39.1582, lon: -75.5244 }, // Dover (Kent) + { lat: 38.6906, lon: -75.3877 }, // Georgetown (Sussex) +]; /** Camera CATALOGS change rarely; 15 min keeps multi-megabyte upstream list refetches (Austin rows.json + 4 Caltrans districts + TfL + Ontario 511) infrequent. Frames are fetched per-request and are unaffected. */ export const CCTV_SOURCE_CACHE_MS = 15 * 60 * 1000; /** Per-provider catalog-fetch timeout. Bounds the worst-case refresh so one diff --git a/server/providers/cctv/sources.js b/server/providers/cctv/sources.js index ef0ae64b8..d517443af 100644 --- a/server/providers/cctv/sources.js +++ b/server/providers/cctv/sources.js @@ -51,6 +51,9 @@ import { SYDNEY_CENTER, NSW_MAX_VIEW_LABEL, CCTV_SOURCE_FETCH_TIMEOUT_MS, + DELDOT_CCTV_URL, + DEFAULT_DELDOT_MAX_SOURCES, + DELDOT_ANCHORS, } from './constants.js'; import { toFiniteNumber, @@ -1389,3 +1392,93 @@ export async function loadNswSourcesFromOpenData() { return []; } } +export async function loadDelDOTSourcesFromOpenData() { + try { + const resp = await fetch(DELDOT_CCTV_URL, { + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(CCTV_SOURCE_FETCH_TIMEOUT_MS), + }); + if (!resp.ok) { + console.warn('[CCTV] DelDOT source download failed:', resp.status); + return []; + } + const payload = await resp.json(); + const rows = Array.isArray(payload?.videoCameras) + ? payload.videoCameras + : []; + if (!rows.length) return []; + + const cameras = []; + for (const row of rows) { + if (String(row?.status).toLowerCase() !== 'active') continue; + const lat = toFiniteNumber(row?.lat); + const lon = toFiniteNumber(row?.lon); + if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue; + // Delaware bounding box — a bad upstream coord can't place a camera out of state. + if (lat < 38.4 || lat > 39.9 || lon < -75.85 || lon > -75.0) continue; + + // Official-host pin: RTMP-over-HTTP (rtmpt:80) is the steady transport. + const streamUrl = String(row?.urls?.rtmp || ''); + if (!streamUrl.startsWith('rtmpt://video.deldot.gov:80/live/')) continue; + + const id = String(row?.id || '').trim(); + if (!id) continue; + const cameraId = `deldot-${id.toLowerCase()}`; + const title = String(row?.title || '').trim(); + + // Title carries an unambiguous travel token for ~1 in 5 cameras + // ("US 13 SB @ ..."). directionToHeading (allowBare=false) matches only + // NB/SB/EB/WB, never a bare "W"/"N" street prefix — "W NORTH ST" yields + // no heading and falls back. + const heading = directionToHeading(title); + const hasHeading = Number.isFinite(heading); + + cameras.push({ + id: cameraId, + name: title || `DelDOT ${id}`, + city: String(row?.county ? `${row.county} County` : 'Delaware'), + cityId: `deldot-${String(row?.county || 'de') + .toLowerCase() + .replace(/\s+/g, '-')}`, + provider: 'DelDOT', + lat, + lon, + headingDeg: hasHeading ? heading : fallbackHeadingFromId(cameraId), + headingConfidence: hasHeading ? 'high' : 'low', + // Fabricated RAW PRIOR poses (same personalities as Austin/Caltrans); + // the client ground-snap + manual calibration own the truth. + pitchDeg: hasHeading ? -24 : -18, + fovDeg: hasHeading ? 56 : 44, + rangeM: hasHeading ? 210 : 145, + mountHeightM: hasHeading ? 10 : 8, + groundElevationM: 10, // Delaware is near sea level and flat. + feedType: 'hls', + url: streamUrl, + snapshotUrl: streamUrl, + sourceKind: 'deldot-open-data', + license: 'Public DelDOT traffic camera', + }); + } + + const unique = Array.from( + new Map(cameras.map((camera) => [camera.id, camera])).values(), + ); + const maxRaw = Number( + process.env.CCTV_DELDOT_MAX_SOURCES || DEFAULT_DELDOT_MAX_SOURCES, + ); + const maxCount = Number.isFinite(maxRaw) + ? Math.max(8, Math.min(400, Math.floor(maxRaw))) + : DEFAULT_DELDOT_MAX_SOURCES; + const prioritized = prioritizeSources(unique, maxCount, DELDOT_ANCHORS); + console.log( + `[CCTV] Loaded DelDOT camera sources: ${unique.length} Active (using nearest ${prioritized.length})`, + ); + return prioritized; + } catch (error) { + console.warn( + '[CCTV] DelDOT source download error:', + error?.message || error, + ); + return []; + } +}