Bewegungserkennung pro Kamera (Pillow), Bewegungsbilder in der Timelapse-Galerie mit Filter
This commit is contained in:
1 parent
420e8e1550
commit
ff3dc7ee2b
4 files changed
+125
-32
No files matched your search
+35
-8
@@ -78,6 +78,7 @@ main{padding:22px;max-width:1800px;margin:0 auto}
|
||||
#tl .big .when{position:absolute;left:12px;bottom:10px;background:rgba(0,0,0,.6);padding:2px 10px;border-radius:999px;font-size:13px;font-variant-numeric:tabular-nums}
|
||||
#tl .thumbs{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px}
|
||||
#tl .thumbs img{height:72px;aspect-ratio:16/9;object-fit:cover;border-radius:6px;border:2px solid transparent;cursor:pointer;flex:none;background:#000}
|
||||
#tl .thumbs img.mot{border-color:#f59e0b}
|
||||
#tl .thumbs img.sel{border-color:var(--accent)}
|
||||
#tl .empty{color:var(--muted);margin:auto}
|
||||
@media (max-width:700px){.grid{grid-template-columns:1fr}}
|
||||
@@ -125,6 +126,7 @@ const ICONS = {
|
||||
reload: '<path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15"/>',
|
||||
full: '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>',
|
||||
down: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>',
|
||||
motion: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
|
||||
rec: '<circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3" fill="currentColor"/>',
|
||||
film: '<rect x="2" y="3" width="20" height="18" rx="2"/><path d="M7 3v18M17 3v18M2 8h5M2 16h5M17 8h5M17 16h5"/>',
|
||||
off: '<path d="M1 1l22 22M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"/>'
|
||||
@@ -180,7 +182,7 @@ function makeTile(name, mode, hasSetup) {
|
||||
}, "alt");
|
||||
actions.append(setup);
|
||||
}
|
||||
const o = { name, mode, t, img, state, ovText, rec: null, tl: { enabled: false, interval: 300 }, stopped: false, online: null, timer: null, retry: null };
|
||||
const o = { name, mode, t, img, state, ovText, rec: null, mot: null, tl: { enabled: false, interval: 300 }, stopped: false, online: null, timer: null, retry: null };
|
||||
if (mode === "live") {
|
||||
actions.append(
|
||||
btn("Start", "play", () => startTile(o), "ok"),
|
||||
@@ -188,9 +190,11 @@ function makeTile(name, mode, hasSetup) {
|
||||
btn("Reload", "reload", () => reloadTile(o), "alt"));
|
||||
}
|
||||
const rec = btn("Timelapse aus", "rec", () => setTimelapse(name, { enabled: !tiles[name].tl.enabled }), "alt");
|
||||
const mot = btn("Bewegung aus", "motion", () => setTimelapse(name, { motion: !tiles[name].tl.motion }), "alt");
|
||||
const gallery = btn("Galerie", "film", () => openTimelapse(name), "alt");
|
||||
o.rec = rec;
|
||||
actions.append(snap, rec, gallery, full);
|
||||
o.mot = mot;
|
||||
actions.append(snap, rec, mot, gallery, full);
|
||||
cap.append(el("span", "dot"), link, state, el("span", "sp"), actions);
|
||||
t.append(view, cap);
|
||||
|
||||
@@ -219,12 +223,21 @@ async function openTimelapse(name) {
|
||||
TL_INTERVALS.forEach(v => ivSel.append(new Option("alle " + fmtInterval(v), v)));
|
||||
ivSel.value = tiles[name].tl.interval;
|
||||
ivSel.onchange = () => setTimelapse(name, { interval: +ivSel.value });
|
||||
const sensSel = el("select");
|
||||
sensSel.title = "Empfindlichkeit der Bewegungserkennung";
|
||||
[["low", "Bewegung: niedrig"], ["mid", "Bewegung: mittel"], ["high", "Bewegung: hoch"]].forEach(([v, t]) => sensSel.append(new Option(t, v)));
|
||||
sensSel.value = tiles[name].tl.sensitivity;
|
||||
sensSel.onchange = () => setTimelapse(name, { sensitivity: sensSel.value });
|
||||
sensSel.hidden = !tiles[name].motionOk;
|
||||
const kindSel = el("select");
|
||||
kindSel.title = "Anzeige filtern";
|
||||
[["all", "Alle Bilder"], ["lapse", "Nur Zeitraffer"], ["motion", "Nur Bewegung"]].forEach(([v, t]) => kindSel.append(new Option(t, v)));
|
||||
const fpsSel = el("select");
|
||||
fpsSel.title = "Bilder pro Sekunde im Video";
|
||||
[5, 10, 25].forEach(v => fpsSel.append(new Option(v + " fps", v)));
|
||||
fpsSel.value = 10;
|
||||
const video = btn("Video", "down", () => {}, "alt");
|
||||
bar.append(el("h2", "", "Timelapse " + name), daySel, play, ivSel, fpsSel, video, el("span", "sp"), close);
|
||||
bar.append(el("h2", "", "Timelapse " + name), daySel, kindSel, play, ivSel, sensSel, fpsSel, video, el("span", "sp"), close);
|
||||
bar.querySelector(".sp").style.flex = "1";
|
||||
const body = el("div", "body");
|
||||
tl.replaceChildren(bar, body);
|
||||
@@ -240,7 +253,7 @@ async function openTimelapse(name) {
|
||||
}
|
||||
const days = Object.keys(data.days).filter(d => data.days[d].length);
|
||||
if (!days.length) {
|
||||
body.append(el("div", "empty", "Noch keine Bilder – Timelapse mit dem Button in der Kachel starten"));
|
||||
body.append(el("div", "empty", "Noch keine Bilder – Timelapse oder Bewegungserkennung in der Kachel starten"));
|
||||
play.hidden = daySel.hidden = true;
|
||||
return;
|
||||
}
|
||||
@@ -255,7 +268,7 @@ async function openTimelapse(name) {
|
||||
idx = (i + files.length) % files.length;
|
||||
const d = daySel.value, f = files[idx];
|
||||
bigImg.src = url(d, f);
|
||||
when.textContent = d + " " + f.slice(0, 2) + ":" + f.slice(2, 4) + ":" + f.slice(4, 6);
|
||||
when.textContent = d + " " + f.slice(0, 2) + ":" + f.slice(2, 4) + ":" + f.slice(4, 6) + (f.includes("_m") ? " · Bewegung" : "");
|
||||
thumbs.querySelectorAll("img").forEach((im, k) => im.classList.toggle("sel", k === idx));
|
||||
const sel = thumbs.children[idx];
|
||||
if (sel) sel.scrollIntoView({ block: "nearest", inline: "center" });
|
||||
@@ -263,9 +276,15 @@ async function openTimelapse(name) {
|
||||
const stop = () => { clearInterval(tlPlay); play.querySelector("span").textContent = "Abspielen"; };
|
||||
const loadDay = () => {
|
||||
stop();
|
||||
files = data.days[daySel.value];
|
||||
files = data.days[daySel.value].filter(f => kindSel.value === "all" || (kindSel.value === "motion") === f.includes("_m"));
|
||||
if (!files.length) {
|
||||
thumbs.replaceChildren();
|
||||
bigImg.removeAttribute("src");
|
||||
when.textContent = "Keine Bilder dieser Art an diesem Tag";
|
||||
return;
|
||||
}
|
||||
thumbs.replaceChildren(...files.map((f, i) => {
|
||||
const im = el("img");
|
||||
const im = el("img", f.includes("_m") ? "mot" : "");
|
||||
im.loading = "lazy";
|
||||
im.src = url(daySel.value, f);
|
||||
im.onclick = () => { stop(); show(i); };
|
||||
@@ -274,12 +293,13 @@ async function openTimelapse(name) {
|
||||
show(files.length - 1);
|
||||
};
|
||||
daySel.onchange = loadDay;
|
||||
kindSel.onchange = loadDay;
|
||||
video.onclick = async () => {
|
||||
const label = video.querySelector("span");
|
||||
video.disabled = true;
|
||||
label.textContent = "erstelle …";
|
||||
try {
|
||||
const r = await fetch(url(daySel.value, "video") + "?fps=" + fpsSel.value);
|
||||
const r = await fetch(url(daySel.value, "video") + "?fps=" + fpsSel.value + (kindSel.value === "motion" ? "&kind=motion" : ""));
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
const a = el("a");
|
||||
a.href = URL.createObjectURL(await r.blob());
|
||||
@@ -294,6 +314,7 @@ async function openTimelapse(name) {
|
||||
video.disabled = false;
|
||||
};
|
||||
play.onclick = () => {
|
||||
if (!files.length) return;
|
||||
if (tlPlay) { const was = play.querySelector("span").textContent === "Pause"; stop(); if (was) return; }
|
||||
play.querySelector("span").textContent = "Pause";
|
||||
if (idx === files.length - 1) show(0);
|
||||
@@ -311,6 +332,11 @@ function renderTimelapse(o) {
|
||||
o.rec.classList.toggle("alt", !on);
|
||||
o.rec.title = on ? "Timelapse läuft (alle " + fmtInterval(o.tl.interval) + ") – klicken zum Stoppen" : "Timelapse starten";
|
||||
o.rec.querySelector("span").textContent = on ? "Timelapse " + fmtInterval(o.tl.interval) : "Timelapse aus";
|
||||
o.mot.hidden = !o.motionOk;
|
||||
o.mot.classList.toggle("rec", !!o.tl.motion);
|
||||
o.mot.classList.toggle("alt", !o.tl.motion);
|
||||
o.mot.title = o.tl.motion ? "Bewegungserkennung läuft – klicken zum Stoppen" : "Bewegungserkennung starten";
|
||||
o.mot.querySelector("span").textContent = o.tl.motion ? "Bewegung an" : "Bewegung aus";
|
||||
}
|
||||
|
||||
async function setTimelapse(name, change) {
|
||||
@@ -456,6 +482,7 @@ async function poll() {
|
||||
data.cameras.forEach(c => {
|
||||
setOnline(tiles[c.name], c.online);
|
||||
tiles[c.name].tl = c.timelapse;
|
||||
tiles[c.name].motionOk = c.motion_ok;
|
||||
renderTimelapse(tiles[c.name]);
|
||||
});
|
||||
const n = data.cameras.filter(c => c.online).length;
|
||||
|
||||
Reference in new issue
Block a user