Improve player controls and media seeking
This commit is contained in:
@@ -156,6 +156,7 @@
|
|||||||
let initialOffset = 0;
|
let initialOffset = 0;
|
||||||
let zoom = 1;
|
let zoom = 1;
|
||||||
let focus = 43200;
|
let focus = 43200;
|
||||||
|
let encodingWasRunning = false;
|
||||||
|
|
||||||
// encoding
|
// encoding
|
||||||
const encToggle = document.querySelector("#encode-toggle");
|
const encToggle = document.querySelector("#encode-toggle");
|
||||||
@@ -175,6 +176,8 @@
|
|||||||
document.querySelector("#encode-status").textContent = d.running
|
document.querySelector("#encode-status").textContent = d.running
|
||||||
? `${d.completed}/${d.total} ${d.skipped} skip ${d.failed} err`
|
? `${d.completed}/${d.total} ${d.skipped} skip ${d.failed} err`
|
||||||
: d.total ? `${d.completed} done ${d.skipped} skip ${d.failed} err` : "Ready";
|
: 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);
|
if (d.running) setTimeout(updateEncodeStatus, 1000);
|
||||||
}
|
}
|
||||||
document.querySelector("#encode-btn").addEventListener("click", async () => {
|
document.querySelector("#encode-btn").addEventListener("click", async () => {
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ def scan_recordings(root):
|
|||||||
class PlayerHandler(BaseHTTPRequestHandler):
|
class PlayerHandler(BaseHTTPRequestHandler):
|
||||||
protocol_version = "HTTP/1.1"
|
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):
|
def do_GET(self):
|
||||||
request = urlparse(self.path)
|
request = urlparse(self.path)
|
||||||
if request.path == "/":
|
if request.path == "/":
|
||||||
@@ -95,7 +101,7 @@ class PlayerHandler(BaseHTTPRequestHandler):
|
|||||||
}
|
}
|
||||||
self.send_json(payload)
|
self.send_json(payload)
|
||||||
|
|
||||||
def send_video(self, query):
|
def send_video(self, query, head_only=False):
|
||||||
params = parse_qs(query)
|
params = parse_qs(query)
|
||||||
day = params.get("date", [""])[0]
|
day = params.get("date", [""])[0]
|
||||||
name = params.get("name", [""])[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.
|
# Pre-encoded clips can be served immediately; the offset is applied by the UI.
|
||||||
if clip["path"].suffix.lower() == ".mp4":
|
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 = [
|
command = [
|
||||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-ss", str(offset),
|
"ffmpeg", "-hide_banner", "-loglevel", "error", "-ss", str(offset),
|
||||||
@@ -139,6 +145,47 @@ class PlayerHandler(BaseHTTPRequestHandler):
|
|||||||
process.kill()
|
process.kill()
|
||||||
process.wait()
|
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):
|
def log_message(self, message, *args):
|
||||||
print(f"{self.client_address[0]} - {message % args}")
|
print(f"{self.client_address[0]} - {message % args}")
|
||||||
|
|
||||||
@@ -177,7 +224,7 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
def run_encoding(server):
|
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}
|
status = {"running": True, "total": len(source_files), "completed": 0, "skipped": 0, "failed": 0}
|
||||||
server.encode_status = status
|
server.encode_status = status
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user