From 99918cb774ac5efd496615c59fbf9aae08fc640a Mon Sep 17 00:00:00 2001 From: human insan Date: Thu, 27 Aug 2026 22:13:10 +0100 Subject: [PATCH] Initial commit: CCTV player with 24h timeline --- README.md | 17 + __pycache__/server.cpython-314.pyc | Bin 0 -> 15127 bytes index.html | 383 ++++++++++++++++++++++ server.log | 504 +++++++++++++++++++++++++++++ server.py | 220 +++++++++++++ 5 files changed, 1124 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/server.cpython-314.pyc create mode 100644 index.html create mode 100644 server.log create mode 100644 server.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..e084dfa --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# C41 Recorder Player + +Run: + +```sh +python3 server.py +``` + +Then open . The server indexes the dated folders in +`/home/human/Videos/Yassine Salon/C41/media` and uses FFmpeg to convert only the +currently playing AVI clip for the browser. Original recordings are unchanged. + +To use another archive or port: + +```sh +python3 server.py "/path/to/media" --port 9000 +``` diff --git a/__pycache__/server.cpython-314.pyc b/__pycache__/server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3b9bfbb404cb1e3dd6740c5f344eeeac3c75c79 GIT binary patch literal 15127 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 + + + + + C41 Archive + + + +
+
+

C41 Archive

+ +
+ +
+ +
+
+
+
+
00:00
+
+
+ + + +
+ + +
+ 0:00 / 0:00 + + --:--:-- +
+
+
+ +
+
+

24h Timeline

