114 lines
3.9 KiB
JavaScript
114 lines
3.9 KiB
JavaScript
export function createCameraPreview({ root, baseUrl, getState, onAvailable }) {
|
|
const cameras = new Map();
|
|
let enabled = false;
|
|
let timer = null;
|
|
|
|
function sources() {
|
|
const { state, receivedAt } = getState();
|
|
if (!enabled || Date.now() - receivedAt >= 3000) return [];
|
|
const list = Array.isArray(state?.cameras) ? state.cameras
|
|
: state?.topic_status?.camera_image?.available ? [{ id: 'legacy', name: 'Camera', available: true }] : [];
|
|
const seen = new Set();
|
|
return list.filter(item => {
|
|
if (!item?.available || !/^[a-zA-Z0-9_-]{1,64}$/.test(item.id) || seen.has(item.id)) return false;
|
|
seen.add(item.id);
|
|
return true;
|
|
}).slice(0, 8);
|
|
}
|
|
|
|
function clearFrame(camera) {
|
|
if (camera.url) URL.revokeObjectURL(camera.url);
|
|
camera.url = null;
|
|
camera.image.removeAttribute('src');
|
|
}
|
|
|
|
function remove(id, camera) {
|
|
cameras.delete(id);
|
|
camera.controller?.abort();
|
|
clearFrame(camera);
|
|
camera.element.remove();
|
|
}
|
|
|
|
async function frame(id, camera) {
|
|
if (camera.controller || Date.now() < camera.retryAt || document.hidden) return;
|
|
const controller = new AbortController();
|
|
camera.controller = controller;
|
|
const timeout = window.setTimeout(() => controller.abort(), 2000);
|
|
try {
|
|
const query = id === 'legacy' ? '' : `?camera_id=${encodeURIComponent(id)}`;
|
|
const response = await fetch(`${baseUrl}/camera-frame${query}`, { cache: 'no-store', signal: controller.signal });
|
|
if (!response.ok) throw new Error('Camera frame unavailable');
|
|
const blob = await response.blob();
|
|
if (controller.signal.aborted || document.hidden || cameras.get(id) !== camera || !sources().some(item => item.id === id)) return;
|
|
const previous = camera.url;
|
|
camera.url = URL.createObjectURL(blob);
|
|
camera.image.src = camera.url;
|
|
camera.retryAt = 0;
|
|
if (previous) URL.revokeObjectURL(previous);
|
|
} catch (_) {
|
|
if (cameras.get(id) === camera) {
|
|
camera.retryAt = Date.now() + 5000;
|
|
clearFrame(camera);
|
|
}
|
|
} finally {
|
|
window.clearTimeout(timeout);
|
|
if (camera.controller === controller) camera.controller = null;
|
|
}
|
|
}
|
|
|
|
function refresh() {
|
|
const live = sources();
|
|
const ids = new Set(live.map(item => item.id));
|
|
for (const [id, camera] of cameras) if (!ids.has(id)) remove(id, camera);
|
|
for (const item of live) {
|
|
let camera = cameras.get(item.id);
|
|
if (!camera) {
|
|
const element = document.createElement('div');
|
|
element.className = 'camera-preview-item';
|
|
element.dataset.cameraId = item.id;
|
|
const label = document.createElement('div');
|
|
label.className = 'camera-preview-label';
|
|
const image = document.createElement('img');
|
|
image.className = 'camera-preview-image';
|
|
element.append(label, image);
|
|
root.append(element);
|
|
camera = { element, label, image, url: null, controller: null, retryAt: 0 };
|
|
image.addEventListener('error', () => {
|
|
if (cameras.get(item.id) === camera) {
|
|
clearFrame(camera);
|
|
camera.retryAt = Date.now() + 5000;
|
|
}
|
|
});
|
|
cameras.set(item.id, camera);
|
|
}
|
|
camera.label.textContent = item.name || item.id;
|
|
camera.image.alt = item.name || item.id;
|
|
void frame(item.id, camera);
|
|
}
|
|
onAvailable(enabled && cameras.size > 0);
|
|
}
|
|
|
|
function visibilityChanged() {
|
|
if (document.hidden) for (const camera of cameras.values()) camera.controller?.abort();
|
|
else refresh();
|
|
}
|
|
document.addEventListener('visibilitychange', visibilityChanged);
|
|
|
|
function setEnabled(value) {
|
|
enabled = value;
|
|
if (timer) window.clearInterval(timer);
|
|
timer = null;
|
|
refresh();
|
|
if (enabled) timer = window.setInterval(refresh, 200);
|
|
}
|
|
|
|
return {
|
|
setEnabled,
|
|
refresh,
|
|
dispose() {
|
|
setEnabled(false);
|
|
document.removeEventListener('visibilitychange', visibilityChanged);
|
|
},
|
|
};
|
|
}
|