Ahhh Controls go BRRR

This commit is contained in:
dirwin31
2026-08-27 13:25:57 -07:00
parent b619371441
commit f85bfb277b
7 changed files with 593 additions and 141 deletions
@@ -1239,19 +1239,49 @@
}
.dashcam-player-state[hidden],
.dashcam-segment-status[hidden],
.dashcam-segment-bar[hidden],
.dashcam-camera-selector button[hidden] {
display: none;
}
.dashcam-segment-status {
.dashcam-segment-bar {
align-items: center;
background: var(--sidebar-bg);
border-bottom: var(--border-width-thin) solid var(--sidebar-border-color);
color: var(--text-muted);
font-size: var(--font-size-sm);
display: flex;
gap: var(--gap-xs);
padding: 0.65rem var(--padding-lg);
}
.dashcam-segment-bar .segment-step {
align-items: center;
background: var(--input-bg);
border: var(--border-width-thin) solid var(--sidebar-border-color);
border-radius: var(--border-radius-md);
color: var(--text-color);
cursor: pointer;
display: flex;
font-size: var(--font-size-base);
justify-content: center;
padding: 0.35rem 0.7rem;
}
.dashcam-segment-bar .segment-step:disabled {
cursor: default;
opacity: 0.4;
}
.dashcam-segment-bar .segment-select {
background: var(--input-bg);
border: var(--border-width-thin) solid var(--sidebar-border-color);
border-radius: var(--border-radius-md);
color: var(--text-color);
cursor: pointer;
font-size: var(--font-size-sm);
padding: 0.35rem 0.6rem;
}
.dashcam-camera-selector {
gap: var(--gap-xs);
padding: var(--padding-base) var(--padding-lg) 0;
@@ -1355,7 +1385,7 @@
}
.dashcam-player-header,
.dashcam-segment-status,
.dashcam-segment-bar,
.dashcam-camera-selector,
.dashcam-player-actions {
padding-left: var(--padding-base);
@@ -6,8 +6,9 @@ import {
cameraVideoUrl,
computeRouteStats,
formatApproxDuration,
getSegmentStatus,
groupRoutesByDate,
getSegmentOptions,
supportsLowQuality,
groupRoutesForView,
MAX_RENDERED_ROUTES,
normalizeRoute,
} from "/assets/components/recordings/dashcam_routes_helpers.js"
@@ -32,6 +33,10 @@ let routesRequestToken = 0
let seenRouteNames = new Set()
let overlay = null
const routeLogsCache = new Map()
const FULL_QUALITY_RETRIES = 3
const FULL_QUALITY_RETRY_MS = 4000
// Wait for the viewer to settle, so scrubbing never queues a remux per segment.
const FULL_QUALITY_SETTLE_MS = 1500
function routeLabel(route) {
return route.displayName || route.displayDate || route.name
@@ -119,8 +124,11 @@ function closeDialog(dialog) {
}
function replaceRoute(updatedRoute) {
state.routes = state.routes.map(route => route.name === updatedRoute.name ? updatedRoute : route)
if (state.selectedRoute?.name === updatedRoute.name) state.selectedRoute = updatedRoute
// Rows bind to this exact object, so replacing it would strand them on the stale one.
const existing = state.routes.find(route => route.name === updatedRoute.name)
if (existing) Object.assign(existing, updatedRoute)
const selected = state.selectedRoute
if (selected?.name === updatedRoute.name && selected !== existing) Object.assign(selected, updatedRoute)
}
async function deleteRoute(route) {
@@ -285,10 +293,14 @@ async function openOverlay(route) {
<button class="dashcam-player-close action-close" type="button" aria-label="Close player">&times;</button>
</header>
<div class="dashcam-video-shell">
<video controls muted playsinline></video>
<video controls muted playsinline preload="metadata"></video>
<div class="dashcam-player-state" role="status">Loading route metadata&hellip;</div>
</div>
<div class="dashcam-segment-status" aria-live="polite" hidden></div>
<div class="dashcam-segment-bar" hidden>
<button class="segment-step action-prev-segment" type="button" title="Previous segment (Shift + \u2190)" aria-label="Previous segment"><i class="bi bi-skip-start-fill"></i></button>
<select class="segment-select" aria-label="Jump to segment"></select>
<button class="segment-step action-next-segment" type="button" title="Next segment (Shift + \u2192)" aria-label="Next segment"><i class="bi bi-skip-end-fill"></i></button>
</div>
<div class="dashcam-camera-selector" aria-label="Camera selector">
<button class="camera-button" data-camera="forward" type="button" disabled hidden>Forward</button>
<button class="camera-button" data-camera="wide" type="button" disabled hidden>Wide</button>
@@ -305,7 +317,10 @@ async function openOverlay(route) {
const video = overlay.querySelector("video")
const playerState = overlay.querySelector(".dashcam-player-state")
const statusStrip = overlay.querySelector(".dashcam-segment-status")
const segmentBar = overlay.querySelector(".dashcam-segment-bar")
const segmentSelect = overlay.querySelector(".segment-select")
const prevSegmentButton = overlay.querySelector(".action-prev-segment")
const nextSegmentButton = overlay.querySelector(".action-next-segment")
const downloadButton = overlay.querySelector(".action-download")
const logsButton = overlay.querySelector(".action-logs")
const cameraButtons = [...overlay.querySelectorAll(".camera-button")]
@@ -313,27 +328,130 @@ async function openOverlay(route) {
let current = 0
let selectedCamera = null
let logsData = null
let qualityToken = 0
let warmedSegment = null
let upgradeTimer = null
const setPlayerMessage = (message, isError = false) => {
playerState.textContent = message
playerState.hidden = !message
playerState.classList.toggle("error", isError)
}
const updateSegmentStatus = () => {
const status = getSegmentStatus(segments, current)
statusStrip.textContent = status
statusStrip.hidden = !status
const syncSegmentControls = () => {
segmentSelect.value = String(current)
segmentSelect.disabled = segments.length < 2
prevSegmentButton.disabled = current <= 0
nextSegmentButton.disabled = current >= segments.length - 1
}
const playCurrentSegment = () => {
if (!segments[current] || !selectedCamera) return
updateSegmentStatus()
setPlayerMessage("Loading video…")
video.src = cameraVideoUrl(segments[current], selectedCamera)
const buildSegmentPicker = () => {
segmentSelect.innerHTML = getSegmentOptions(segments)
.map(option => `<option value="${option.index}">${escapeHtml(option.label)}</option>`)
.join("")
segmentBar.hidden = !segments.length
}
const swapSource = (url, { message } = {}) => {
const playbackTime = Number.isFinite(video.currentTime) ? video.currentTime : 0
const shouldResume = !video.paused && !video.ended
video.addEventListener("loadedmetadata", () => {
if (playbackTime > 0) {
try {
video.currentTime = Math.min(playbackTime, Number.isFinite(video.duration) ? video.duration : playbackTime)
} catch (_) {}
}
if (shouldResume) video.play().catch(() => {})
}, { once: true })
if (message) setPlayerMessage(message)
video.src = url
video.load()
video.play().catch(() => {})
}
// Full-res needs a device-side remux, so wait for it behind playback rather than in front.
const requestFullQuality = (segmentUrl, camera, attempt = 0) => {
const token = ++qualityToken
const fullUrl = cameraVideoUrl(segmentUrl, camera)
const stillCurrent = () =>
token === qualityToken && segments[current] === segmentUrl && selectedCamera === camera && !!overlay
fetch(fullUrl, { method: "HEAD" })
.then(response => {
if (!stillCurrent()) return
if (response.ok) {
swapSource(fullUrl)
return
}
// 503 means the remux is queued behind another one; check back a few times.
if (response.status === 503 && attempt < FULL_QUALITY_RETRIES) {
setTimeout(() => {
if (stillCurrent()) requestFullQuality(segmentUrl, camera, attempt + 1)
}, FULL_QUALITY_RETRY_MS)
}
})
.catch(() => {})
}
overlay._cancelUpgrade = () => {
qualityToken += 1
clearTimeout(upgradeTimer)
}
const upgradeToFullQuality = (segmentUrl, camera) => {
qualityToken += 1
clearTimeout(upgradeTimer)
upgradeTimer = setTimeout(() => {
if (segments[current] === segmentUrl && selectedCamera === camera && overlay) {
requestFullQuality(segmentUrl, camera)
}
}, FULL_QUALITY_SETTLE_MS)
}
const warmNextSegment = () => {
const nextUrl = segments[current + 1]
if (!nextUrl || !selectedCamera || !supportsLowQuality(selectedCamera)) return
if (warmedSegment === nextUrl) return
warmedSegment = nextUrl
// Only the ffmpeg-free stream is warmed; never transcode a segment nobody watches.
fetch(cameraVideoUrl(nextUrl, selectedCamera, "low"), { method: "HEAD" }).catch(() => {})
}
const playCurrentSegment = (autoplay = true) => {
if (!segments[current] || !selectedCamera) return
syncSegmentControls()
setPlayerMessage("Loading video…")
const segmentUrl = segments[current]
const camera = selectedCamera
const useLowFirst = supportsLowQuality(camera)
qualityToken += 1
video.src = cameraVideoUrl(segmentUrl, camera, useLowFirst ? "low" : undefined)
video.load()
if (autoplay) video.play().catch(() => {})
if (useLowFirst) upgradeToFullQuality(segmentUrl, camera)
warmNextSegment()
}
const goToSegment = index => {
if (!segments.length) return
const target = Math.min(Math.max(index, 0), segments.length - 1)
if (target === current) {
syncSegmentControls()
return
}
const keepPlaying = video.ended || (!video.paused && !video.error)
current = target
warmedSegment = null
playCurrentSegment(keepPlaying)
}
const closeOnEscape = event => {
if (event.key === "Escape" && !document.querySelector(".route-logs-dialog")) closeOverlay()
if (document.querySelector(".route-logs-dialog")) return
if (event.key === "Escape") {
closeOverlay()
return
}
if (!event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return
if (event.key === "ArrowLeft") {
event.preventDefault()
goToSegment(current - 1)
} else if (event.key === "ArrowRight") {
event.preventDefault()
goToSegment(current + 1)
}
}
overlay.addEventListener("click", event => { if (event.target === overlay) closeOverlay() })
@@ -358,30 +476,23 @@ async function openOverlay(route) {
video.addEventListener("playing", () => setPlayerMessage(""))
video.addEventListener("waiting", () => setPlayerMessage("Loading video…"))
video.addEventListener("error", () => setPlayerMessage("This segment could not be played.", true))
video.addEventListener("ended", () => {
if (current + 1 >= segments.length) return
current += 1
playCurrentSegment()
})
video.addEventListener("ended", () => goToSegment(current + 1))
prevSegmentButton.onclick = () => goToSegment(current - 1)
nextSegmentButton.onclick = () => goToSegment(current + 1)
segmentSelect.onchange = () => goToSegment(Number(segmentSelect.value))
for (const button of cameraButtons) {
button.addEventListener("click", () => {
if (button.disabled || button.dataset.camera === selectedCamera || !segments[current]) return
const playbackTime = Number.isFinite(video.currentTime) ? video.currentTime : 0
const shouldResume = !video.paused && !video.ended
selectedCamera = button.dataset.camera
cameraButtons.forEach(candidate => candidate.classList.toggle("active", candidate === button))
video.addEventListener("loadedmetadata", () => {
if (playbackTime > 0) {
try {
video.currentTime = Math.min(playbackTime, Number.isFinite(video.duration) ? video.duration : playbackTime)
} catch (_) {}
}
if (shouldResume) video.play().catch(() => {})
}, { once: true })
setPlayerMessage("Switching camera…")
video.src = cameraVideoUrl(segments[current], selectedCamera)
video.load()
const segmentUrl = segments[current]
const camera = selectedCamera
const useLowFirst = supportsLowQuality(camera)
qualityToken += 1
swapSource(cameraVideoUrl(segmentUrl, camera, useLowFirst ? "low" : undefined), { message: "Switching camera…" })
if (useLowFirst) upgradeToFullQuality(segmentUrl, camera)
})
}
@@ -402,6 +513,7 @@ async function openOverlay(route) {
button.classList.toggle("active", button.dataset.camera === selectedCamera)
}
downloadButton.disabled = false
buildSegmentPicker()
playCurrentSegment()
} catch (error) {
cameraButtons.forEach(button => { button.disabled = true })
@@ -411,6 +523,7 @@ async function openOverlay(route) {
function closeOverlay() {
if (!overlay) return
overlay._cancelUpgrade?.()
document.removeEventListener("keydown", overlay._closeOnEscape)
overlay.remove()
overlay = null
@@ -521,8 +634,7 @@ export function RouteRecordings() {
${() => {
const view = buildRouteView(state.routes, { preservedOnly: state.showPreservedOnly, searchQuery: state.searchQuery, sortOrder: state.sortOrder })
const groups = groupRoutesByDate(view.visible)
const isGrid = state.viewMode === "grid"
const groups = groupRoutesForView(view.visible, state.sortOrder)
return html`
<div class="dashcam-results-summary" aria-live="polite">
@@ -540,13 +652,12 @@ export function RouteRecordings() {
<h2>${group.label}</h2>
<span class="dashcam-date-group-count">${group.routes.length} ${group.routes.length === 1 ? "drive" : "drives"}</span>
</div>
${isGrid ? html`
<div class="screen-recordings-grid dashcam-routes-grid">
${() => state.viewMode === "grid" ? html`<div class="screen-recordings-grid dashcam-routes-grid">
${group.routes.map(route => html`
<article class="recording-card dashcam-route-card ${route.is_preserved ? "is-preserved" : ""}" @click="${() => { state.selectedRoute = route }}">
<article class="${() => `recording-card dashcam-route-card ${route.is_preserved ? "is-preserved" : ""}`}" @click="${() => { state.selectedRoute = route }}">
<div class="dashcam-card-top-bar">
<button class="btn-route-action btn-preserve ${route.is_preserved ? "active" : ""}" type="button" aria-label="${route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${route.is_preserved ? "Preserved (click to unpreserve)" : "Click to preserve"}" @click="${event => togglePreserved(route, event)}">
<i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i>
<button class="${() => `btn-route-action btn-preserve ${route.is_preserved ? "active" : ""}`}" type="button" aria-label="${() => route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${() => route.is_preserved ? "Preserved (click to unpreserve)" : "Click to preserve"}" @click="${event => togglePreserved(route, event)}">
<i class="${() => `bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}`}"></i>
</button>
<span class="meta-pill id-pill" title="Route ID: ${route.name}"><i class="bi bi-hash"></i>${route.name.split("--").slice(1).join("--") || route.name}</span>
</div>
@@ -581,11 +692,9 @@ export function RouteRecordings() {
</div>
</div>
</article>`)}
</div>
` : html`
<div class="dashcam-routes-list">
</div>` : html`<div class="dashcam-routes-list">
${group.routes.map(route => html`
<article class="dashcam-route-row ${route.is_preserved ? "is-preserved" : ""}" @click="${() => { state.selectedRoute = route }}">
<article class="${() => `dashcam-route-row ${route.is_preserved ? "is-preserved" : ""}`}" @click="${() => { state.selectedRoute = route }}">
<div class="dashcam-mini-preview">
<span class="dashcam-mini-fallback"><i class="bi bi-camera-video"></i></span>
<img src="${route.png}" class="dashcam-mini-img" loading="lazy" alt="" @error="${thumbnailFailed}">
@@ -600,7 +709,7 @@ export function RouteRecordings() {
<div class="dashcam-route-meta-pills">
<span class="meta-pill duration-pill" title="Estimated duration"><i class="bi bi-clock"></i> ${formatApproxDuration(route.approxDurationSeconds)}</span>
<span class="meta-pill segments-pill" title="Segments recorded"><i class="bi bi-collection-play"></i> ${route.segmentCount} segment${route.segmentCount === 1 ? "" : "s"}</span>
${route.is_preserved ? html`<span class="meta-pill preserved-pill" title="Preserved from deletion"><i class="bi bi-heart-fill"></i> Preserved</span>` : ""}
${() => route.is_preserved ? html`<span class="meta-pill preserved-pill" title="Preserved from deletion"><i class="bi bi-heart-fill"></i> Preserved</span>` : ""}
<span class="meta-pill id-pill" title="Route ID: ${route.name}"><i class="bi bi-hash"></i>${route.name.split("--").slice(1).join("--") || route.name}</span>
</div>
</div>
@@ -608,8 +717,8 @@ export function RouteRecordings() {
<button class="btn-route-action btn-play" type="button" title="Play recording" @click="${() => { state.selectedRoute = route }}">
<i class="bi bi-play-fill"></i> <span>Play</span>
</button>
<button class="btn-route-action btn-preserve ${route.is_preserved ? "active" : ""}" type="button" aria-label="${route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${route.is_preserved ? "Preserved (click to unpreserve)" : "Click to preserve"}" @click="${event => togglePreserved(route, event)}">
<i class="bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}"></i>
<button class="${() => `btn-route-action btn-preserve ${route.is_preserved ? "active" : ""}`}" type="button" aria-label="${() => route.is_preserved ? "Remove preservation" : "Preserve route"}" title="${() => route.is_preserved ? "Preserved (click to unpreserve)" : "Click to preserve"}" @click="${event => togglePreserved(route, event)}">
<i class="${() => `bi ${route.is_preserved ? "bi-heart-fill" : "bi-heart"}`}"></i>
</button>
<button class="btn-route-action btn-icon" type="button" title="View & download logs" @click="${event => openLogsFromRow(route, event)}">
<i class="bi bi-file-earmark-arrow-down"></i>
@@ -147,6 +147,16 @@ export function groupRoutesByDate(routes, now = new Date(), locale) {
return groups
}
export function groupRoutesForView(routes, sortOrder = "newest", now, locale) {
// Date headers would scramble a duration sort, so show one flat group instead.
if (sortOrder === "longest" || sortOrder === "shortest") {
const list = Array.isArray(routes) ? [...routes] : []
if (!list.length) return []
return [{ key: sortOrder, label: sortOrder === "longest" ? "Longest first" : "Shortest first", routes: list }]
}
return groupRoutesByDate(routes, now, locale)
}
export function formatApproxDuration(seconds) {
const minutes = Math.max(0, Math.round(Number(seconds) / 60) || 0)
if (minutes < 1) return "Less than 1 min"
@@ -168,10 +178,24 @@ export function getSegmentStatus(segmentUrls, playbackIndex) {
if (!Array.isArray(segmentUrls) || !Number.isInteger(playbackIndex) || playbackIndex < 0 || playbackIndex >= segmentUrls.length) return ""
const segmentNumber = parseStoredSegmentNumber(segmentUrls[playbackIndex])
if (segmentNumber == null) return ""
return `Segment ${segmentNumber} · ${playbackIndex + 1} of ${segmentUrls.length}`
return `Segment ${segmentNumber}`
}
export function cameraVideoUrl(segmentUrl, camera) {
export function getSegmentOptions(segmentUrls) {
if (!Array.isArray(segmentUrls)) return []
return segmentUrls.map((_, index) => ({
index,
label: getSegmentStatus(segmentUrls, index) || `Clip ${index + 1}`,
}))
}
export function cameraVideoUrl(segmentUrl, camera, quality) {
const separator = String(segmentUrl).includes("?") ? "&" : "?"
return `${segmentUrl}${separator}camera=${encodeURIComponent(camera)}`
const url = `${segmentUrl}${separator}camera=${encodeURIComponent(camera)}`
return quality ? `${url}&quality=${encodeURIComponent(quality)}` : url
}
// qcamera.ts only exists for the road camera, so the instant-start tier is forward-only.
export function supportsLowQuality(camera) {
return camera === "forward"
}
@@ -1,6 +1,7 @@
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
import io
import os
from pathlib import Path
import threading
import time
@@ -391,19 +392,64 @@ def test_preserve_limit_counts_routes_not_segments(monkeypatch, tmp_path):
assert client.post("/api/routes/00000099--9f0a7bdf9c/preserve").status_code == 400
def test_video_cache_evicts_oldest_instead_of_wiping_everything(monkeypatch, tmp_path):
"""A tight disk used to delete every cached mp4, so playback re-muxed on every request."""
cache = tmp_path / "video_cache"
cache.mkdir()
monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
monkeypatch.setattr(utilities, "VIDEO_CACHE_MAX_BYTES", 300)
for index in range(4):
entry = cache / f"{index}.mp4"
entry.write_bytes(b"x" * 100)
os.utime(entry, (1000 + index, 1000 + index))
utilities._prune_video_cache()
survivors = sorted(path.name for path in cache.glob("*.mp4"))
# Budget is 300 bytes of 400 used, so only the oldest goes.
assert survivors == ["1.mp4", "2.mp4", "3.mp4"]
def test_video_cache_never_evicts_the_entry_being_written(monkeypatch, tmp_path):
cache = tmp_path / "video_cache"
cache.mkdir()
monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
monkeypatch.setattr(utilities, "VIDEO_CACHE_MAX_BYTES", 50)
for index in range(3):
entry = cache / f"{index}.mp4"
entry.write_bytes(b"x" * 100)
os.utime(entry, (1000 + index, 1000 + index))
keep = cache / "0.mp4"
utilities._prune_video_cache(keep_path=keep)
assert keep.exists()
def _stub_remux(monkeypatch, tmp_path, payload=b"wrapped-video"):
"""Stand in for the ffmpeg remux, returning a real file so send_file can stream it."""
wrapped = tmp_path / "wrapped.mp4"
wrapped.write_bytes(payload)
monkeypatch.setattr(utilities, "ffmpeg_mp4_wrap_to_path", lambda path: wrapped)
return wrapped
def test_sparse_route_metadata_and_video_downloads(monkeypatch, tmp_path):
segments = [_make_segment(tmp_path, segment_num=number) for number in (0, 3, 11)]
for segment in segments:
(segment / "fcamera.hevc").write_bytes(b"hevc")
monkeypatch.setattr(utilities, "get_video_duration", lambda path: 60)
monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
monkeypatch.setattr(utilities, "ffmpeg_mp4_wrap_process_builder", lambda path: io.BytesIO(b"wrapped-video"))
_stub_remux(monkeypatch, tmp_path)
monkeypatch.setattr(utilities, "ffmpeg_concat_segments_to_mp4", lambda paths, cache_key=None: io.BytesIO(b"combined-video"))
client = _make_client(monkeypatch, tmp_path)
metadata = client.get(f"/api/routes/{ROUTE_NAME}")
assert metadata.status_code == 200
assert metadata.get_json()["segment_urls"] == [f"/video/{ROUTE_NAME}--{number}" for number in (0, 3, 11)]
# One minute per segment, without probing each one with ffprobe.
assert metadata.get_json()["total_duration"] == 180
segment_video = client.get(f"/video/{ROUTE_NAME}--3?camera=forward")
assert segment_video.status_code == 200
@@ -414,3 +460,99 @@ def test_sparse_route_metadata_and_video_downloads(monkeypatch, tmp_path):
assert combined_video.status_code == 200
assert combined_video.mimetype == "video/mp4"
assert combined_video.data == b"combined-video"
def test_route_metadata_never_probes_segments_with_ffprobe(monkeypatch, tmp_path):
"""Probing each segment put one subprocess per segment in front of playback."""
for number in (0, 1, 2):
segment = _make_segment(tmp_path, segment_num=number)
(segment / "fcamera.hevc").write_bytes(b"hevc")
def explode(path):
raise AssertionError(f"ffprobe must stay off the route metadata path: {path}")
monkeypatch.setattr(utilities, "get_video_duration", explode)
monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
client = _make_client(monkeypatch, tmp_path)
metadata = client.get(f"/api/routes/{ROUTE_NAME}")
assert metadata.status_code == 200
assert metadata.get_json()["total_duration"] == 180
def test_low_quality_serves_qcamera_without_touching_ffmpeg(monkeypatch, tmp_path):
segment = _make_segment(tmp_path, segment_num=0)
(segment / "fcamera.hevc").write_bytes(b"hevc")
(segment / "qcamera.ts").write_bytes(b"qcamera-bytes")
def explode(path):
raise AssertionError(f"the low quality tier must not remux: {path}")
monkeypatch.setattr(utilities, "ffmpeg_mp4_wrap_to_path", explode)
client = _make_client(monkeypatch, tmp_path)
low = client.get(f"/video/{ROUTE_NAME}--0?camera=forward&quality=low")
assert low.status_code == 200
assert low.data == b"qcamera-bytes"
def test_low_quality_falls_back_when_qcamera_is_missing(monkeypatch, tmp_path):
segment = _make_segment(tmp_path, segment_num=0)
(segment / "fcamera.hevc").write_bytes(b"hevc")
_stub_remux(monkeypatch, tmp_path)
client = _make_client(monkeypatch, tmp_path)
# No qcamera.ts on disk, and the driver camera never has one.
assert client.get(f"/video/{ROUTE_NAME}--0?camera=forward&quality=low").status_code == 404
assert client.get(f"/video/{ROUTE_NAME}--0?camera=driver&quality=low").status_code == 404
full = client.get(f"/video/{ROUTE_NAME}--0?camera=forward")
assert full.status_code == 200
assert full.data == b"wrapped-video"
def test_segment_video_supports_range_requests(monkeypatch, tmp_path):
segment = _make_segment(tmp_path, segment_num=0)
(segment / "fcamera.hevc").write_bytes(b"hevc")
_stub_remux(monkeypatch, tmp_path, payload=b"0123456789")
client = _make_client(monkeypatch, tmp_path)
partial = client.get(f"/video/{ROUTE_NAME}--0?camera=forward", headers={"Range": "bytes=2-5"})
assert partial.status_code == 206
assert partial.data == b"2345"
assert partial.headers["Content-Range"] == "bytes 2-5/10"
# A malformed range used to raise inside the hand-rolled parser.
assert client.get(f"/video/{ROUTE_NAME}--0?camera=forward", headers={"Range": "bytes=abc"}).status_code in (200, 416)
def test_concurrent_requests_for_one_segment_share_a_single_remux(monkeypatch, tmp_path):
segment = _make_segment(tmp_path, segment_num=0)
(segment / "fcamera.hevc").write_bytes(b"hevc")
wrapped = tmp_path / "wrapped.mp4"
wrapped.write_bytes(b"wrapped-video")
calls = []
started = threading.Event()
def slow_remux(path):
calls.append(path)
started.set()
time.sleep(0.3)
return wrapped
monkeypatch.setattr(utilities, "ffmpeg_mp4_wrap_to_path", slow_remux)
client = _make_client(monkeypatch, tmp_path)
results = []
def fetch():
results.append(client.get(f"/video/{ROUTE_NAME}--0?camera=forward").status_code)
threads = [threading.Thread(target=fetch) for _ in range(4)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert results == [200, 200, 200, 200]
assert len(calls) == 1
@@ -143,7 +143,45 @@ def test_filters_preserved_routes_before_applying_the_render_limit():
assert view["allPreserved"] is True
def test_segment_status_uses_stored_sparse_numbers_and_playback_position():
def test_duration_sorts_render_as_one_flat_group():
result = evaluate("""
const routes = [
{ name: "a", _startedAtMs: Date.parse("2026-08-20T10:00:00Z"), approxDurationSeconds: 3000 },
{ name: "b", _startedAtMs: Date.parse("2026-08-22T10:00:00Z"), approxDurationSeconds: 2700 },
{ name: "c", _startedAtMs: Date.parse("2026-08-21T10:00:00Z"), approxDurationSeconds: 1800 },
]
const view = buildRouteView(routes, { sortOrder: "longest" })
const groups = groupRoutesForView(view.visible, "longest", new Date("2026-08-27T12:00:00Z"), "en-US")
return { labels: groups.map(g => g.label), order: groups.flatMap(g => g.routes.map(r => r.name)) }
""")
assert result["labels"] == ["Longest first"]
assert result["order"] == ["a", "b", "c"]
def test_date_sorts_still_group_by_day():
labels = evaluate("""
const routes = [
{ name: "a", _startedAtMs: Date.parse("2026-08-20T10:00:00Z"), approxDurationSeconds: 3000 },
{ name: "b", _startedAtMs: Date.parse("2026-08-22T10:00:00Z"), approxDurationSeconds: 2700 },
]
const view = buildRouteView(routes, { sortOrder: "newest" })
return groupRoutesForView(view.visible, "newest", new Date("2026-08-27T12:00:00Z"), "en-US").map(g => g.label)
""")
assert labels == ["August 22, 2026", "August 20, 2026"]
def test_grouping_an_empty_list_yields_no_groups():
assert evaluate("""
return [
groupRoutesForView([], "longest").length,
groupRoutesForView([], "newest").length,
]
""") == [0, 0]
def test_segment_status_reports_the_stored_segment_number():
statuses = evaluate('''
const segments = [
"/video/0000006a--9f0a7bdf9c--0",
@@ -153,13 +191,29 @@ def test_segment_status_uses_stored_sparse_numbers_and_playback_position():
return segments.map((_, index) => getSegmentStatus(segments, index))
''')
assert statuses == [
"Segment 0 · 1 of 3",
"Segment 3 · 2 of 3",
"Segment 11 · 3 of 3",
assert statuses == ["Segment 0", "Segment 3", "Segment 11"]
def test_segment_options_label_every_clip_for_the_jump_picker():
options = evaluate("""
return getSegmentOptions([
"/video/0000006a--9f0a7bdf9c--12",
"/video/0000006a--9f0a7bdf9c--13",
"/video/not-a-segment",
])
""")
assert options == [
{"index": 0, "label": "Segment 12"},
{"index": 1, "label": "Segment 13"},
{"index": 2, "label": "Clip 3"},
]
def test_segment_options_tolerate_a_missing_segment_list():
assert evaluate("return [getSegmentOptions(undefined), getSegmentOptions([])]") == [[], []]
def test_hides_segment_status_when_the_stored_number_is_unsafe():
results = evaluate('''
return [
@@ -172,6 +226,28 @@ def test_hides_segment_status_when_the_stored_number_is_unsafe():
assert results == [None, "", ""]
def test_camera_video_url_carries_an_optional_quality_tier():
result = evaluate("""
const segment = "/video/0000006a--9f0a7bdf9c--7"
return {
full: cameraVideoUrl(segment, "forward"),
low: cameraVideoUrl(segment, "forward", "low"),
lowWide: cameraVideoUrl(segment, "wide", "low"),
}
""")
assert result["full"] == "/video/0000006a--9f0a7bdf9c--7?camera=forward"
assert result["low"] == "/video/0000006a--9f0a7bdf9c--7?camera=forward&quality=low"
assert result["lowWide"] == "/video/0000006a--9f0a7bdf9c--7?camera=wide&quality=low"
def test_only_the_road_camera_has_a_low_quality_tier():
"""loggerd writes qcamera.ts alongside the road camera only."""
assert evaluate("""
return ["forward", "wide", "driver"].map(supportsLowQuality)
""") == [True, False, False]
def test_switching_camera_changes_only_the_url_and_not_segment_status():
result = evaluate('''
const segments = ["/video/0000006a--9f0a7bdf9c--7"]
+75 -59
View File
@@ -1223,6 +1223,15 @@ ROUTE_THUMBNAIL_CACHE_SECONDS = 7 * 24 * 60 * 60
# Browsers only allow a handful of connections per origin, so a request must never
# park on the preview queue: give up and let the card fall back, the job keeps running.
ROUTE_THUMBNAIL_WAIT_SECONDS = 25
# One minute per segment, matching loggerd's segment length.
SEGMENT_DURATION_SECONDS = 60
# Only ever remux one segment at a time; the driving stack needs the headroom.
VIDEO_REMUX_WAIT_SECONDS = 25
# Segment media never changes once loggerd has closed it, so let the browser keep it.
VIDEO_CACHE_SECONDS = 7 * 24 * 60 * 60
_VIDEO_REMUX_EXECUTOR = ThreadPoolExecutor(max_workers=1, thread_name_prefix="video-remux")
_VIDEO_REMUX_FUTURES = {}
_VIDEO_REMUX_LOCK = threading.Lock()
_ROUTE_THUMBNAIL_EXECUTOR = ThreadPoolExecutor(max_workers=2, thread_name_prefix="route-thumbnail")
_ROUTE_THUMBNAIL_FUTURES = {}
_ROUTE_THUMBNAIL_LOCK = threading.Lock()
@@ -1324,6 +1333,36 @@ def _generate_route_thumbnail(preview_path):
return None
def _remove_video_remux_future(key, future):
with _VIDEO_REMUX_LOCK:
if _VIDEO_REMUX_FUTURES.get(key) is future:
_VIDEO_REMUX_FUTURES.pop(key, None)
def _get_or_create_segment_mp4(source_path):
"""Remuxed mp4 for one segment, or None if it is not ready in time.
Concurrent requests share one ffmpeg run instead of racing to write the same file.
"""
key = str(source_path)
created = False
with _VIDEO_REMUX_LOCK:
future = _VIDEO_REMUX_FUTURES.get(key)
if future is None:
future = _VIDEO_REMUX_EXECUTOR.submit(utilities.ffmpeg_mp4_wrap_to_path, source_path)
_VIDEO_REMUX_FUTURES[key] = future
created = True
if created:
future.add_done_callback(lambda completed: _remove_video_remux_future(key, completed))
try:
return future.result(timeout=VIDEO_REMUX_WAIT_SECONDS)
except TimeoutError:
# The callback keeps the running job deduplicated, then evicts it when done.
return None
def _remove_route_thumbnail_future(key, future):
with _ROUTE_THUMBNAIL_LOCK:
if _ROUTE_THUMBNAIL_FUTURES.get(key) is future:
@@ -6425,14 +6464,13 @@ def setup(app):
if segments:
base_path = os.path.join(footage_path, segments[0])
segment_urls = [f"/video/{segment}" for segment in segments]
total_duration = sum(
utilities.get_video_duration(os.path.join(footage_path, segment, "fcamera.hevc"))
for segment in segments
)
# Probing each segment cost an ffprobe before playback could even start,
# and segments are a fixed minute anyway.
total_duration = len(segments) * SEGMENT_DURATION_SECONDS
return {
"name": name,
"segment_urls": segment_urls,
"total_duration": round(total_duration),
"total_duration": total_duration,
"date": utilities.get_route_start_time(base_path),
"available_cameras": utilities.get_available_cameras(base_path),
}, 200
@@ -8922,65 +8960,43 @@ def setup(app):
@app.route("/video/<path>", methods=["GET"])
def get_video(path):
if not utilities.SEGMENT_RE.fullmatch(path or ""):
return {"error": "Invalid segment name"}, 400
camera = request.args.get("camera")
filename = {"driver": "dcamera.hevc", "wide": "ecamera.hevc"}.get(camera, "fcamera.hevc")
# loggerd writes qcamera.ts as H.264 in MPEG-TS, so it plays with no ffmpeg at
# all. It exists for the road camera only, so other views skip this tier.
if request.args.get("quality") == "low" and filename == "fcamera.hevc":
for footage_path in FOOTAGE_PATHS:
preview_path = os.path.join(footage_path, path, "qcamera.ts")
if os.path.isfile(preview_path):
return send_file(
preview_path,
mimetype="video/mp2t",
conditional=True,
max_age=VIDEO_CACHE_SECONDS,
)
return {"error": "Low quality video not available"}, 404
for footage_path in FOOTAGE_PATHS:
filepath = f"{footage_path}{path}/{filename}"
filepath = os.path.join(footage_path, path, filename)
if os.path.exists(filepath):
file_handle = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
try:
cache_path = _get_or_create_segment_mp4(filepath)
except (FileNotFoundError, ValueError) as error:
return {"error": str(error)}, 409
if cache_path is None:
return {"error": "Video is still being prepared"}, 503
file_handle.seek(0, 2)
file_size = file_handle.tell()
file_handle.seek(0)
range_header = request.headers.get('Range', None)
if range_header:
byte_start = 0
byte_end = file_size - 1
if range_header.startswith('bytes='):
range_spec = range_header[6:]
if '-' in range_spec:
start, end = range_spec.split('-', 1)
if start:
byte_start = max(0, int(start))
if end:
byte_end = min(file_size - 1, int(end))
if byte_start >= file_size:
file_handle.close()
return Response("Requested Range Not Satisfiable", 416)
byte_end = max(byte_start, byte_end)
file_handle.seek(byte_start)
read_length = byte_end - byte_start + 1
data = file_handle.read(read_length)
response = Response(
data,
206,
headers={
'Content-Range': f'bytes {byte_start}-{byte_end}/{file_size}',
'Accept-Ranges': 'bytes',
'Content-Length': str(len(data)),
'Content-Type': 'video/mp4'
}
)
else:
data = file_handle.read()
response = Response(
data,
200,
headers={
'Accept-Ranges': 'bytes',
'Content-Length': str(file_size),
'Content-Type': 'video/mp4'
}
)
file_handle.close()
return response
# send_file streams from disk and handles Range and ETag itself.
return send_file(
cache_path,
mimetype="video/mp4",
conditional=True,
max_age=VIDEO_CACHE_SECONDS,
)
return {"error": "Video not found"}, 404
def main():
+72 -17
View File
@@ -650,6 +650,57 @@ def encode_parameters(params_dict):
encoded_data = base64.b64encode(obfuscated_data.encode("utf-8")).decode("utf-8")
return encoded_data
# The venv ships ffmpeg/ffprobe as console scripts that exec the real binary only
# after a full CPython startup. Resolve past the shim once and skip that per call.
def _resolve_ffmpeg_binary(name):
try:
import ffmpeg as ffmpeg_package
candidate = Path(ffmpeg_package.__file__).parent / "install" / "bin" / name
if candidate.is_file() and os.access(candidate, os.X_OK):
return str(candidate)
except Exception:
pass
return shutil.which(name) or name
FFMPEG_BIN = _resolve_ffmpeg_binary("ffmpeg")
FFPROBE_BIN = _resolve_ffmpeg_binary("ffprobe")
# Bound the cache by its own size rather than reacting to free space: loggerd already
# keeps the disk near full, so the old policy wiped every mp4 on almost every request.
VIDEO_CACHE_MAX_BYTES = 512 * 1024 * 1024
def _prune_video_cache(keep_path=None):
"""Evict oldest-first until the cache fits its budget."""
try:
entries = []
for cache_file in VIDEO_CACHE_PATH.glob("*.mp4"):
try:
stat = cache_file.stat()
except OSError:
continue
entries.append((stat.st_mtime, stat.st_size, cache_file))
except OSError:
return
total = sum(size for _, size, _ in entries)
if total <= VIDEO_CACHE_MAX_BYTES:
return
keep = str(keep_path) if keep_path else None
for _, size, cache_file in sorted(entries):
if total <= VIDEO_CACHE_MAX_BYTES:
break
if keep and str(cache_file) == keep:
continue
try:
cache_file.unlink()
total -= size
except OSError:
pass
def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
if not input_files:
raise ValueError("No input files provided for concatenation")
@@ -665,6 +716,8 @@ def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
if cache_path.exists() and all(cache_path.stat().st_mtime > Path(f).stat().st_mtime for f in input_files):
return open(cache_path, "rb")
_prune_video_cache(keep_path=cache_path)
list_file = VIDEO_CACHE_PATH / f"{file_hash}.txt"
with open(list_file, "w") as f:
for seg in input_files:
@@ -672,14 +725,14 @@ def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
try:
subprocess.run(
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
[FFMPEG_BIN, "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
"-i", str(list_file), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)],
check=True
)
except subprocess.CalledProcessError:
try:
subprocess.run(
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
[FFMPEG_BIN, "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
"-i", str(list_file), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)],
check=True
)
@@ -693,7 +746,11 @@ def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
return open(cache_path, "rb")
def ffmpeg_mp4_wrap_process_builder(filename):
def ffmpeg_mp4_wrap_to_path(filename):
"""Remux one raw .hevc segment to mp4 and return the cache path.
Callers get a path, not a handle, so send_file can stream it without reading it all.
"""
input_path = Path(filename)
if not input_path.exists():
@@ -708,31 +765,29 @@ def ffmpeg_mp4_wrap_process_builder(filename):
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
total, used, free = shutil.disk_usage(VIDEO_CACHE_PATH)
if free < 500 * 1024 * 1024:
for cache_file in VIDEO_CACHE_PATH.glob("*.mp4"):
try:
cache_file.unlink()
except:
pass
file_hash = hashlib.md5(str(input_path).encode()).hexdigest()
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
if cache_path.exists() and cache_path.stat().st_mtime > input_path.stat().st_mtime:
return open(cache_path, "rb")
return cache_path
_prune_video_cache(keep_path=cache_path)
try:
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)], check=True)
subprocess.run([FFMPEG_BIN, "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)], check=True)
except subprocess.CalledProcessError:
try:
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)], check=True)
subprocess.run([FFMPEG_BIN, "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)], check=True)
except subprocess.CalledProcessError:
if cache_path.exists():
cache_path.unlink()
raise ValueError(f"Cannot process video file: {input_path}")
return open(cache_path, "rb")
return cache_path
def ffmpeg_mp4_wrap_process_builder(filename):
return open(ffmpeg_mp4_wrap_to_path(filename), "rb")
def format_git_date(raw_date: str):
date_object = datetime.strptime(raw_date.split()[1], "%Y-%m-%d")
@@ -3014,7 +3069,7 @@ def get_segments_in_route(route_time_str, footage_path):
def get_video_duration(input_path):
try:
result = subprocess.run([
"ffprobe", "-v", "error", "-show_entries", "format=duration",
FFPROBE_BIN, "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", str(input_path)
], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True)
return float(result.stdout)
@@ -3105,7 +3160,7 @@ VIDEO_TO_PNG_TIMEOUT_SECONDS = 20
def video_to_png(input_path, output_path):
try:
subprocess.run([
"ffmpeg", "-hide_banner", "-loglevel", "error",
FFMPEG_BIN, "-hide_banner", "-loglevel", "error",
"-ss", "1",
"-i", str(input_path),
"-frames:v", "1",