diff --git a/index.html b/index.html
index fafe143..0212ca6 100644
--- a/index.html
+++ b/index.html
@@ -156,6 +156,7 @@
let initialOffset = 0;
let zoom = 1;
let focus = 43200;
+ let encodingWasRunning = false;
// encoding
const encToggle = document.querySelector("#encode-toggle");
@@ -175,6 +176,8 @@
document.querySelector("#encode-status").textContent = d.running
? `${d.completed}/${d.total} ${d.skipped} skip ${d.failed} err`
: d.total ? `${d.completed} done ${d.skipped} skip ${d.failed} err` : "Ready";
+ if (encodingWasRunning && !d.running) loadDate(dateSelect.value);
+ encodingWasRunning = d.running;
if (d.running) setTimeout(updateEncodeStatus, 1000);
}
document.querySelector("#encode-btn").addEventListener("click", async () => {
diff --git a/server.py b/server.py
index 650ff7d..557fbac 100644
--- a/server.py
+++ b/server.py
@@ -45,6 +45,12 @@ def scan_recordings(root):
class PlayerHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
+ def do_HEAD(self):
+ if urlparse(self.path).path == "/video":
+ self.send_video(self.path.split("?", 1)[1] if "?" in self.path else "", head_only=True)
+ else:
+ self.send_error(404)
+
def do_GET(self):
request = urlparse(self.path)
if request.path == "/":
@@ -95,7 +101,7 @@ class PlayerHandler(BaseHTTPRequestHandler):
}
self.send_json(payload)
- def send_video(self, query):
+ def send_video(self, query, head_only=False):
params = parse_qs(query)
day = params.get("date", [""])[0]
name = params.get("name", [""])[0]
@@ -113,7 +119,7 @@ class PlayerHandler(BaseHTTPRequestHandler):
# Pre-encoded clips can be served immediately; the offset is applied by the UI.
if clip["path"].suffix.lower() == ".mp4":
- return self.send_file(clip["path"], "video/mp4")
+ return self.send_range_file(clip["path"], "video/mp4", head_only)
command = [
"ffmpeg", "-hide_banner", "-loglevel", "error", "-ss", str(offset),
@@ -139,6 +145,47 @@ class PlayerHandler(BaseHTTPRequestHandler):
process.kill()
process.wait()
+ def send_range_file(self, path, content_type, head_only=False):
+ size = path.stat().st_size
+ range_header = self.headers.get("Range")
+ start, end = 0, size - 1
+ if range_header and range_header.startswith("bytes="):
+ try:
+ value = range_header[6:].split(",", 1)[0]
+ first, last = value.split("-", 1)
+ if first:
+ start = int(first)
+ end = int(last) if last else size - 1
+ else:
+ start = max(0, size - int(last))
+ end = min(end, size - 1)
+ if start > end or start >= size:
+ self.send_response(416)
+ self.send_header("Content-Range", f"bytes */{size}")
+ self.end_headers()
+ return
+ except ValueError:
+ pass
+ length = end - start + 1
+ self.send_response(206 if range_header else 200)
+ self.send_header("Content-Type", content_type)
+ self.send_header("Accept-Ranges", "bytes")
+ self.send_header("Content-Length", str(length))
+ if range_header:
+ self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
+ self.end_headers()
+ if head_only:
+ return
+ with path.open("rb") as source:
+ source.seek(start)
+ remaining = length
+ while remaining:
+ chunk = source.read(min(256 * 1024, remaining))
+ if not chunk:
+ break
+ self.wfile.write(chunk)
+ remaining -= len(chunk)
+
def log_message(self, message, *args):
print(f"{self.client_address[0]} - {message % args}")
@@ -177,7 +224,7 @@ def main():
def run_encoding(server):
- source_files = sorted(server.source_root.rglob("*.avi"))
+ source_files = sorted(path for path in server.source_root.rglob("*") if path.is_file() and path.suffix.lower() == ".avi")
status = {"running": True, "total": len(source_files), "completed": 0, "skipped": 0, "failed": 0}
server.encode_status = status
lock = threading.Lock()