mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
cctv: DelDOT statewide loader — open catalog (tmc.deldot.gov), Active-only, rtmpt:80 HLS, heading from title travel tokens; registered in LIVE_PACKS, gated by CCTV_DELDOT_ENABLED
This commit is contained in:
1 parent
71aa889133
commit
dfe2298ad0
3 files changed
+107
No files matched your search
@@ -16,6 +16,7 @@ import {
|
|||||||
loadTarkteeSourcesFromDatex,
|
loadTarkteeSourcesFromDatex,
|
||||||
loadWarendorfSourcesFromCatalog,
|
loadWarendorfSourcesFromCatalog,
|
||||||
loadNswSourcesFromOpenData,
|
loadNswSourcesFromOpenData,
|
||||||
|
loadDelDOTSourcesFromOpenData,
|
||||||
} from './sources.js';
|
} from './sources.js';
|
||||||
|
|
||||||
/** Env kill switch: unset or anything but "0" means enabled. */
|
/** Env kill switch: unset or anything but "0" means enabled. */
|
||||||
@@ -80,6 +81,11 @@ const LIVE_PACKS = [
|
|||||||
enabled: () => envEnabled('CCTV_NSW_ENABLED'),
|
enabled: () => envEnabled('CCTV_NSW_ENABLED'),
|
||||||
load: loadNswSourcesFromOpenData,
|
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).
|
* Load CCTV sources from a local JSON file (CCTV_SOURCES_FILE env or default).
|
||||||
|
|||||||
@@ -220,6 +220,14 @@ export const NSW_IMAGE_USER_AGENT =
|
|||||||
* out around 120 characters.
|
* out around 120 characters.
|
||||||
*/
|
*/
|
||||||
export const NSW_MAX_VIEW_LABEL = 140;
|
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. */
|
/** 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;
|
export const CCTV_SOURCE_CACHE_MS = 15 * 60 * 1000;
|
||||||
/** Per-provider catalog-fetch timeout. Bounds the worst-case refresh so one
|
/** Per-provider catalog-fetch timeout. Bounds the worst-case refresh so one
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ import {
|
|||||||
SYDNEY_CENTER,
|
SYDNEY_CENTER,
|
||||||
NSW_MAX_VIEW_LABEL,
|
NSW_MAX_VIEW_LABEL,
|
||||||
CCTV_SOURCE_FETCH_TIMEOUT_MS,
|
CCTV_SOURCE_FETCH_TIMEOUT_MS,
|
||||||
|
DELDOT_CCTV_URL,
|
||||||
|
DEFAULT_DELDOT_MAX_SOURCES,
|
||||||
|
DELDOT_ANCHORS,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
import {
|
import {
|
||||||
toFiniteNumber,
|
toFiniteNumber,
|
||||||
@@ -1389,3 +1392,93 @@ export async function loadNswSourcesFromOpenData() {
|
|||||||
return [];
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user