From f85bfb277b68419de43d188c8c0e35b20d2f8694 Mon Sep 17 00:00:00 2001
From: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Date: Thu, 27 Aug 2026 13:25:57 -0700
Subject: [PATCH] Ahhh Controls go BRRR
---
.../components/recordings/dashcam_routes.css | 40 +++-
.../components/recordings/dashcam_routes.js | 209 +++++++++++++-----
.../recordings/dashcam_routes_helpers.js | 30 ++-
.../the_galaxy/tests/test_dashcam_routes.py | 146 +++++++++++-
.../tests/test_dashcam_routes_helpers.py | 86 ++++++-
starpilot/system/the_galaxy/the_galaxy.py | 134 ++++++-----
starpilot/system/the_galaxy/utilities.py | 89 ++++++--
7 files changed, 593 insertions(+), 141 deletions(-)
diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css
index 3d3186b02..dfb94fe97 100644
--- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css
+++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css
@@ -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);
diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js
index 42abb0020..f61909077 100644
--- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js
+++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.js
@@ -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) {
-
+
Loading route metadata…
-
+
+
+
+
+
@@ -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 => `
`)
+ .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`
@@ -540,13 +652,12 @@ export function RouteRecordings() {
${group.label}
${group.routes.length} ${group.routes.length === 1 ? "drive" : "drives"}
- ${isGrid ? html`
-
+ ${() => state.viewMode === "grid" ? html`
${group.routes.map(route => html`
-
{ state.selectedRoute = route }}">
+ { state.selectedRoute = route }}">
-
@@ -581,11 +692,9 @@ export function RouteRecordings() {
`)}
-
- ` : html`
-
+
` : html`
${group.routes.map(route => html`
-
{ state.selectedRoute = route }}">
+ { state.selectedRoute = route }}">

@@ -600,7 +709,7 @@ export function RouteRecordings() {
${formatApproxDuration(route.approxDurationSeconds)}
${route.segmentCount} segment${route.segmentCount === 1 ? "" : "s"}
- ${route.is_preserved ? html` Preserved` : ""}
+ ${() => route.is_preserved ? html` Preserved` : ""}
${route.name.split("--").slice(1).join("--") || route.name}
@@ -608,8 +717,8 @@ export function RouteRecordings() {
{ state.selectedRoute = route }}">
Play
- togglePreserved(route, event)}">
-
+ togglePreserved(route, event)}">
+
openLogsFromRow(route, event)}">
diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js
index eb251b11e..f84c9415b 100644
--- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js
+++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js
@@ -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"
}
diff --git a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
index 4c3d9dbac..77228c0a9 100644
--- a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
+++ b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
@@ -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
diff --git a/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py b/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py
index d0818191b..9bbfe70b5 100644
--- a/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py
+++ b/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py
@@ -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"]
diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py
index 392d65679..64c32316d 100644
--- a/starpilot/system/the_galaxy/the_galaxy.py
+++ b/starpilot/system/the_galaxy/the_galaxy.py
@@ -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/", 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():
diff --git a/starpilot/system/the_galaxy/utilities.py b/starpilot/system/the_galaxy/utilities.py
index 4bee69c82..c0b898a06 100644
--- a/starpilot/system/the_galaxy/utilities.py
+++ b/starpilot/system/the_galaxy/utilities.py
@@ -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",