Display recorder system highlights
This commit is contained in:
@@ -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
@@ -70,6 +70,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; }
|
||||||
@@ -165,7 +166,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">
|
||||||
@@ -203,6 +204,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;
|
||||||
@@ -321,6 +323,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 () => {
|
||||||
@@ -403,6 +410,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++) {
|
||||||
@@ -454,6 +467,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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import concurrent.futures
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import struct
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
|
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
|
||||||
@@ -63,6 +65,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"
|
||||||
|
|
||||||
@@ -82,6 +102,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)
|
||||||
@@ -276,6 +299,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")
|
||||||
|
|||||||
Reference in New Issue
Block a user