+
+ + +
+
+
+ +
+
+
RecordedNo recording
+
+ +
+ +
+ + + Ready +
+
+
+ + + + diff --git a/server.log b/server.log new file mode 100644 index 0000000..76dbd3f --- /dev/null +++ b/server.log @@ -0,0 +1,504 @@ +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 33568) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 33562) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 56898) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39706) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39718) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39720) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 52840) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39066) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39068) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39084) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39090) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 55796) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 35948) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 35964) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39890) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39892) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 500, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 39894) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 468, in handle_one_request + self.raw_requestline = self.rfile.readline(65537) + ~~~~~~~~~~~~~~~~~~~^^^^^^^ + File "/usr/lib/python3.14/socket.py", line 729, in readinto + return self._sock.recv_into(b) + ~~~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 52604) +Traceback (most recent call last): + File "/usr/lib/python3.14/socketserver.py", line 697, in process_request_thread + self.finish_request(request, client_address) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 766, in __init__ + self.handle() + ~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 502, in handle + self.handle_one_request() + ~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/usr/lib/python3.14/http/server.py", line 488, in handle_one_request + method() + ~~~~~~^^ + File "/home/human/Work/recorder-player/server.py", line 57, in do_GET + return self.send_video(request.query) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 116, in send_video + return self.send_file(clip["path"], "video/mp4") + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/human/Work/recorder-player/server.py", line 82, in send_file + self.wfile.write(data) + ~~~~~~~~~~~~~~~~^^^^^^ + File "/usr/lib/python3.14/socketserver.py", line 845, in write + self._sock.sendall(b) + ~~~~~~~~~~~~~~~~~~^^^ +ConnectionResetError: [Errno 104] Connection reset by peer +---------------------------------------- diff --git a/server.py b/server.py new file mode 100644 index 0000000..650ff7d --- /dev/null +++ b/server.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +import argparse +import concurrent.futures +import json +import re +import subprocess +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import parse_qs, urlparse + + +DATE_RE = re.compile(r"^\d{8}$") +VIDEO_RE = re.compile(r"^(\d{2})\.(\d{2})\.(\d{2})\.(avi|mp4)$", re.IGNORECASE) +APP_DIR = Path(__file__).resolve().parent + + +def scan_recordings(root): + recordings = {} + if not root.is_dir(): + return recordings + + for day_dir in root.iterdir(): + if not day_dir.is_dir() or not DATE_RE.fullmatch(day_dir.name): + continue + clips = [] + for path in day_dir.iterdir(): + match = VIDEO_RE.fullmatch(path.name) + if not path.is_file() or not match: + continue + hour, minute, second = map(int, match.groups()[:3]) + if hour > 23 or minute > 59 or second > 59: + continue + clips.append({ + "name": path.name, + "start": hour * 3600 + minute * 60 + second, + "duration": 120, + "path": path, + }) + if clips: + recordings[day_dir.name] = sorted(clips, key=lambda clip: clip["start"]) + return recordings + + +class PlayerHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self): + request = urlparse(self.path) + if request.path == "/": + return self.send_file(APP_DIR / "index.html", "text/html; charset=utf-8") + if request.path == "/api/recordings": + return self.send_recordings(request.query) + if request.path == "/api/encode/status": + return self.send_json(self.server.encode_status) + if request.path == "/video": + return self.send_video(request.query) + self.send_error(404) + + def do_POST(self): + if urlparse(self.path).path == "/api/encode": + if not self.server.encode_status["running"]: + threading.Thread(target=run_encoding, args=(self.server,), daemon=True).start() + return self.send_json(self.server.encode_status) + self.send_error(404) + + def send_json(self, payload): + data = json.dumps(payload).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def send_file(self, path, content_type): + data = path.read_bytes() + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def send_recordings(self, query): + params = parse_qs(query) + day = params.get("date", [""])[0] + clips = self.server.recordings.get(day, []) + payload = { + "dates": sorted(self.server.recordings, reverse=True), + "date": day, + "recordings": [ + {key: clip[key] for key in ("name", "start", "duration")} + for clip in clips + ], + } + self.send_json(payload) + + def send_video(self, query): + params = parse_qs(query) + day = params.get("date", [""])[0] + name = params.get("name", [""])[0] + clip = next( + (item for item in self.server.recordings.get(day, []) if item["name"] == name), + None, + ) + if not clip: + return self.send_error(404, "Recording not found") + + try: + offset = max(0, min(float(params.get("offset", ["0"])[0]), 119.9)) + except ValueError: + return self.send_error(400, "Invalid offset") + + # 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") + + command = [ + "ffmpeg", "-hide_banner", "-loglevel", "error", "-ss", str(offset), + "-i", str(clip["path"]), "-map", "0:v:0", "-map", "0:a:0?", + "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", + "-crf", "28", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "96k", + "-g", "20", "-flush_packets", "1", + "-movflags", "frag_keyframe+empty_moov+default_base_moof", + "-f", "mp4", "pipe:1", + ] + process = subprocess.Popen(command, stdout=subprocess.PIPE) + self.send_response(200) + self.send_header("Content-Type", "video/mp4") + self.send_header("Cache-Control", "no-store") + self.send_header("Connection", "close") + self.end_headers() + try: + while chunk := process.stdout.read(256 * 1024): + self.wfile.write(chunk) + except (BrokenPipeError, ConnectionResetError): + pass + finally: + process.kill() + process.wait() + + def log_message(self, message, *args): + print(f"{self.client_address[0]} - {message % args}") + + +def main(): + parser = argparse.ArgumentParser(description="Play dated recorder AVI files in a browser") + parser.add_argument( + "root", + nargs="?", + type=Path, + default=Path("/home/human/Videos/Yassine Salon/C41/media-mp4"), + help="folder containing YYYYMMDD recording directories", + ) + parser.add_argument("--port", type=int, default=8765) + parser.add_argument( + "--source", + type=Path, + default=Path("/home/human/Videos/Yassine Salon/C41/media"), + help="folder containing source AVI files for manual encoding", + ) + args = parser.parse_args() + + recordings = scan_recordings(args.root.expanduser().resolve()) + server = ThreadingHTTPServer(("127.0.0.1", args.port), PlayerHandler) + server.recordings = recordings + server.source_root = args.source.expanduser().resolve() + server.output_root = args.root.expanduser().resolve() + server.encode_status = {"running": False, "total": 0, "completed": 0, "skipped": 0, "failed": 0} + count = sum(map(len, recordings.values())) + print(f"Indexed {count} recordings across {len(recordings)} days") + print(f"Open http://127.0.0.1:{args.port}") + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nStopped") + + +def run_encoding(server): + source_files = sorted(server.source_root.rglob("*.avi")) + status = {"running": True, "total": len(source_files), "completed": 0, "skipped": 0, "failed": 0} + server.encode_status = status + lock = threading.Lock() + + def encode(source): + relative = source.relative_to(server.source_root) + output = server.output_root / relative.with_suffix(".mp4") + output.parent.mkdir(parents=True, exist_ok=True) + temporary = output.with_suffix(".encoding.mp4") + if output.exists(): + return "skipped" + command = [ + "ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", str(source), + "-map", "0:v:0", "-map", "0:a:0?", "-vf", "scale=854:-2", + "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", + "-crf", "32", "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "64k", + "-movflags", "+faststart", "-f", "mp4", str(temporary), + ] + try: + result = subprocess.run(command, capture_output=True, text=True) + if result.returncode != 0: + temporary.unlink(missing_ok=True) + return "failed" + temporary.replace(output) + return "completed" + except OSError: + temporary.unlink(missing_ok=True) + return "failed" + + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as workers: + futures = [workers.submit(encode, source) for source in source_files] + for future in concurrent.futures.as_completed(futures): + result = future.result() + with lock: + status[result] += 1 + status["running"] = False + + +if __name__ == "__main__": + main()