Initial commit: CCTV player with 24h timeline
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# C41 Recorder Player
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
python3 server.py
|
||||
```
|
||||
|
||||
Then open <http://127.0.0.1:8765>. 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
|
||||
```
|
||||
Binary file not shown.
+383
@@ -0,0 +1,383 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>C41 Archive</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; --bg: #0f0f0f; --surface: #272727; --hover: #3d3d3d; --text: #f1f1f1; --dim: #aaa; --accent: #3ea6ff; --red: #ff4e45; }
|
||||
* { box-sizing: border-box; margin: 0; }
|
||||
body { font-family: "Roboto", "Arial", sans-serif; background: var(--bg); color: var(--text); }
|
||||
.page { max-width: 900px; margin: 0 auto; padding: 16px; }
|
||||
|
||||
/* header */
|
||||
.header { display: flex; align-items: center; justify-content: space-between; padding: 8px 0 16px; }
|
||||
.header h1 { font-size: 18px; font-weight: 500; }
|
||||
.header select { background: var(--surface); color: var(--text); border: none; padding: 6px 10px; border-radius: 3px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
/* player wrapper */
|
||||
.player-wrap { position: relative; background: #000; border-radius: 12px; overflow: hidden; }
|
||||
.player-wrap video { display: block; width: 100%; aspect-ratio: 16/9; object-fit: contain; background: #000; }
|
||||
|
||||
/* big center play */
|
||||
.big-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 68px; height: 48px; background: rgba(0,0,0,.6); border-radius: 12px; display: grid; place-items: center; pointer-events: none; opacity: 0; transition: opacity .15s; }
|
||||
.player-wrap.paused .big-play { opacity: 1; }
|
||||
.big-play svg { width: 28px; height: 28px; }
|
||||
|
||||
/* bottom gradient */
|
||||
.controls-bar { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); padding: 30px 12px 10px; }
|
||||
|
||||
/* scrub bar */
|
||||
.scrub { position: relative; height: 4px; background: rgba(255,255,255,.2); cursor: pointer; margin-bottom: 8px; border-radius: 2px; transition: height .1s; }
|
||||
.scrub:hover { height: 6px; }
|
||||
.scrub-fill { position: absolute; left: 0; top: 0; bottom: 0; background: var(--red); border-radius: 2px; pointer-events: none; }
|
||||
.scrub-hover { position: absolute; top: -28px; background: rgba(0,0,0,.8); color: #fff; font-size: 12px; padding: 3px 7px; border-radius: 3px; pointer-events: none; white-space: nowrap; display: none; }
|
||||
.scrub:hover .scrub-hover { display: block; }
|
||||
|
||||
/* control row */
|
||||
.ctrl-row { display: flex; align-items: center; gap: 8px; }
|
||||
.ctrl-row button { background: none; border: none; color: #fff; cursor: pointer; padding: 4px; border-radius: 50%; display: grid; place-items: center; }
|
||||
.ctrl-row button:hover { background: rgba(255,255,255,.15); }
|
||||
.ctrl-row svg { width: 22px; height: 22px; fill: currentColor; }
|
||||
.time-label { font-size: 13px; color: #ddd; font-variant-numeric: tabular-nums; margin-left: 4px; user-select: none; }
|
||||
.spacer { flex: 1; }
|
||||
.vol-wrap { display: flex; align-items: center; gap: 4px; }
|
||||
.vol-wrap input { width: 0; opacity: 0; transition: width .2s, opacity .2s; accent-color: #fff; }
|
||||
.vol-wrap:hover input, .vol-wrap input:focus { width: 70px; opacity: 1; }
|
||||
|
||||
/* timeline below player */
|
||||
.timeline-section { margin-top: 16px; }
|
||||
.timeline-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
||||
.timeline-header h2 { font-size: 14px; font-weight: 500; color: var(--dim); }
|
||||
.zoom-btns { display: flex; gap: 4px; }
|
||||
.zoom-btns button { background: var(--surface); border: none; color: var(--text); padding: 4px 10px; border-radius: 3px; font-size: 12px; cursor: pointer; }
|
||||
.zoom-btns button:hover { background: var(--hover); }
|
||||
.timeline-canvas { position: relative; }
|
||||
.timeline-canvas canvas { display: block; width: 100%; height: 56px; border-radius: 6px; cursor: pointer; }
|
||||
.hours { display: flex; justify-content: space-between; margin-top: 4px; color: var(--dim); font-size: 10px; user-select: none; }
|
||||
.legend { display: flex; gap: 16px; margin-top: 10px; font-size: 11px; color: var(--dim); }
|
||||
.legend span { display: flex; align-items: center; gap: 5px; }
|
||||
.legend span::before { content: ""; display: block; width: 14px; height: 6px; border-radius: 2px; }
|
||||
.legend .rec::before { background: var(--accent); }
|
||||
.legend .gap::before { background: #2a2a2a; border: 1px solid #444; }
|
||||
|
||||
/* encoding panel */
|
||||
.encode-panel { margin-top: 16px; background: var(--surface); border-radius: 8px; overflow: hidden; }
|
||||
.encode-toggle { width: 100%; background: none; border: none; color: var(--text); padding: 12px 16px; font-size: 13px; font-weight: 500; cursor: pointer; text-align: left; display: flex; align-items: center; gap: 8px; }
|
||||
.encode-toggle:hover { background: var(--hover); }
|
||||
.encode-toggle svg { width: 16px; height: 16px; fill: var(--dim); transition: transform .2s; }
|
||||
.encode-toggle.open svg { transform: rotate(90deg); }
|
||||
.encode-body { display: none; padding: 0 16px 14px; }
|
||||
.encode-body.show { display: flex; align-items: center; gap: 12px; }
|
||||
.encode-body button { background: var(--accent); color: #000; border: none; padding: 7px 16px; border-radius: 3px; font-size: 13px; font-weight: 500; cursor: pointer; white-space: nowrap; }
|
||||
.encode-body button:hover { background: #65b8ff; }
|
||||
.encode-body button:disabled { background: #555; color: #999; cursor: default; }
|
||||
.encode-body progress { flex: 1; height: 4px; accent-color: var(--accent); }
|
||||
.encode-body .enc-status { font-size: 12px; color: var(--dim); min-width: 180px; text-align: right; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page { padding: 8px; }
|
||||
.ctrl-row svg { width: 18px; height: 18px; }
|
||||
.hours span:nth-child(even) { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<h1>C41 Archive</h1>
|
||||
<select id="date"></select>
|
||||
</div>
|
||||
|
||||
<div class="player-wrap paused" id="player-wrap">
|
||||
<video id="video" playsinline></video>
|
||||
<div class="big-play"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
|
||||
<div class="controls-bar">
|
||||
<div class="scrub" id="scrub">
|
||||
<div class="scrub-fill" id="scrub-fill"></div>
|
||||
<div class="scrub-hover" id="scrub-hover">00:00</div>
|
||||
</div>
|
||||
<div class="ctrl-row">
|
||||
<button id="btn-play" aria-label="Play">
|
||||
<svg id="icon-play" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg id="icon-pause" viewBox="0 0 24 24" style="display:none"><path d="M6 19h4V5H6zm8-14v14h4V5z"/></svg>
|
||||
</button>
|
||||
<button id="btn-back" aria-label="Back 10s"><svg viewBox="0 0 24 24"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/><text x="10" y="16" font-size="7" fill="currentColor" text-anchor="middle" font-family="sans-serif">10</text></svg></button>
|
||||
<button id="btn-fwd" aria-label="Forward 10s"><svg viewBox="0 0 24 24"><path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/><text x="14" y="16" font-size="7" fill="currentColor" text-anchor="middle" font-family="sans-serif">10</text></svg></button>
|
||||
<div class="vol-wrap">
|
||||
<button id="btn-vol" aria-label="Volume">
|
||||
<svg id="icon-vol-on" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 8.3v7.4a4.5 4.5 0 0 0 2.5-3.7zM14 3.2v2.1a7 7 0 0 1 0 13.4v2.1A9 9 0 0 0 14 3.2z"/></svg>
|
||||
<svg id="icon-vol-off" viewBox="0 0 24 24" style="display:none"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 8.3v7.4a4.5 4.5 0 0 0 2.5-3.7z"/></svg>
|
||||
</button>
|
||||
<input id="volume" type="range" min="0" max="1" step=".01" value=".8">
|
||||
</div>
|
||||
<span class="time-label"><span id="time-cur">0:00</span> / <span id="time-dur">0:00</span></span>
|
||||
<span class="spacer"></span>
|
||||
<span class="time-label" id="clock" style="color:var(--accent);font-weight:500;">--:--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-section">
|
||||
<div class="timeline-header">
|
||||
<h2>24h Timeline</h2>
|
||||
<div class="zoom-btns">
|
||||
<button id="zoom-out">-</button>
|
||||
<button id="zoom-in">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-canvas">
|
||||
<canvas id="timeline"></canvas>
|
||||
</div>
|
||||
<div class="hours" id="hours"></div>
|
||||
<div class="legend"><span class="rec">Recorded</span><span class="gap">No recording</span></div>
|
||||
</div>
|
||||
|
||||
<div class="encode-panel">
|
||||
<button class="encode-toggle" id="encode-toggle"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>Encoding</button>
|
||||
<div class="encode-body" id="encode-body">
|
||||
<button id="encode-btn">Start</button>
|
||||
<progress id="encode-progress" value="0" max="1"></progress>
|
||||
<span class="enc-status" id="encode-status">Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const video = document.querySelector("#video");
|
||||
const wrap = document.querySelector("#player-wrap");
|
||||
const dateSelect = document.querySelector("#date");
|
||||
const timeline = document.querySelector("#timeline");
|
||||
const scrub = document.querySelector("#scrub");
|
||||
const scrubFill = document.querySelector("#scrub-fill");
|
||||
const scrubHover = document.querySelector("#scrub-hover");
|
||||
let clips = [];
|
||||
let activeClip = null;
|
||||
let initialOffset = 0;
|
||||
let zoom = 1;
|
||||
let focus = 43200;
|
||||
|
||||
// encoding
|
||||
const encToggle = document.querySelector("#encode-toggle");
|
||||
encToggle.addEventListener("click", () => {
|
||||
encToggle.classList.toggle("open");
|
||||
document.querySelector("#encode-body").classList.toggle("show");
|
||||
});
|
||||
async function updateEncodeStatus() {
|
||||
const r = await fetch("/api/encode/status");
|
||||
const d = await r.json();
|
||||
const btn = document.querySelector("#encode-btn");
|
||||
const prog = document.querySelector("#encode-progress");
|
||||
btn.disabled = d.running;
|
||||
btn.textContent = d.running ? "Encoding..." : "Start";
|
||||
prog.max = d.total || 1;
|
||||
prog.value = d.completed + d.skipped + d.failed;
|
||||
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 (d.running) setTimeout(updateEncodeStatus, 1000);
|
||||
}
|
||||
document.querySelector("#encode-btn").addEventListener("click", async () => {
|
||||
await fetch("/api/encode", {method: "POST"});
|
||||
updateEncodeStatus();
|
||||
});
|
||||
|
||||
// play / pause
|
||||
function setPlaying(p) {
|
||||
wrap.classList.toggle("paused", !p);
|
||||
document.querySelector("#icon-play").style.display = p ? "none" : "";
|
||||
document.querySelector("#icon-pause").style.display = p ? "" : "none";
|
||||
}
|
||||
document.querySelector("#btn-play").addEventListener("click", () => {
|
||||
if (!activeClip) return;
|
||||
video.paused ? video.play() : video.pause();
|
||||
});
|
||||
wrap.addEventListener("click", e => {
|
||||
if (e.target.closest(".controls-bar")) return;
|
||||
if (!activeClip) return;
|
||||
video.paused ? video.play() : video.pause();
|
||||
});
|
||||
video.addEventListener("play", () => setPlaying(true));
|
||||
video.addEventListener("pause", () => setPlaying(false));
|
||||
|
||||
// volume
|
||||
const volInput = document.querySelector("#volume");
|
||||
volInput.addEventListener("input", () => { video.volume = +volInput.value; updateVolIcon(); });
|
||||
document.querySelector("#btn-vol").addEventListener("click", () => {
|
||||
video.muted = !video.muted;
|
||||
updateVolIcon();
|
||||
});
|
||||
function updateVolIcon() {
|
||||
const on = !video.muted && video.volume > 0;
|
||||
document.querySelector("#icon-vol-on").style.display = on ? "" : "none";
|
||||
document.querySelector("#icon-vol-off").style.display = on ? "none" : "";
|
||||
}
|
||||
video.volume = .8;
|
||||
|
||||
// time format
|
||||
function fmt(s) {
|
||||
s = Math.max(0, Math.floor(s));
|
||||
const h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60;
|
||||
return h ? `${h}:${String(m).padStart(2,"0")}:${String(sec).padStart(2,"0")}` : `${m}:${String(sec).padStart(2,"0")}`;
|
||||
}
|
||||
function timeOfDay(s) {
|
||||
s = Math.max(0, Math.floor(s));
|
||||
return [s/3600, s%3600/60, s%60].map(v => String(Math.floor(v)).padStart(2,"0")).join(":");
|
||||
}
|
||||
|
||||
// scrub bar
|
||||
function updateScrub() {
|
||||
if (!activeClip || !video.duration) { scrubFill.style.width = "0%"; return; }
|
||||
scrubFill.style.width = (video.currentTime / video.duration * 100) + "%";
|
||||
}
|
||||
scrub.addEventListener("click", e => {
|
||||
if (!activeClip || !video.duration) return;
|
||||
video.currentTime = (e.offsetX / scrub.clientWidth) * video.duration;
|
||||
});
|
||||
scrub.addEventListener("mousemove", e => {
|
||||
if (!activeClip || !video.duration) return;
|
||||
const t = (e.offsetX / scrub.clientWidth) * video.duration;
|
||||
scrubHover.textContent = fmt(t);
|
||||
scrubHover.style.left = e.offsetX + "px";
|
||||
});
|
||||
|
||||
// seek buttons
|
||||
function seekBy(sec) {
|
||||
if (!activeClip) return;
|
||||
const cur = activeClip.start + initialOffset + video.currentTime;
|
||||
const target = cur + sec;
|
||||
let clip = clips.find(c => target >= c.start && target < c.start + c.duration);
|
||||
if (!clip) {
|
||||
clip = sec > 0
|
||||
? clips.find(c => c.start >= target)
|
||||
: [...clips].reverse().find(c => c.start + c.duration <= target);
|
||||
if (!clip) clip = sec > 0 ? clips[clips.length - 1] : clips[0];
|
||||
}
|
||||
let offset = target - clip.start;
|
||||
if (target < clip.start) offset = 0;
|
||||
if (target >= clip.start + clip.duration) offset = clip.duration - 0.1;
|
||||
if (clip !== activeClip) return playClip(clip, Math.max(0, offset));
|
||||
video.currentTime = Math.max(0, Math.min(video.duration || clip.duration, offset));
|
||||
}
|
||||
document.querySelector("#btn-back").addEventListener("click", () => seekBy(-10));
|
||||
document.querySelector("#btn-fwd").addEventListener("click", () => seekBy(10));
|
||||
|
||||
// timeline
|
||||
function drawTimeline() {
|
||||
const ratio = devicePixelRatio || 1;
|
||||
const w = timeline.clientWidth;
|
||||
timeline.width = w * ratio;
|
||||
timeline.height = 56 * ratio;
|
||||
const ctx = timeline.getContext("2d");
|
||||
ctx.scale(ratio, ratio);
|
||||
ctx.fillStyle = "#1a1a1a";
|
||||
ctx.fillRect(0, 0, w, 56);
|
||||
const winSize = 86400 / zoom;
|
||||
const winStart = Math.max(0, Math.min(86400 - winSize, focus - winSize / 2));
|
||||
const winEnd = winStart + winSize;
|
||||
// recorded blocks
|
||||
ctx.fillStyle = "#3ea6ff";
|
||||
const rec = new Uint8Array(1440);
|
||||
clips.forEach(c => {
|
||||
const a = Math.floor(c.start / 60), b = Math.min(1439, Math.floor((c.start + c.duration - .001) / 60));
|
||||
for (let i = a; i <= b; i++) rec[i] = 1;
|
||||
});
|
||||
rec.forEach((v, i) => {
|
||||
if (!v) return;
|
||||
const l = (i * 60 - winStart) / winSize * w;
|
||||
const r = ((i + 1) * 60 - 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
|
||||
ctx.strokeStyle = "rgba(255,255,255,.08)";
|
||||
for (let h = Math.ceil(winStart / 3600); h < winEnd / 3600; h++) {
|
||||
const x = (h * 3600 - winStart) / winSize * w;
|
||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 56); ctx.stroke();
|
||||
}
|
||||
// playhead
|
||||
if (activeClip) {
|
||||
const cur = activeClip.start + initialOffset + (video.currentTime || 0);
|
||||
const x = (cur - winStart) / winSize * w;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(x - 1.5, 0, 3, 56);
|
||||
}
|
||||
// labels
|
||||
const step = zoom >= 8 ? 1 : zoom >= 2 ? 2 : 4;
|
||||
const first = Math.ceil(winStart / 3600 / step) * step;
|
||||
document.querySelector("#hours").innerHTML = Array.from({length: 25}, (_, i) => first + i * step)
|
||||
.filter(h => h <= winEnd / 3600).map(h => `<span>${timeOfDay(h * 3600)}</span>`).join("");
|
||||
}
|
||||
|
||||
timeline.addEventListener("click", e => {
|
||||
const winSize = 86400 / zoom;
|
||||
const winStart = Math.max(0, Math.min(86400 - winSize, focus - winSize / 2));
|
||||
const sec = winStart + e.offsetX / timeline.clientWidth * winSize;
|
||||
focus = sec;
|
||||
const clip = clips.find(c => sec >= c.start && sec < c.start + c.duration)
|
||||
|| clips.find(c => c.start >= sec) || clips[clips.length - 1];
|
||||
if (clip) playClip(clip, Math.max(0, Math.min(clip.duration - .1, sec - clip.start)));
|
||||
});
|
||||
timeline.addEventListener("mousemove", e => {
|
||||
const winSize = 86400 / zoom;
|
||||
const winStart = Math.max(0, Math.min(86400 - winSize, focus - winSize / 2));
|
||||
timeline.title = timeOfDay(winStart + e.offsetX / timeline.clientWidth * winSize);
|
||||
});
|
||||
document.querySelector("#zoom-in").addEventListener("click", () => { zoom = Math.min(24, zoom * 2); drawTimeline(); });
|
||||
document.querySelector("#zoom-out").addEventListener("click", () => { zoom = Math.max(1, zoom / 2); drawTimeline(); });
|
||||
|
||||
// load date
|
||||
async function loadDate(date) {
|
||||
video.pause(); video.removeAttribute("src"); activeClip = null; setPlaying(false);
|
||||
const r = await fetch(`/api/recordings?date=${encodeURIComponent(date)}`);
|
||||
const d = await r.json();
|
||||
clips = d.recordings;
|
||||
dateSelect.innerHTML = d.dates.map(day => `<option value="${day}">${day.slice(0,4)}-${day.slice(4,6)}-${day.slice(6)}</option>`).join("");
|
||||
dateSelect.value = date || d.dates[0];
|
||||
document.querySelector("#clock").textContent = "--:--:--";
|
||||
document.querySelector("#time-cur").textContent = "0:00";
|
||||
document.querySelector("#time-dur").textContent = "0:00";
|
||||
scrubFill.style.width = "0%";
|
||||
drawTimeline();
|
||||
if (!date && d.dates[0]) loadDate(d.dates[0]);
|
||||
}
|
||||
|
||||
function playClip(clip, offset = 0) {
|
||||
activeClip = clip;
|
||||
initialOffset = offset;
|
||||
document.querySelector("#clock").textContent = timeOfDay(clip.start + offset);
|
||||
video.src = `/video?date=${dateSelect.value}&name=${encodeURIComponent(clip.name)}&offset=${offset.toFixed(3)}`;
|
||||
video.play().catch(() => {});
|
||||
drawTimeline();
|
||||
}
|
||||
|
||||
video.addEventListener("timeupdate", () => {
|
||||
if (!activeClip) return;
|
||||
document.querySelector("#clock").textContent = timeOfDay(activeClip.start + initialOffset + video.currentTime);
|
||||
document.querySelector("#time-cur").textContent = fmt(video.currentTime);
|
||||
updateScrub();
|
||||
drawTimeline();
|
||||
});
|
||||
video.addEventListener("loadedmetadata", () => {
|
||||
document.querySelector("#time-dur").textContent = fmt(video.duration);
|
||||
if (initialOffset > 0 && video.currentTime < initialOffset) video.currentTime = initialOffset;
|
||||
});
|
||||
video.addEventListener("ended", () => {
|
||||
const i = clips.indexOf(activeClip);
|
||||
if (i >= 0 && clips[i + 1]) playClip(clips[i + 1]);
|
||||
});
|
||||
// keyboard
|
||||
document.addEventListener("keydown", e => {
|
||||
if (e.target.tagName === "SELECT" || e.target.tagName === "INPUT") return;
|
||||
if (e.code === "ArrowLeft") { e.preventDefault(); seekBy(-10); }
|
||||
else if (e.code === "ArrowRight") { e.preventDefault(); seekBy(10); }
|
||||
else if (e.code === "Space") { e.preventDefault(); if (activeClip) video.paused ? video.play() : video.pause(); }
|
||||
else if (e.code === "KeyM") { video.muted = !video.muted; updateVolIcon(); }
|
||||
});
|
||||
|
||||
dateSelect.addEventListener("change", () => loadDate(dateSelect.value));
|
||||
addEventListener("resize", drawTimeline);
|
||||
loadDate("").catch(() => {});
|
||||
updateEncodeStatus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+504
@@ -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
|
||||
----------------------------------------
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user