Files
gods-eye-view/src/layers/cctv/source.js
T
Rudy Mizrahi Celekli 2fd1b4323c fix(tools): identify historical camera fallback imagery (#1118)
The CCTV frame proxy marks a Street View fallback with X-CCTV-Source, but the portable source dropped the header and get_cctv_snapshot always reported a current view credited to the camera's agency. The source now keeps the recognised provenance, and the tool labels Street View frames as historical Google imagery, refuses synthetic placeholders, and reports unknown provenance without claiming a current view.
2026-10-16 17:06:34 -07:00

80 lines
2.7 KiB
JavaScript

import {
ACTIVE_FRAME_REFRESH_MS,
FRAME_ENDPOINT,
MEDIA_ENDPOINT,
} from './sourcePolicy.js';
const FRAME_SOURCE_KINDS = new Set([
'upstream-image',
'hls-pull',
'streetview',
'synthetic',
]);
function safeNumber(value, fallback = NaN) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}
function frameUrlFor(camera, refreshMs = ACTIVE_FRAME_REFRESH_MS) {
const cadenceMs = Math.max(
1000,
safeNumber(refreshMs, ACTIVE_FRAME_REFRESH_MS),
);
const tick = Math.floor(Date.now() / cadenceMs);
const params = new URLSearchParams({
label: camera.name,
city: camera.city,
lat: camera.lat.toFixed(6),
lon: camera.lon.toFixed(6),
heading: String(Math.round(camera.headingDeg)),
fov: String(Math.round(camera.fovDeg)),
pitch: String(Math.round(camera.pitchDeg || -10)),
ts: String(tick),
});
return `${FRAME_ENDPOINT}/${encodeURIComponent(camera.id)}?${params.toString()}`;
}
function mediaUrlFor(camera) {
return `${MEDIA_ENDPOINT}/${encodeURIComponent(camera.id)}?ts=${Math.floor(Date.now() / 15000)}`;
}
/** Supply catalog/health records and the existing registered camera URL families. */
export function createCctvSource({
fetchImpl = (...args) => globalThis.fetch(...args),
} = {}) {
async function read(path, key, { signal } = {}) {
signal?.throwIfAborted();
const response = await fetchImpl(path, { cache: 'no-store', signal });
if (!response.ok) throw new Error('Camera source HTTP ' + response.status);
const payload = await response.json();
signal?.throwIfAborted();
if (!Array.isArray(payload?.[key]))
throw new Error('Malformed camera ' + key + ' snapshot');
return payload;
}
return {
getCatalog(options) {
return read('/api/cctv/sources', 'sources', options);
},
getHealth(options) {
return read('/api/cctv/health', 'cameras', options);
},
getFrameUrl: frameUrlFor,
getMediaUrl: mediaUrlFor,
/** Read a registered frame and its provenance, including fallback frames. */
async getFrame(camera, { signal } = {}) {
signal?.throwIfAborted();
const response = await fetchImpl(frameUrlFor(camera), {
cache: 'no-store',
signal,
});
if (!response.ok) throw new Error('Camera frame HTTP ' + response.status);
const bytes = new Uint8Array(await response.arrayBuffer());
signal?.throwIfAborted();
const type = response.headers.get('content-type') || '';
const sourceKind = response.headers.get('x-cctv-source');
return {
contentType: type.split(';')[0].trim().toLowerCase(),
bytes,
sourceKind: FRAME_SOURCE_KINDS.has(sourceKind) ? sourceKind : null,
};
},
};
}