Display recorder system highlights

This commit is contained in:
human insan
2026-08-27 22:55:20 +01:00
parent 0bfea476da
commit eebbc346e1
3 changed files with 40 additions and 1 deletions
+1
View File
@@ -8,6 +8,7 @@ dated video folders into a simple daily view that is easy to search and play.
- Shows recordings on a full 24-hour timeline. - Shows recordings on a full 24-hour timeline.
- Uses the time in each filename to place clips at the correct time of day. - Uses the time in each filename to place clips at the correct time of day.
- Shows recorded minutes in light blue and empty periods in dark blue. - Shows recorded minutes in light blue and empty periods in dark blue.
- Shows recorder-detected system events in dark yellow when `fileindex.dat` is present.
- Lets you choose the recording date. - Lets you choose the recording date.
- Plays browser-friendly MP4 files immediately. - Plays browser-friendly MP4 files immediately.
- Converts an AVI clip automatically when no MP4 copy exists. - Converts an AVI clip automatically when no MP4 copy exists.
+15 -1
View File
@@ -66,6 +66,7 @@
.legend .rec::before { background: var(--accent); } .legend .rec::before { background: var(--accent); }
.legend .gap::before { background: #2a2a2a; border: 1px solid #444; } .legend .gap::before { background: #2a2a2a; border: 1px solid #444; }
.legend .highlight::before { background: #ffd43b; } .legend .highlight::before { background: #ffd43b; }
.legend .system::before { background: #a87900; }
.highlights { margin-top: 18px; background: var(--surface); border-radius: 8px; padding: 14px 16px; } .highlights { margin-top: 18px; background: var(--surface); border-radius: 8px; padding: 14px 16px; }
.highlights h2 { font-size: 14px; font-weight: 500; margin-bottom: 10px; } .highlights h2 { font-size: 14px; font-weight: 500; margin-bottom: 10px; }
.highlight-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .highlight-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
@@ -160,7 +161,7 @@
<canvas id="timeline"></canvas> <canvas id="timeline"></canvas>
</div> </div>
<div class="hours" id="hours"></div> <div class="hours" id="hours"></div>
<div class="legend"><span class="rec">Recorded</span><span class="highlight">Highlight</span><span class="gap">No recording</span></div> <div class="legend"><span class="rec">Recorded</span><span class="highlight">Highlight</span><span class="system">System highlight</span><span class="gap">No recording</span></div>
</div> </div>
<section class="highlights"> <section class="highlights">
@@ -198,6 +199,7 @@
let zoom = 1; let zoom = 1;
let focus = 43200; let focus = 43200;
let highlights = []; let highlights = [];
let systemHighlights = [];
let highlightStart = null; let highlightStart = null;
let highlightEnd = null; let highlightEnd = null;
let encodingWasRunning = false; let encodingWasRunning = false;
@@ -316,6 +318,11 @@
list.append(row); list.append(row);
}); });
} }
async function loadSystemHighlights() {
const response = await fetch(`/api/system-highlights?date=${dateSelect.value}`);
systemHighlights = await response.json();
drawTimeline();
}
document.querySelector("#mark-start").onclick = () => setMark("start"); document.querySelector("#mark-start").onclick = () => setMark("start");
document.querySelector("#mark-end").onclick = () => setMark("end"); document.querySelector("#mark-end").onclick = () => setMark("end");
document.querySelector("#save-highlight").onclick = async () => { document.querySelector("#save-highlight").onclick = async () => {
@@ -398,6 +405,12 @@
const r = (item.end - winStart) / winSize * w; const r = (item.end - winStart) / winSize * w;
if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56); if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56);
}); });
ctx.fillStyle = "#a87900";
systemHighlights.forEach(item => {
const l = (item.start - winStart) / winSize * w;
const r = (item.end - winStart) / winSize * w;
if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56);
});
// hour lines // hour lines
ctx.strokeStyle = "rgba(255,255,255,.08)"; ctx.strokeStyle = "rgba(255,255,255,.08)";
for (let h = Math.ceil(winStart / 3600); h < winEnd / 3600; h++) { for (let h = Math.ceil(winStart / 3600); h < winEnd / 3600; h++) {
@@ -449,6 +462,7 @@
scrubFill.style.width = "0%"; scrubFill.style.width = "0%";
drawTimeline(); drawTimeline();
loadHighlights(); loadHighlights();
loadSystemHighlights();
if (!date && d.dates[0]) loadDate(d.dates[0]); if (!date && d.dates[0]) loadDate(d.dates[0]);
} }
+24
View File
@@ -4,7 +4,9 @@ import concurrent.futures
import json import json
import re import re
import subprocess import subprocess
import struct
import threading import threading
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
@@ -62,6 +64,24 @@ def normalize_date(value):
return date return date
def scan_system_highlights(root):
try:
data = (root / "fileindex.dat").read_bytes()
except OSError:
return []
highlights = []
for offset in range(0, len(data) - 15, 16):
timestamp, packed, _, _ = struct.unpack_from("<4I", data, offset)
duration = packed & 0xffff
event = (packed >> 16) & 1
if not event or not duration:
continue
moment = datetime.fromtimestamp(timestamp)
start = moment.hour * 3600 + moment.minute * 60 + moment.second
highlights.append({"date": moment.strftime("%Y%m%d"), "start": start, "end": min(86400, start + duration)})
return highlights
class PlayerHandler(BaseHTTPRequestHandler): class PlayerHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" protocol_version = "HTTP/1.1"
@@ -81,6 +101,9 @@ class PlayerHandler(BaseHTTPRequestHandler):
return self.send_json(self.server.encode_status) return self.send_json(self.server.encode_status)
if request.path == "/api/highlights": if request.path == "/api/highlights":
return self.send_json(self.server.highlights) return self.send_json(self.server.highlights)
if request.path == "/api/system-highlights":
date = parse_qs(request.query).get("date", [""])[0]
return self.send_json([item for item in self.server.system_highlights if item["date"] == date])
if request.path == "/video": if request.path == "/video":
return self.send_video(request.query) return self.send_video(request.query)
self.send_error(404) self.send_error(404)
@@ -274,6 +297,7 @@ def main():
server.output_root = args.root.expanduser().resolve() server.output_root = args.root.expanduser().resolve()
server.highlights_path = server.output_root / "highlights.json" server.highlights_path = server.output_root / "highlights.json"
server.highlights = load_highlights(server.highlights_path) server.highlights = load_highlights(server.highlights_path)
server.system_highlights = scan_system_highlights(server.source_root)
server.encode_status = {"running": False, "total": 0, "completed": 0, "skipped": 0, "failed": 0} server.encode_status = {"running": False, "total": 0, "completed": 0, "skipped": 0, "failed": 0}
count = sum(map(len, recordings.values())) count = sum(map(len, recordings.values()))
print(f"Indexed {count} recordings across {len(recordings)} days") print(f"Indexed {count} recordings across {len(recordings)} days")