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()