dashcamd v2: native C++ process with direct camera capture

- 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>
This commit is contained in:
2026-04-11 08:54:41 +00:00
parent ed9c14616a
commit 4afd25fb5b
15 changed files with 560 additions and 119 deletions

View File

@@ -0,0 +1,16 @@
Import('env', 'arch', 'common', 'messaging', 'visionipc', 'cereal')
clearpilot_env = env.Clone()
clearpilot_env['CPPPATH'] += ['#selfdrive/frogpilot/screenrecorder/openmax/include/']
# Disable --as-needed so static lib ordering doesn't matter
clearpilot_env['LINKFLAGS'] = [f for f in clearpilot_env.get('LINKFLAGS', []) if f != '-Wl,--as-needed']
if arch == "larch64":
omx_obj = File('#selfdrive/frogpilot/screenrecorder/omx_encoder.o')
clearpilot_env.Program(
'dashcamd',
['dashcamd.cc', omx_obj],
LIBS=[common, 'json11', cereal, visionipc, messaging,
'zmq', 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread',
'OmxCore', 'avformat', 'avcodec', 'avutil', 'yuv']
)

View File

@@ -0,0 +1,140 @@
/*
* CLEARPILOT dashcamd — records raw camera footage to MP4 using OMX H.264 hardware encoder.
*
* Connects to camerad via VisionIPC, receives NV12 frames, and feeds them directly
* to the Qualcomm OMX encoder. Produces 3-minute MP4 segments in /data/media/0/videos/.
*
* Suspends recording after 10 minutes of standstill, resumes when car moves.
*/
#include <cstdio>
#include <ctime>
#include <string>
#include <sys/stat.h>
#include <sys/resource.h>
#include <unistd.h>
#include "cereal/messaging/messaging.h"
#include "cereal/visionipc/visionipc_client.h"
#include "common/params.h"
#include "common/timing.h"
#include "common/swaglog.h"
#include "common/util.h"
#include "selfdrive/frogpilot/screenrecorder/omx_encoder.h"
const std::string VIDEOS_DIR = "/data/media/0/videos";
const int SEGMENT_SECONDS = 180; // 3 minutes
const int CAMERA_FPS = 20;
const int FRAMES_PER_SEGMENT = SEGMENT_SECONDS * CAMERA_FPS;
const int BITRATE = 4 * 1024 * 1024; // 4 Mbps
const double STANDSTILL_TIMEOUT_SECONDS = 600.0; // 10 minutes
ExitHandler do_exit;
static std::string make_filename() {
char buf[64];
time_t t = time(NULL);
struct tm tm = *localtime(&t);
snprintf(buf, sizeof(buf), "%04d%02d%02d-%02d%02d%02d.mp4",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
return std::string(buf);
}
int main(int argc, char *argv[]) {
setpriority(PRIO_PROCESS, 0, -10);
// Ensure output directory exists
mkdir(VIDEOS_DIR.c_str(), 0755);
LOGW("dashcamd: connecting to camerad road stream");
VisionIpcClient vipc("camerad", VISION_STREAM_ROAD, false);
while (!do_exit && !vipc.connect(false)) {
usleep(100000);
}
if (do_exit) return 0;
int width = vipc.buffers[0].width;
int height = vipc.buffers[0].height;
int y_stride = vipc.buffers[0].stride;
int uv_stride = y_stride;
LOGW("dashcamd: connected %dx%d, stride=%d", width, height, y_stride);
// Subscribe to carState for standstill detection
SubMaster sm({"carState"});
// Create encoder — H.264, no downscale, with MP4 remuxing (h265=false)
OmxEncoder encoder(VIDEOS_DIR.c_str(), width, height, CAMERA_FPS, BITRATE, false, false);
int frame_count = FRAMES_PER_SEGMENT; // force new segment on first frame
uint64_t start_ts = 0;
bool recording = false;
bool suspended = false;
double standstill_start = 0.0;
while (!do_exit) {
VisionBuf *buf = vipc.recv();
if (buf == nullptr) continue;
// Check standstill state
sm.update(0);
bool is_standstill = sm.valid("carState") && sm["carState"].getCarState().getStandstill();
double now = nanos_since_boot() / 1e9;
if (is_standstill) {
if (standstill_start == 0.0) {
standstill_start = now;
}
// Suspend after 10 minutes of continuous standstill
if (!suspended && (now - standstill_start) >= STANDSTILL_TIMEOUT_SECONDS) {
LOGW("dashcamd: suspending — standstill for 10 minutes");
if (recording) {
encoder.encoder_close();
recording = false;
frame_count = FRAMES_PER_SEGMENT;
}
suspended = true;
}
} else {
standstill_start = 0.0;
if (suspended) {
LOGW("dashcamd: resuming — car moving");
suspended = false;
}
}
if (suspended) continue;
// Start new segment if needed
if (frame_count >= FRAMES_PER_SEGMENT) {
if (recording) {
encoder.encoder_close();
}
std::string filename = make_filename();
LOGW("dashcamd: opening segment %s", filename.c_str());
encoder.encoder_open(filename.c_str());
frame_count = 0;
start_ts = nanos_since_boot();
recording = true;
}
uint64_t ts = nanos_since_boot() - start_ts;
// Feed NV12 frame directly to OMX encoder
uint8_t *y_ptr = buf->y;
uint8_t *uv_ptr = buf->uv;
encoder.encode_frame_nv12(y_ptr, y_stride, uv_ptr, uv_stride, width, height, ts);
frame_count++;
}
if (recording) {
encoder.encoder_close();
}
LOGW("dashcamd: stopped");
return 0;
}

View File

@@ -0,0 +1,121 @@
#!/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()