4afd25fb5b
- New dashcamd: connects to camerad via VisionIPC, feeds raw NV12 frames directly to OMX H.264 encoder. Full 1928x1208 resolution, 4Mbps, 3-minute MP4 segments. Works regardless of UI state. - Added encode_frame_nv12() to OmxEncoder — skips RGBA->NV12 conversion - Suspends recording after 10 minutes of standstill - Disabled old screen recorder timer in onroad.cc - Suppress debug button alert (clpDebug event still fires for screen toggle) - launch_openpilot.sh self-cleans other instances before starting - Register DashcamDebug param in params.cc and manager.py - Add dashcamd to build system (SConscript) and process_config - Updated CLAUDE.md with all session changes - Added GOALS.md feature roadmap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
CLEARPILOT dashcamd — records raw camera footage to MP4 using hardware H.264 encoder.
|
|
|
|
Connects directly to camerad via VisionIPC, receives NV12 frames, and pipes them
|
|
to ffmpeg's h264_v4l2m2m encoder. Produces 3-minute MP4 segments in /data/media/0/videos/.
|
|
|
|
This replaces the FrogPilot screen recorder approach (QWidget::grab -> OMX) with a
|
|
direct camera capture that works regardless of UI state (screen off, alternate modes, etc).
|
|
"""
|
|
import os
|
|
import time
|
|
import subprocess
|
|
import signal
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
from cereal.visionipc import VisionIpcClient, VisionStreamType
|
|
from openpilot.common.params import Params
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.selfdrive import sentry
|
|
|
|
PROCESS_NAME = "selfdrive.clearpilot.dashcamd"
|
|
VIDEOS_DIR = "/data/media/0/videos"
|
|
SEGMENT_SECONDS = 180 # 3 minutes
|
|
CAMERA_FPS = 20
|
|
FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS
|
|
|
|
|
|
def make_filename():
|
|
return datetime.now().strftime("%Y%m%d-%H%M%S") + ".mp4"
|
|
|
|
|
|
def open_encoder(width, height, filepath):
|
|
"""Start an ffmpeg subprocess that accepts raw NV12 on stdin and writes MP4."""
|
|
cmd = [
|
|
"ffmpeg", "-y", "-nostdin", "-loglevel", "error",
|
|
"-f", "rawvideo",
|
|
"-pix_fmt", "nv12",
|
|
"-s", f"{width}x{height}",
|
|
"-r", str(CAMERA_FPS),
|
|
"-i", "pipe:0",
|
|
"-c:v", "h264_v4l2m2m",
|
|
"-b:v", "4M",
|
|
"-f", "mp4",
|
|
"-movflags", "+faststart",
|
|
filepath,
|
|
]
|
|
return subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
|
|
|
|
|
def main():
|
|
sentry.set_tag("daemon", PROCESS_NAME)
|
|
cloudlog.bind(daemon=PROCESS_NAME)
|
|
|
|
os.makedirs(VIDEOS_DIR, exist_ok=True)
|
|
|
|
params = Params()
|
|
|
|
# Connect to camerad road stream
|
|
cloudlog.info("dashcamd: connecting to camerad road stream")
|
|
vipc = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, False)
|
|
while not vipc.connect(False):
|
|
time.sleep(0.1)
|
|
|
|
width, height = vipc.width, vipc.height
|
|
# NV12 frame: Y plane (w*h) + UV plane (w*h/2)
|
|
frame_size = width * height * 3 // 2
|
|
cloudlog.info(f"dashcamd: connected, {width}x{height}, frame_size={frame_size}")
|
|
|
|
frame_count = 0
|
|
encoder = None
|
|
lock_path = None
|
|
|
|
try:
|
|
while True:
|
|
buf = vipc.recv()
|
|
if buf is None:
|
|
continue
|
|
|
|
# Start new segment if needed
|
|
if encoder is None or frame_count >= FRAMES_PER_SEGMENT:
|
|
# Close previous segment
|
|
if encoder is not None:
|
|
encoder.stdin.close()
|
|
encoder.wait()
|
|
if lock_path and os.path.exists(lock_path):
|
|
os.remove(lock_path)
|
|
cloudlog.info(f"dashcamd: closed segment, {frame_count} frames")
|
|
|
|
# Open new segment
|
|
filename = make_filename()
|
|
filepath = os.path.join(VIDEOS_DIR, filename)
|
|
lock_path = filepath + ".lock"
|
|
Path(lock_path).touch()
|
|
|
|
cloudlog.info(f"dashcamd: opening segment {filename}")
|
|
encoder = open_encoder(width, height, filepath)
|
|
frame_count = 0
|
|
|
|
# Write raw NV12 frame to ffmpeg stdin
|
|
try:
|
|
encoder.stdin.write(buf.data[:frame_size])
|
|
frame_count += 1
|
|
except BrokenPipeError:
|
|
cloudlog.error("dashcamd: encoder pipe broken, restarting segment")
|
|
encoder = None
|
|
|
|
except (KeyboardInterrupt, SystemExit):
|
|
pass
|
|
finally:
|
|
if encoder is not None:
|
|
encoder.stdin.close()
|
|
encoder.wait()
|
|
if lock_path and os.path.exists(lock_path):
|
|
os.remove(lock_path)
|
|
cloudlog.info("dashcamd: stopped")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|