Update multiCameraViewer.htm

master
Alan 2025-06-03 08:43:08 +01:00 zatwierdzone przez GitHub
rodzic 426bd8ceb5
commit b0937eae7c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
1 zmienionych plików z 91 dodań i 57 usunięć

Wyświetl plik

@ -11,8 +11,10 @@
Add camera IPs under "ADD YOUR CAMERA IPs HERE" Add camera IPs under "ADD YOUR CAMERA IPs HERE"
Modify load settings keys under 'load settings with keypress' Modify load settings keys under 'load settings with keypress'
Specify config on url: http://x.x.x.x?config=xxx
26May25 03Jun25
--> -->
@ -20,7 +22,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Camera Viewer with Motion Detection</title> <title>Camera Viewer</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0"> <meta http-equiv="Expires" content="0">
@ -89,8 +91,8 @@
position: absolute; position: absolute;
top: 4px; top: 4px;
right: 4px; right: 4px;
background: #333; background: rgba(0,0,0,0);
color: white; color: #00A;
border: none; border: none;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
@ -101,11 +103,11 @@
.cam-title { .cam-title {
position: absolute; position: absolute;
top: 4px; top: 4px;
left: 8px; left: 5px;
color: #fff; color: #00A;
background: rgba(0,0,0,0.5); background: rgba(0,0,0,0);
padding: 2px 6px; padding: 2px 6px;
font-size: 14px; font-size: 16px;
z-index: 5; z-index: 5;
pointer-events: none; pointer-events: none;
} }
@ -113,31 +115,31 @@
/* Styles for motion detection controls */ /* Styles for motion detection controls */
.motion-controls { .motion-controls {
position: absolute; position: absolute;
bottom: 4px; top: 26px;
left: 8px; left: 5px;
background: rgba(0,0,0,0.5); background: rgba(0,0,0,0);
padding: 3px 6px; padding: 0px 0px;
border-radius: 3px; border-radius: 0px;
z-index: 10; z-index: 10;
color: white; color: #00A;
font-size: 12px; font-size: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
} }
.motion-controls label { .motion-controls label {
margin-right: 5px; margin-right: 0px;
cursor: pointer; cursor: pointer;
} }
.motion-controls input[type="checkbox"] { .motion-controls input[type="checkbox"] {
margin-right: 5px; margin-right: 0px;
cursor: pointer; cursor: pointer;
} }
.motion-controls input[type="range"] { .motion-controls input[type="range"] {
width: 80px; width: 120px;
height: 10px; height: 8px;
cursor: pointer; cursor: pointer;
display: none; /* Hidden by default */ display: none; /* Hidden by default */
margin-left: 5px; margin-left: 5px;
@ -188,7 +190,7 @@
} }
// --- END DEBUGGING --- // --- END DEBUGGING ---
// load settings with keypress ('r' and 'b' unavailable)
const keyConfigMap = [ const keyConfigMap = [
{ key: 'm', config: 'main' }, { key: 'm', config: 'main' },
{ key: 's', config: 'small' }, { key: 's', config: 'small' },
@ -203,7 +205,7 @@
const currentDisplay = window.getComputedStyle(tb).display; const currentDisplay = window.getComputedStyle(tb).display;
tb.style.display = (currentDisplay === 'none' || tb.style.display === 'none') ? 'block' : 'none'; tb.style.display = (currentDisplay === 'none' || tb.style.display === 'none') ? 'block' : 'none';
} }
if (e.key === 'c') resetCameras(); if (e.key === 'r') resetCameras();
const match = keyConfigMap.find(item => e.key === item.key); const match = keyConfigMap.find(item => e.key === item.key);
if (match) { if (match) {
@ -223,7 +225,7 @@
const cameras = [ const cameras = [
{ src: "http://192.168.1.100/jpg", id: "Front" }, { src: "http://192.168.1.100/jpg", id: "Front" },
{ src: "http://192.168.1.101/jpg", id: "Side" }, { src: "http://192.168.1.101/jpg", id: "Side" },
{ src: "http://192.168.1.102/jpg", id: "Back" }, { src: "http://192.168.1.102/jpg", id: "Back" }
]; ];
const toolbar = document.getElementById('toolbar'); const toolbar = document.getElementById('toolbar');
@ -310,15 +312,17 @@
function showInstructions() { function showInstructions() {
alert(`CCTV Camera Viewer with Motion Detection alert(`CCTV Camera Viewer with Motion Detection
- Click a camera button to open its feed. - Click camera button to open its feed.
- Save/load/import/export configurations. - Save/Load/Import/Export configuration.
- Drag/resize windows. - Drag/Resize windows.
- SHIFT + Scroll to zoom, SHIFT + drag to pan. - SHIFT + Scroll to zoom, SHIFT + drag to pan.
- Motion Detection: Check box to enable, - Motion Detection: Check box to enable;
use slider for sensitivity. use slider for sensitivity.
- Press 'b' to hide/show the toolbar. - 'b' hide/show the toolbar.
- Quick load configs: 'm', 's', 'f'. - Quick load configs:
- Reset to close all feeds (or press 'C'). 'm' Main, 's' Side, 'f' Front.
- 'r'/Reset close all feeds.
- Specify config on URL: ?config=
`); `);
} }
@ -370,20 +374,27 @@
debugLog('State saved.'); debugLog('State saved.');
} }
function loadState() { function getQueryParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
function loadState(configName) {
debugLog('Loading state...'); debugLog('Loading state...');
// First, clear all existing cameras and their loops properly
// Clear existing cameras
document.querySelectorAll('.img-container').forEach(el => { document.querySelectorAll('.img-container').forEach(el => {
if (el._zoomPanState) { if (el._zoomPanState) el._zoomPanState.isActive = false;
debugLog(`Load: Setting isActive=false for old ${el.id}`);
el._zoomPanState.isActive = false;
}
el.remove(); el.remove();
}); });
const stateString = localStorage.getItem('cameraState'); // Get state from URL config or fallback to default
const stateString = configName
? localStorage.getItem('cameraState_' + configName)
: localStorage.getItem('cameraState');
if (!stateString) { if (!stateString) {
debugLog('No state found in localStorage to load.'); debugLog('No state found to load.');
return; return;
} }
@ -392,28 +403,21 @@
stateToLoad = JSON.parse(stateString); stateToLoad = JSON.parse(stateString);
} catch (e) { } catch (e) {
console.error("Error parsing saved state:", e); console.error("Error parsing saved state:", e);
alert("Error loading saved state. It might be corrupted."); alert("Error loading saved state.");
localStorage.removeItem('cameraState'); // Clear corrupted state
return; return;
} }
if (Object.keys(stateToLoad).length === 0) { for (const camDef of cameras) {
debugLog('Saved state is empty. Nothing to load.'); (stateToLoad[camDef.id] || []).forEach(cfg => {
return; createCameraWindow(camDef, cfg);
});
} }
for (const camDef of cameras) { // Iterate over defined cameras
if (stateToLoad[camDef.id]) { // If this camera type has saved instances
(stateToLoad[camDef.id] || []).forEach(cfg => {
debugLog(`Loading config for ${camDef.id} (unique: ${cfg.uniqueId})`);
createCameraWindow(camDef, cfg);
});
}
}
debugLog('State loaded.'); debugLog('State loaded.');
} }
function initAudio() { function initAudio() {
if (!audioContext) { if (!audioContext) {
try { try {
@ -491,7 +495,7 @@
<div class="img-inner"><img crossorigin="anonymous" /></div> <div class="img-inner"><img crossorigin="anonymous" /></div>
<div class="motion-indicator"></div> <div class="motion-indicator"></div>
<div class="motion-controls"> <div class="motion-controls">
<label for="motion_check_${uniqueId}">Motion:</label> <label for="motion_check_${uniqueId}"></label>
<input type="checkbox" id="motion_check_${uniqueId}" class="motion-checkbox"> <input type="checkbox" id="motion_check_${uniqueId}" class="motion-checkbox">
<input type="range" id="motion_slider_${uniqueId}" class="motion-slider" min="1" max="100" value="50"> <input type="range" id="motion_slider_${uniqueId}" class="motion-slider" min="1" max="100" value="50">
</div> </div>
@ -500,12 +504,36 @@
initContainer(div, cam.src, saved); initContainer(div, cam.src, saved);
} }
function compareFrames(ctx1, ctx2, width, height, sensitivity) { function compareFrames(ctx1, ctx2, width, height, sensitivity, containerId = "unknown_cam") { // Added containerId for logging
const imgData1 = ctx1.getImageData(0, 0, width, height).data; const imgData1 = ctx1.getImageData(0, 0, width, height).data;
const imgData2 = ctx2.getImageData(0, 0, width, height).data; const imgData2 = ctx2.getImageData(0, 0, width, height).data;
let diffCount = 0; let diffCount = 0;
const threshold = 25;
const requiredDiffPixels = (101 - sensitivity) * (width * height / 400); // --- MODIFIED PARAMETERS ---
// Original: const threshold = 10;
const threshold = 8; // Lowered from 10. Defines how much a single pixel's grayscale value needs to change.
// This makes individual pixel changes easier to detect.
// Original calculation was: (101 - sensitivity) * (width * height / 400)
// New calculation aims for a less steep sensitivity curve and generally more sensitivity.
// N_scaler: Compresses the effect of the sensitivity slider.
// A higher N_scaler means the slider has a less dramatic effect (curve is flatter).
// Original formula implicitly had N_scaler around 1.0 relative to its 1-100 range.
const N_scaler = 2.0;
// baseDivisor: Affects overall sensitivity. Higher baseDivisor = more sensitive (fewer pixels needed).
// Original was 400.0.
const baseDivisor = 800.0;
// 'sensitivity' is the slider value from 1 (low sensitivity) to 100 (high sensitivity).
// We want 'effectiveSensitivityFactor' to be low for high slider sensitivity, and high for low slider sensitivity.
// When slider sensitivity = 100 (max), effectiveSensitivityFactor = ((100-100)/2.0) + 1.0 = 1.0 (most sensitive factor part).
// When slider sensitivity = 1 (min), effectiveSensitivityFactor = ((100-1)/2.0) + 1.0 = 49.5 + 1.0 = 50.5 (least sensitive factor part).
const effectiveSensitivityFactor = ((100.0 - parseFloat(sensitivity)) / N_scaler) + 1.0;
const requiredDiffPixels = effectiveSensitivityFactor * (width * height / baseDivisor);
// --- END MODIFIED PARAMETERS ---
for (let i = 0; i < imgData1.length; i += 4) { for (let i = 0; i < imgData1.length; i += 4) {
const gray1 = (imgData1[i] + imgData1[i + 1] + imgData1[i + 2]) / 3; const gray1 = (imgData1[i] + imgData1[i + 1] + imgData1[i + 2]) / 3;
@ -513,7 +541,12 @@
if (Math.abs(gray1 - gray2) > threshold) diffCount++; if (Math.abs(gray1 - gray2) > threshold) diffCount++;
} }
const motionDetected = diffCount > requiredDiffPixels; const motionDetected = diffCount > requiredDiffPixels;
// debugLog(`compareFrames: diffCount=${diffCount}, required=${requiredDiffPixels.toFixed(2)}, detected=${motionDetected}`); // Very spammy
// For debugging the new values:
// Only log ~10% of the calls to reduce console spam, if DEBUG_MOTION is true.
if (DEBUG_MOTION && Math.random() < 0.1) { // Adjust 0.1 (10%) as needed for logging frequency
debugLog(`[${containerId}] compareFrames (slider: ${sensitivity}): diffCount=${diffCount}, reqDiffPixels=${requiredDiffPixels.toFixed(2)}, effectiveSensFactor=${effectiveSensitivityFactor.toFixed(2)}, threshold=${threshold}, detected=${motionDetected}`);
}
return motionDetected; return motionDetected;
} }
@ -743,7 +776,7 @@
if (prevImageLoaded) { if (prevImageLoaded) {
// debugLog(`[${container.id}] Comparing frames.`); // Spammy // debugLog(`[${container.id}] Comparing frames.`); // Spammy
if (compareFrames(ctx1, ctx2, motionWidth, motionHeight, state.motionSensitivity)) { if (compareFrames(ctx1, ctx2, motionWidth, motionHeight, state.motionSensitivity, container.id)) {
debugLog(`[${container.id}] Motion DETECTED! Calling playBeep.`); debugLog(`[${container.id}] Motion DETECTED! Calling playBeep.`);
playBeep(container.id); playBeep(container.id);
motionIndicator.style.display = 'block'; motionIndicator.style.display = 'block';
@ -805,7 +838,8 @@
} }
// Initial load of state from localStorage when the page loads // Initial load of state from localStorage when the page loads
loadState(); const startupConfig = getQueryParam('config');
loadState(startupConfig);
</script> </script>
</body> </body>