port dashcamd: hardware-encoded MP4 dashcam

VisionIPC frames from camerad → OMX H.264 hardware encoder → 3-min MP4
segments + SRT GPS subtitles in /data/media/0/videos/<trip>/. Manages
its own trip lifecycle (WAITING/RECORDING/IDLE_TIMEOUT) and writes
DashcamState/DashcamFrames memory params for the UI's Status window.
Honors DashcamShutdown for graceful close before power-off.

Files added:
- selfdrive/clearpilot/dashcamd.cc + SConscript

Files modified:
- selfdrive/frogpilot/screenrecorder/omx_encoder.{cc,h}: ported broken's
  version, which adds encode_frame_nv12() (direct NV12 input from camerad,
  alongside the existing encode_frame_rgba used by the disabled screen
  recorder) and simplifies the libyuv conversion paths to NEON-only since
  this device is aarch64.
- selfdrive/SConscript: register selfdrive/clearpilot/SConscript so the
  dashcamd binary is part of the build.
- selfdrive/manager/process_config.py:
  - camerad gating driverview → always_run so dashcamd can record the
    moment ignition+drive arrives without waiting for camera startup.
  - Register dashcamd as NativeProcess gated always_run.
- system/loggerd/deleter.py:
  - MIN_BYTES 5 GB → 9 GB to leave headroom for dashcam footage.
  - delete_oldest_video(): trip-aware cleanup. Drops entire oldest trip
    dir first; if only the active trip remains, drops oldest segment
    inside it; cleans up legacy flat .mp4s too.
  - cleanup_log2(): keeps /data/log2 session logs under 4 GB total.
  - Hooked into deleter_thread: video first when out of bytes/percent;
    log2 quota check on the idle path. New code uses print(stderr) per
    the no-cloudlog rule.

Verified: built clean, manager started, dashcamd in WAITING state
(DashcamState=waiting, DashcamFrames=0), camerad running, no errors.
This commit is contained in:
2026-05-03 23:14:23 -05:00
parent 3f5172b58b
commit 8a7a776f9b
8 changed files with 889 additions and 418 deletions
+103 -1
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import os
import shutil
import sys
import threading
from openpilot.system.hardware.hw import Paths
from openpilot.common.swaglog import cloudlog
@@ -8,11 +9,17 @@ from openpilot.system.loggerd.config import get_available_bytes, get_available_p
from openpilot.system.loggerd.uploader import listdir_by_creation
from openpilot.system.loggerd.xattr_cache import getxattr
MIN_BYTES = 5 * 1024 * 1024 * 1024
# CLEARPILOT: bumped from 5 GB to 9 GB so dashcam footage has headroom
MIN_BYTES = 9 * 1024 * 1024 * 1024
MIN_PERCENT = 10
DELETE_LAST = ['boot', 'crash']
# CLEARPILOT: dashcam footage directory (trip dirs YYYYMMDD-HHMMSS/ with .mp4 segments)
VIDEOS_DIR = '/data/media/0/videos'
# CLEARPILOT: max total size for /data/log2 session logs
LOG2_MAX_BYTES = 4 * 1024 * 1024 * 1024
PRESERVE_ATTR_NAME = 'user.preserve'
PRESERVE_ATTR_VALUE = b'1'
PRESERVE_COUNT = 5
@@ -44,12 +51,105 @@ def get_preserved_segments(dirs_by_creation: list[str]) -> list[str]:
return preserved
def delete_oldest_video():
"""CLEARPILOT: prune dashcam footage when disk space is low.
Trip directories are /data/media/0/videos/YYYYMMDD-HHMMSS/ containing .mp4
segments. Deletes entire oldest trip directory first. If only one trip
remains (the active one), deletes individual segments oldest-first within
it. Also cleans up legacy flat .mp4 files.
Returns True if something was deleted."""
try:
if not os.path.isdir(VIDEOS_DIR):
return False
legacy_files = []
trip_dirs = []
for entry in os.listdir(VIDEOS_DIR):
path = os.path.join(VIDEOS_DIR, entry)
if os.path.isfile(path) and entry.endswith('.mp4'):
legacy_files.append(entry)
elif os.path.isdir(path):
trip_dirs.append(entry)
if legacy_files:
legacy_files.sort()
delete_path = os.path.join(VIDEOS_DIR, legacy_files[0])
print(f"CLP deleter: deleting legacy video {delete_path}", file=sys.stderr, flush=True)
os.remove(delete_path)
return True
if not trip_dirs:
return False
trip_dirs.sort() # timestamp names = chronological order
if len(trip_dirs) > 1:
delete_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
print(f"CLP deleter: deleting trip {delete_path}", file=sys.stderr, flush=True)
shutil.rmtree(delete_path)
return True
# Only one trip left (likely active) — drop its oldest segment
trip_path = os.path.join(VIDEOS_DIR, trip_dirs[0])
segments = sorted(f for f in os.listdir(trip_path) if f.endswith('.mp4'))
if not segments:
return False
delete_path = os.path.join(trip_path, segments[0])
print(f"CLP deleter: deleting segment {delete_path}", file=sys.stderr, flush=True)
os.remove(delete_path)
return True
except OSError as e:
print(f"CLP deleter: issue deleting video from {VIDEOS_DIR}: {e}", file=sys.stderr, flush=True)
return False
def cleanup_log2():
"""CLEARPILOT: keep /data/log2 session logs under LOG2_MAX_BYTES total.
Deletes oldest dated session directories first (the 'current' symlink/dir
is preserved). Runs even when disk space is fine."""
log_base = "/data/log2"
if not os.path.isdir(log_base):
return
dirs = []
for entry in sorted(os.listdir(log_base)):
if entry == "current":
continue
path = os.path.join(log_base, entry)
if os.path.isdir(path) and not os.path.islink(path):
try:
size = sum(f.stat().st_size for f in os.scandir(path) if f.is_file())
except OSError:
size = 0
dirs.append((entry, path, size))
total = sum(s for _, _, s in dirs)
current = os.path.join(log_base, "current")
if os.path.isdir(current):
try:
total += sum(f.stat().st_size for f in os.scandir(current) if f.is_file())
except OSError:
pass
while total > LOG2_MAX_BYTES and dirs:
entry, path, size = dirs.pop(0)
try:
print(f"CLP deleter: deleting log session {path} ({size // 1024 // 1024} MB)",
file=sys.stderr, flush=True)
shutil.rmtree(path)
total -= size
except OSError as e:
print(f"CLP deleter: issue deleting log {path}: {e}", file=sys.stderr, flush=True)
def deleter_thread(exit_event):
while not exit_event.is_set():
out_of_bytes = get_available_bytes(default=MIN_BYTES + 1) < MIN_BYTES
out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT
if out_of_percent or out_of_bytes:
# CLEARPILOT: drop oldest dashcam footage first, fall back to log segments
if delete_oldest_video():
exit_event.wait(.1)
continue
dirs = listdir_by_creation(Paths.log_root())
# skip deleting most recent N preserved segments (and their prior segment)
@@ -73,6 +173,8 @@ def deleter_thread(exit_event):
cloudlog.exception(f"issue deleting {delete_path}")
exit_event.wait(.1)
else:
# CLEARPILOT: keep /data/log2 quota even when disk space is fine
cleanup_log2()
exit_event.wait(30)