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:
@@ -3,4 +3,5 @@ SConscript(['controls/lib/lateral_mpc_lib/SConscript'])
|
||||
SConscript(['controls/lib/longitudinal_mpc_lib/SConscript'])
|
||||
SConscript(['locationd/SConscript'])
|
||||
SConscript(['modeld/SConscript'])
|
||||
SConscript(['ui/SConscript'])
|
||||
SConscript(['ui/SConscript'])
|
||||
SConscript(['clearpilot/SConscript'])
|
||||
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()
|
||||
@@ -1233,7 +1233,7 @@ class Controls:
|
||||
self.frogpilot_variables.use_ev_tables = self.params.get_bool("EVTable")
|
||||
|
||||
def update_clearpilot_events(self, CS):
|
||||
if (len(CS.buttonEvents) > 0):
|
||||
if (len(CS.buttonEvents) > 0):
|
||||
print (CS.buttonEvents)
|
||||
if any(be.pressed and be.type == FrogPilotButtonType.lkas for be in CS.buttonEvents):
|
||||
self.events.add(EventName.clpDebug)
|
||||
|
||||
@@ -778,8 +778,8 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
ET.SOFT_DISABLE: soft_disable_alert("Sensor Data Invalid"),
|
||||
},
|
||||
|
||||
# CLEARPILOT: alert suppressed — event still fires for screen toggle and future actions
|
||||
EventName.clpDebug: {
|
||||
ET.PERMANENT: clp_debug_notice,
|
||||
},
|
||||
|
||||
EventName.noGps: {
|
||||
|
||||
@@ -565,6 +565,60 @@ int OmxEncoder::encode_frame_rgba(const uint8_t *ptr, int in_width, int in_heigh
|
||||
return ret;
|
||||
}
|
||||
|
||||
// CLEARPILOT: encode raw NV12 frames directly (no RGBA conversion needed)
|
||||
int OmxEncoder::encode_frame_nv12(const uint8_t *y_ptr, int y_stride, const uint8_t *uv_ptr, int uv_stride,
|
||||
int in_width, int in_height, uint64_t ts) {
|
||||
if (!this->is_open) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
OMX_BUFFERHEADERTYPE* in_buf = nullptr;
|
||||
while (!this->free_in.try_pop(in_buf, 20)) {
|
||||
if (do_exit) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = this->counter;
|
||||
|
||||
uint8_t *in_buf_ptr = in_buf->pBuffer;
|
||||
int venus_y_stride = VENUS_Y_STRIDE(COLOR_FMT_NV12, this->width);
|
||||
int venus_uv_stride = VENUS_UV_STRIDE(COLOR_FMT_NV12, this->width);
|
||||
uint8_t *dst_y = in_buf_ptr;
|
||||
uint8_t *dst_uv = in_buf_ptr + (venus_y_stride * VENUS_Y_SCANLINES(COLOR_FMT_NV12, this->height));
|
||||
|
||||
// Copy Y plane row by row (source stride may differ from VENUS stride)
|
||||
for (int row = 0; row < in_height; row++) {
|
||||
memcpy(dst_y + row * venus_y_stride, y_ptr + row * y_stride, in_width);
|
||||
}
|
||||
// Copy UV plane row by row
|
||||
int uv_height = in_height / 2;
|
||||
for (int row = 0; row < uv_height; row++) {
|
||||
memcpy(dst_uv + row * venus_uv_stride, uv_ptr + row * uv_stride, in_width);
|
||||
}
|
||||
|
||||
in_buf->nFilledLen = VENUS_BUFFER_SIZE(COLOR_FMT_NV12, this->width, this->height);
|
||||
in_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME;
|
||||
in_buf->nOffset = 0;
|
||||
in_buf->nTimeStamp = ts / 1000LL;
|
||||
this->last_t = in_buf->nTimeStamp;
|
||||
|
||||
OMX_CHECK(OMX_EmptyThisBuffer(this->handle, in_buf));
|
||||
|
||||
while (true) {
|
||||
OMX_BUFFERHEADERTYPE *out_buf;
|
||||
if (!this->done_out.try_pop(out_buf)) {
|
||||
break;
|
||||
}
|
||||
handle_out_buf(this, out_buf);
|
||||
}
|
||||
|
||||
this->dirty = true;
|
||||
this->counter++;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void OmxEncoder::encoder_open(const char* filename) {
|
||||
int err;
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ public:
|
||||
~OmxEncoder();
|
||||
|
||||
int encode_frame_rgba(const uint8_t *ptr, int in_width, int in_height, uint64_t ts);
|
||||
int encode_frame_nv12(const uint8_t *y_ptr, int y_stride, const uint8_t *uv_ptr, int uv_stride,
|
||||
int in_width, int in_height, uint64_t ts);
|
||||
void encoder_open(const char* filename);
|
||||
void encoder_close();
|
||||
|
||||
|
||||
@@ -135,6 +135,7 @@ def manager_init(frogpilot_functions) -> None:
|
||||
("DisableOpenpilotLongitudinal", "0"),
|
||||
("DisableVTSCSmoothing", "0"),
|
||||
("DisengageVolume", "100"),
|
||||
("DashcamDebug", "1"),
|
||||
("DragonPilotTune", "0"),
|
||||
("DriverCamera", "0"),
|
||||
("DynamicPathWidth", "0"),
|
||||
|
||||
@@ -51,6 +51,10 @@ def allow_uploads(started, params, CP: car.CarParams) -> bool:
|
||||
allow_uploads = not (params.get_bool("DeviceManagement") and params.get_bool("NoUploads"))
|
||||
return allow_uploads
|
||||
|
||||
# ClearPilot functions
|
||||
def dashcam_should_run(started, params, CP: car.CarParams) -> bool:
|
||||
return started or params.get_bool("DashcamDebug")
|
||||
|
||||
procs = [
|
||||
DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"),
|
||||
|
||||
@@ -102,6 +106,9 @@ procs = [
|
||||
# FrogPilot processes
|
||||
PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run),
|
||||
PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run),
|
||||
|
||||
# ClearPilot processes
|
||||
NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run),
|
||||
]
|
||||
|
||||
managed_processes = {p.name: p for p in procs}
|
||||
@@ -919,14 +919,14 @@ void AnnotatedCameraWidget::initializeFrogPilotWidgets() {
|
||||
animationFrameIndex = (animationFrameIndex + 1) % totalFrames;
|
||||
});
|
||||
|
||||
// CLEARPILOT: screen recorder timer — feeds frames to the OMX encoder
|
||||
QTimer *record_timer = new QTimer(this);
|
||||
connect(record_timer, &QTimer::timeout, this, [this]() {
|
||||
if (recorder_btn) {
|
||||
recorder_btn->update_screen();
|
||||
}
|
||||
});
|
||||
record_timer->start(1000 / UI_FREQ);
|
||||
// CLEARPILOT: screen recorder disabled — replaced by dedicated dashcamd process
|
||||
// QTimer *record_timer = new QTimer(this);
|
||||
// connect(record_timer, &QTimer::timeout, this, [this]() {
|
||||
// if (recorder_btn) {
|
||||
// recorder_btn->update_screen();
|
||||
// }
|
||||
// });
|
||||
// record_timer->start(1000 / UI_FREQ);
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::updateFrogPilotWidgets() {
|
||||
|
||||
Reference in New Issue
Block a user