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:
16
selfdrive/clearpilot/SConscript
Normal file
16
selfdrive/clearpilot/SConscript
Normal 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']
|
||||
)
|
||||
140
selfdrive/clearpilot/dashcamd.cc
Normal file
140
selfdrive/clearpilot/dashcamd.cc
Normal 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;
|
||||
}
|
||||
121
selfdrive/clearpilot/dashcamd.py
Normal file
121
selfdrive/clearpilot/dashcamd.py
Normal 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()
|
||||
Reference in New Issue
Block a user