进行第四阶段开发

This commit is contained in:
sladro 2025-12-26 16:58:18 +08:00
parent 5ee7061624
commit af4cdf3934
24 changed files with 2559 additions and 3 deletions

116
Readme.md
View File

@ -497,13 +497,125 @@ GraphMgr 负责:
| 节点类型 | 功能描述 | 关键参数 | 硬件依赖 |
| :---------- | :--------------------------------- | :----------------------------------- | :----------------- |
| `osd` | 绘制检测框、文字等 | `draw_bbox`, `draw_text`, `labels`, `line_width`, `font_scale` | RGA / CPU |
| `alarm` | 报警规则(区域、时间、目标类型) | `rules`(含 ROI、时间段、阈值等 | CPU |
| `storage` | 录像MP4/TS 切片) | `mode`, `segment_sec`, `path` | CPUIO |
| `alarm` | 报警规则 + 多种报警动作 | `rules`, `actions`(见下文详细说明) | CPU |
| `storage` | 持续录像MP4/TS 切片) | `mode`, `segment_sec`, `path` | MPP VENC + IO |
| `publish` | 编码并推流RTSP/HLS | `codec`, `bitrate`, `outputs` | MPP VENC + 网络 |
**osd 参数说明:**
- `labels`: 自定义类别标签数组,如 `["cat", "dog", "bird"]`,为空则使用 COCO 默认标签
### 6.4 alarm 插件详细说明
alarm 插件采用**模块化 Actions 架构**,支持多种报警动作的灵活组合:
**架构设计:**
```
┌─────────────────────────────────────────────────────────┐
│ AlarmNode │
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
│ │ RuleEngine │ │ FrameRingBuffer (pre-event) │ │
│ │ - 类别过滤 │ │ - 保存最近 N 秒的帧 │ │
│ │ - ROI 检测 │ └──────────────────────────────────┘ │
│ │ - 持续时间 │ │
│ │ - 冷却控制 │ ┌──────────────────────────────────┐ │
│ └──────────────┘ │ AlarmActions (可配置) │ │
│ │ - LogAction → 日志输出 │ │
│ │ - HttpAction → HTTP 回调 │ │
│ │ - SnapshotAction → 截图+上传 │ │
│ │ - ClipAction → 视频片段+上传 │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
**完整配置示例:**
```json
{
"id": "alarm_cam1",
"type": "alarm",
"role": "sink",
"labels": ["矿体A", "矿体B", "异物"],
"rules": [
{
"name": "ore_detection",
"class_ids": [0, 1],
"objects": ["矿体A"],
"roi": { "x": 0.1, "y": 0.1, "w": 0.8, "h": 0.8 },
"min_duration_ms": 500,
"cooldown_ms": 5000,
"schedule": "08:00-20:00"
}
],
"actions": {
"log": { "enable": true, "level": "info" },
"http": {
"enable": true,
"url": "http://业务系统/api/alarm",
"timeout_ms": 3000,
"include_media_url": true
},
"snapshot": {
"enable": true,
"format": "jpg",
"quality": 85,
"upload": {
"type": "minio",
"endpoint": "http://minio:9000",
"bucket": "alarms",
"access_key": "${MINIO_ACCESS_KEY}",
"secret_key": "${MINIO_SECRET_KEY}"
}
},
"clip": {
"enable": true,
"pre_sec": 5,
"post_sec": 10,
"format": "mp4",
"upload": { "type": "local", "path": "/tmp/alarms" }
}
}
}
```
**规则参数说明:**
- `class_ids`: 要监控的类别 ID 数组(与模型输出对应)
- `objects`: 要监控的类别名称数组(需配合 `labels` 使用)
- `roi`: 感兴趣区域,归一化坐标 (0-1)
- `min_duration_ms`: 目标在 ROI 内持续多久才触发报警
- `cooldown_ms`: 同一规则连续触发的冷却时间
- `schedule`: 有效时间段,格式 "HH:MM-HH:MM"
**上传器类型:**
- `local`: 保存到本地文件系统
- `minio`: 上传到 MinIO 对象存储
- `s3`: 上传到 AWS S3兼容 MinIO
### 6.5 storage 插件详细说明
storage 插件用于**持续录像**7x24 小时),与 alarm 的事件录像功能互补。
**配置示例:**
```json
{
"id": "storage_cam1",
"type": "storage",
"role": "sink",
"mode": "continuous",
"format": "mp4",
"codec": "h264",
"segment_sec": 300,
"path": "/rec/cam1",
"filename_pattern": "%Y%m%d/%H%M%S",
"fps": 25,
"bitrate_kbps": 2000
}
```
**参数说明:**
- `mode`: 录像模式,当前支持 `continuous`(持续录像)
- `format`: 输出格式,`mp4` 或 `ts`
- `segment_sec`: 单个文件时长(秒)
- `filename_pattern`: 文件名模式,支持 strftime 格式
---
## 七、线程模型Thread Model

View File

@ -0,0 +1,151 @@
{
"queue": { "size": 8, "strategy": "drop_oldest" },
"graphs": [
{
"name": "cam1_security_pipeline",
"nodes": [
{
"id": "in_cam1",
"type": "input_rtsp",
"role": "source",
"enable": true,
"url": "rtsp://192.168.1.100:554/stream",
"fps": 25,
"width": 1920,
"height": 1080,
"use_mpp": false,
"use_ffmpeg": true,
"force_tcp": true
},
{
"id": "pre_cam1",
"type": "preprocess",
"role": "filter",
"enable": true,
"dst_w": 640,
"dst_h": 640,
"dst_format": "rgb",
"keep_ratio": false,
"use_rga": true
},
{
"id": "ai_cam1",
"type": "ai_yolo",
"role": "filter",
"enable": true,
"model_path": "/models/yolov8n.rknn",
"model_version": "v8",
"num_classes": 80,
"conf": 0.5,
"nms": 0.45,
"class_filter": []
},
{
"id": "alarm_cam1",
"type": "alarm",
"role": "sink",
"enable": true,
"labels": [],
"rules": [
{
"name": "object_detection",
"class_ids": [0, 1, 2],
"roi": { "x": 0.1, "y": 0.1, "w": 0.8, "h": 0.8 },
"min_duration_ms": 500,
"cooldown_ms": 5000
}
],
"actions": {
"log": { "enable": true, "level": "info" },
"http": {
"enable": false,
"url": "http://localhost:8080/api/alarm",
"timeout_ms": 3000,
"include_media_url": true
},
"snapshot": {
"enable": true,
"format": "jpg",
"quality": 85,
"upload": {
"type": "local",
"path": "/tmp/alarms"
}
},
"clip": {
"enable": false,
"pre_sec": 5,
"post_sec": 10,
"format": "mp4",
"fps": 25,
"upload": {
"type": "local",
"path": "/tmp/alarms"
}
}
}
},
{
"id": "osd_cam1",
"type": "osd",
"role": "filter",
"enable": true,
"draw_bbox": true,
"draw_text": true,
"line_width": 2,
"font_scale": 1,
"labels": []
},
{
"id": "post_cam1",
"type": "preprocess",
"role": "filter",
"enable": true,
"dst_w": 1920,
"dst_h": 1080,
"dst_format": "nv12",
"keep_ratio": false,
"use_rga": true
},
{
"id": "storage_cam1",
"type": "storage",
"role": "sink",
"enable": true,
"mode": "continuous",
"format": "mp4",
"codec": "h264",
"segment_sec": 300,
"path": "/rec/cam1",
"filename_pattern": "%Y%m%d/%H%M%S",
"fps": 25,
"bitrate_kbps": 2000
},
{
"id": "pub_cam1",
"type": "publish",
"role": "sink",
"enable": true,
"codec": "h264",
"fps": 25,
"gop": 50,
"bitrate_kbps": 2000,
"use_mpp": true,
"use_ffmpeg_mux": true,
"outputs": [
{ "proto": "rtsp_server", "port": 8554, "path": "/live/cam1" }
]
}
],
"edges": [
["in_cam1", "pre_cam1"],
["pre_cam1", "ai_cam1"],
["ai_cam1", "alarm_cam1"],
["ai_cam1", "osd_cam1"],
["osd_cam1", "post_cam1"],
["post_cam1", "storage_cam1"],
["post_cam1", "pub_cam1"]
]
}
]
}

View File

@ -203,7 +203,73 @@ set_target_properties(osd PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
install(TARGETS input_rtsp publish preprocess ai_yolo osd ai_scheduler
# alarm plugin (rule-based alarm with actions: log, http, snapshot, clip)
add_library(alarm SHARED
alarm/alarm_node.cpp
alarm/rule_engine.cpp
alarm/frame_ring_buffer.cpp
alarm/actions/log_action.cpp
alarm/actions/http_action.cpp
alarm/actions/snapshot_action.cpp
alarm/actions/clip_action.cpp
alarm/uploaders/local_uploader.cpp
alarm/uploaders/minio_uploader.cpp
alarm/uploaders/uploader_factory.cpp
)
target_include_directories(alarm PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/third_party
${CMAKE_CURRENT_SOURCE_DIR}/alarm
)
target_link_libraries(alarm PRIVATE project_options Threads::Threads)
if(RK3588_ENABLE_FFMPEG)
target_compile_definitions(alarm PRIVATE RK3588_ENABLE_FFMPEG)
target_include_directories(alarm PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_libraries(alarm PRIVATE PkgConfig::FFMPEG)
# Also need libswscale for image conversion
pkg_check_modules(SWSCALE IMPORTED_TARGET libswscale)
if(SWSCALE_FOUND)
target_link_libraries(alarm PRIVATE PkgConfig::SWSCALE)
endif()
endif()
# Optional: libcurl for HTTP action (on Linux)
find_package(CURL QUIET)
if(CURL_FOUND)
target_link_libraries(alarm PRIVATE CURL::libcurl)
endif()
set_target_properties(alarm PROPERTIES
OUTPUT_NAME "alarm"
LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
# storage plugin (continuous recording with segment management)
add_library(storage SHARED storage/storage_node.cpp)
target_include_directories(storage PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party)
target_link_libraries(storage PRIVATE project_options Threads::Threads)
if(RK3588_ENABLE_FFMPEG)
target_compile_definitions(storage PRIVATE RK3588_ENABLE_FFMPEG)
target_include_directories(storage PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_libraries(storage PRIVATE PkgConfig::FFMPEG)
if(SWSCALE_FOUND)
target_link_libraries(storage PRIVATE PkgConfig::SWSCALE)
endif()
endif()
if(RK3588_ENABLE_MPP AND RK_MPP_LIB)
target_compile_definitions(storage PRIVATE RK3588_ENABLE_MPP)
target_include_directories(storage PRIVATE
${RK_MPP_ROOT}/inc
${RK_RKNN_ROOT}/examples/3rdparty/mpp/include
)
target_link_libraries(storage PRIVATE ${RK_MPP_LIB})
endif()
set_target_properties(storage PROPERTIES
OUTPUT_NAME "storage"
LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR}
)
install(TARGETS input_rtsp publish preprocess ai_yolo osd alarm storage ai_scheduler
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
)

View File

@ -0,0 +1,31 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "frame/frame.h"
#include "utils/simple_json.h"
namespace rk3588 {
struct AlarmEvent {
std::string rule_name;
std::string node_id;
uint64_t timestamp_ms;
uint64_t frame_id;
std::vector<Detection> detections;
std::string snapshot_url;
std::string clip_url;
};
class IAlarmAction {
public:
virtual ~IAlarmAction() = default;
virtual bool Init(const SimpleJson& config) = 0;
virtual void Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) = 0;
virtual void Drain() {}
virtual std::string Name() const = 0;
};
} // namespace rk3588

View File

@ -0,0 +1,321 @@
#include "clip_action.h"
#include <chrono>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>
#if defined(RK3588_ENABLE_FFMPEG)
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
}
#define HAS_FFMPEG 1
#else
#define HAS_FFMPEG 0
#endif
namespace rk3588 {
ClipAction::~ClipAction() {
Drain();
running_.store(false);
queue_cv_.notify_all();
if (worker_.joinable()) worker_.join();
}
bool ClipAction::Init(const SimpleJson& config) {
pre_sec_ = config.ValueOr<int>("pre_sec", 5);
post_sec_ = config.ValueOr<int>("post_sec", 10);
format_ = config.ValueOr<std::string>("format", "mp4");
fps_ = config.ValueOr<int>("fps", 25);
if (const SimpleJson* upload_cfg = config.Find("upload")) {
uploader_ = CreateUploader(*upload_cfg);
if (!uploader_) {
std::cerr << "[ClipAction] failed to create uploader\n";
return false;
}
} else {
SimpleJson default_cfg;
uploader_ = CreateUploader(default_cfg);
}
running_.store(true);
worker_ = std::thread(&ClipAction::WorkerLoop, this);
std::cout << "[ClipAction] initialized, pre=" << pre_sec_ << "s post=" << post_sec_
<< "s format=" << format_ << "\n";
return true;
}
void ClipAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) {
ClipTask task;
task.event = event;
task.trigger_frame = frame;
// Get pre-event frames
if (frame_buffer_) {
task.pre_frames = frame_buffer_->GetPreEventFrames();
}
// Start collecting post-event frames
{
std::lock_guard<std::mutex> lock(post_mutex_);
collecting_post_ = true;
post_frames_.clear();
post_frames_needed_ = post_sec_ * fps_;
}
// Wait for post-event frames (simplified: just queue the task)
// In a real implementation, we'd collect post-event frames asynchronously
{
std::lock_guard<std::mutex> lock(queue_mutex_);
task_queue_.push(std::move(task));
}
queue_cv_.notify_one();
}
void ClipAction::PushPostEventFrame(std::shared_ptr<Frame> frame) {
std::lock_guard<std::mutex> lock(post_mutex_);
if (!collecting_post_) return;
post_frames_.push_back(frame);
if (static_cast<int>(post_frames_.size()) >= post_frames_needed_) {
collecting_post_ = false;
}
}
void ClipAction::Drain() {
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait_for(lock, std::chrono::seconds(30),
[this] { return task_queue_.empty(); });
}
void ClipAction::WorkerLoop() {
while (running_.load()) {
ClipTask task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait_for(lock, std::chrono::milliseconds(100),
[this] { return !task_queue_.empty() || !running_.load(); });
if (!running_.load() && task_queue_.empty()) break;
if (task_queue_.empty()) continue;
task = std::move(task_queue_.front());
task_queue_.pop();
}
// Wait a bit for post-event frames if needed
if (post_sec_ > 0) {
std::this_thread::sleep_for(std::chrono::seconds(post_sec_));
}
// Get collected post-event frames
{
std::lock_guard<std::mutex> lock(post_mutex_);
// In real impl, we'd associate frames with the task
collecting_post_ = false;
}
std::string url = ProcessClip(task);
if (!url.empty()) {
std::cout << "[ClipAction] clip uploaded: " << url << "\n";
}
}
}
std::string ClipAction::ProcessClip(ClipTask& task) {
#if HAS_FFMPEG
if (task.pre_frames.empty() && !task.trigger_frame) {
std::cerr << "[ClipAction] no frames to process\n";
return "";
}
// Determine frame dimensions from first available frame
std::shared_ptr<Frame> sample_frame =
task.trigger_frame ? task.trigger_frame
: (task.pre_frames.empty() ? nullptr : task.pre_frames[0]);
if (!sample_frame) return "";
int width = sample_frame->width;
int height = sample_frame->height;
// Create temporary file
std::string tmp_path = "/tmp/clip_" + std::to_string(task.event.timestamp_ms) + "." + format_;
// Set up FFmpeg output
AVFormatContext* fmt_ctx = nullptr;
if (avformat_alloc_output_context2(&fmt_ctx, nullptr, format_.c_str(), tmp_path.c_str()) < 0) {
std::cerr << "[ClipAction] failed to create output context\n";
return "";
}
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
avformat_free_context(fmt_ctx);
return "";
}
AVStream* stream = avformat_new_stream(fmt_ctx, codec);
if (!stream) {
avformat_free_context(fmt_ctx);
return "";
}
AVCodecContext* enc_ctx = avcodec_alloc_context3(codec);
enc_ctx->width = width;
enc_ctx->height = height;
enc_ctx->time_base = AVRational{1, fps_};
enc_ctx->framerate = AVRational{fps_, 1};
enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
enc_ctx->bit_rate = 2000000;
enc_ctx->gop_size = fps_;
if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
if (avcodec_open2(enc_ctx, codec, nullptr) < 0) {
avcodec_free_context(&enc_ctx);
avformat_free_context(fmt_ctx);
return "";
}
avcodec_parameters_from_context(stream->codecpar, enc_ctx);
stream->time_base = enc_ctx->time_base;
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&fmt_ctx->pb, tmp_path.c_str(), AVIO_FLAG_WRITE) < 0) {
avcodec_free_context(&enc_ctx);
avformat_free_context(fmt_ctx);
return "";
}
}
if (avformat_write_header(fmt_ctx, nullptr) < 0) {
avcodec_free_context(&enc_ctx);
avformat_free_context(fmt_ctx);
return "";
}
// Encode frames
AVFrame* av_frame = av_frame_alloc();
av_frame->width = width;
av_frame->height = height;
av_frame->format = AV_PIX_FMT_YUV420P;
av_frame_get_buffer(av_frame, 32);
AVPacket* pkt = av_packet_alloc();
int64_t pts = 0;
auto encode_frame = [&](std::shared_ptr<Frame>& frame) {
if (!frame || !frame->data) return;
// Convert to YUV420P (simplified - assumes RGB input)
AVPixelFormat src_fmt = AV_PIX_FMT_RGB24;
if (frame->format == PixelFormat::BGR) src_fmt = AV_PIX_FMT_BGR24;
else if (frame->format == PixelFormat::NV12) src_fmt = AV_PIX_FMT_NV12;
SwsContext* sws = sws_getContext(width, height, src_fmt,
width, height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (sws) {
uint8_t* src_data[4] = {frame->data, nullptr, nullptr, nullptr};
int src_linesize[4] = {width * 3, 0, 0, 0};
if (frame->format == PixelFormat::NV12) {
src_data[1] = frame->data + width * height;
src_linesize[0] = width;
src_linesize[1] = width;
}
sws_scale(sws, src_data, src_linesize, 0, height,
av_frame->data, av_frame->linesize);
sws_freeContext(sws);
}
av_frame->pts = pts++;
avcodec_send_frame(enc_ctx, av_frame);
while (avcodec_receive_packet(enc_ctx, pkt) == 0) {
av_packet_rescale_ts(pkt, enc_ctx->time_base, stream->time_base);
pkt->stream_index = stream->index;
av_interleaved_write_frame(fmt_ctx, pkt);
av_packet_unref(pkt);
}
};
// Encode pre-event frames
for (auto& f : task.pre_frames) {
encode_frame(f);
}
// Encode trigger frame
encode_frame(task.trigger_frame);
// Flush encoder
avcodec_send_frame(enc_ctx, nullptr);
while (avcodec_receive_packet(enc_ctx, pkt) == 0) {
av_packet_rescale_ts(pkt, enc_ctx->time_base, stream->time_base);
pkt->stream_index = stream->index;
av_interleaved_write_frame(fmt_ctx, pkt);
av_packet_unref(pkt);
}
av_write_trailer(fmt_ctx);
av_packet_free(&pkt);
av_frame_free(&av_frame);
avcodec_free_context(&enc_ctx);
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&fmt_ctx->pb);
}
avformat_free_context(fmt_ctx);
// Read file and upload
std::ifstream file(tmp_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ClipAction] failed to read temp file\n";
return "";
}
size_t file_size = file.tellg();
file.seekg(0);
std::vector<uint8_t> file_data(file_size);
file.read(reinterpret_cast<char*>(file_data.data()), file_size);
file.close();
std::string key = GenerateKey(task.event);
auto result = uploader_->Upload(key, file_data.data(), file_data.size(), "video/mp4");
// Clean up temp file
std::filesystem::remove(tmp_path);
if (result.success) {
return result.url;
}
std::cerr << "[ClipAction] upload failed: " << result.error << "\n";
#else
std::cerr << "[ClipAction] FFmpeg not enabled\n";
#endif
return "";
}
std::string ClipAction::GenerateKey(const AlarmEvent& event) {
auto now = std::chrono::system_clock::now();
auto time_t_now = std::chrono::system_clock::to_time_t(now);
std::tm* tm = std::localtime(&time_t_now);
std::ostringstream oss;
oss << std::put_time(tm, "%Y%m%d") << "/"
<< event.node_id << "_"
<< std::put_time(tm, "%H%M%S") << "_"
<< event.frame_id << "." << format_;
return oss.str();
}
} // namespace rk3588

View File

@ -0,0 +1,62 @@
#pragma once
#include "action_base.h"
#include "../uploaders/uploader_base.h"
#include "../frame_ring_buffer.h"
#include <atomic>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
namespace rk3588 {
struct ClipTask {
AlarmEvent event;
std::vector<std::shared_ptr<Frame>> pre_frames;
std::shared_ptr<Frame> trigger_frame;
};
class ClipAction : public IAlarmAction {
public:
~ClipAction();
bool Init(const SimpleJson& config) override;
void Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) override;
void Drain() override;
std::string Name() const override { return "clip"; }
void SetFrameBuffer(std::shared_ptr<FrameRingBuffer> buffer) {
frame_buffer_ = buffer;
}
void PushPostEventFrame(std::shared_ptr<Frame> frame);
private:
void WorkerLoop();
std::string ProcessClip(ClipTask& task);
std::string GenerateKey(const AlarmEvent& event);
int pre_sec_ = 5;
int post_sec_ = 10;
std::string format_ = "mp4";
int fps_ = 25;
std::unique_ptr<IUploader> uploader_;
std::shared_ptr<FrameRingBuffer> frame_buffer_;
std::atomic<bool> running_{false};
std::thread worker_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::queue<ClipTask> task_queue_;
// Post-event collection state
std::mutex post_mutex_;
bool collecting_post_ = false;
std::vector<std::shared_ptr<Frame>> post_frames_;
int post_frames_needed_ = 0;
ClipTask* current_task_ = nullptr;
};
} // namespace rk3588

View File

@ -0,0 +1,145 @@
#include "http_action.h"
#include <chrono>
#include <ctime>
#include <iostream>
#include <sstream>
#if defined(__linux__) || defined(__APPLE__)
#include <curl/curl.h>
#define HAS_CURL 1
#else
#define HAS_CURL 0
#endif
namespace rk3588 {
HttpAction::~HttpAction() {
Drain();
running_.store(false);
queue_cv_.notify_all();
if (worker_.joinable()) worker_.join();
}
bool HttpAction::Init(const SimpleJson& config) {
url_ = config.ValueOr<std::string>("url", "");
timeout_ms_ = config.ValueOr<int>("timeout_ms", 3000);
include_media_url_ = config.ValueOr<bool>("include_media_url", true);
method_ = config.ValueOr<std::string>("method", "POST");
if (url_.empty()) {
std::cerr << "[HttpAction] url is required\n";
return false;
}
#if HAS_CURL
curl_global_init(CURL_GLOBAL_DEFAULT);
#endif
running_.store(true);
worker_ = std::thread(&HttpAction::WorkerLoop, this);
std::cout << "[HttpAction] initialized, url=" << url_ << "\n";
return true;
}
void HttpAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> /*frame*/) {
// Build JSON body
std::ostringstream oss;
oss << "{";
oss << "\"node_id\":\"" << event.node_id << "\",";
oss << "\"rule_name\":\"" << event.rule_name << "\",";
oss << "\"timestamp\":" << event.timestamp_ms << ",";
oss << "\"frame_id\":" << event.frame_id << ",";
if (include_media_url_) {
if (!event.snapshot_url.empty()) {
oss << "\"snapshot_url\":\"" << event.snapshot_url << "\",";
}
if (!event.clip_url.empty()) {
oss << "\"clip_url\":\"" << event.clip_url << "\",";
}
}
oss << "\"detections\":[";
for (size_t i = 0; i < event.detections.size(); ++i) {
const auto& det = event.detections[i];
if (i > 0) oss << ",";
oss << "{\"cls_id\":" << det.cls_id
<< ",\"score\":" << det.score
<< ",\"bbox\":{\"x\":" << det.bbox.x
<< ",\"y\":" << det.bbox.y
<< ",\"w\":" << det.bbox.w
<< ",\"h\":" << det.bbox.h << "}"
<< ",\"track_id\":" << det.track_id << "}";
}
oss << "]}";
std::string json_body = oss.str();
{
std::lock_guard<std::mutex> lock(queue_mutex_);
request_queue_.push(json_body);
}
queue_cv_.notify_one();
}
void HttpAction::Drain() {
// Wait for queue to empty
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait_for(lock, std::chrono::seconds(5),
[this] { return request_queue_.empty(); });
}
void HttpAction::WorkerLoop() {
while (running_.load()) {
std::string body;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait_for(lock, std::chrono::milliseconds(100),
[this] { return !request_queue_.empty() || !running_.load(); });
if (!running_.load() && request_queue_.empty()) break;
if (request_queue_.empty()) continue;
body = std::move(request_queue_.front());
request_queue_.pop();
}
if (!body.empty()) {
SendRequest(body);
}
}
}
bool HttpAction::SendRequest(const std::string& json_body) {
#if HAS_CURL
CURL* curl = curl_easy_init();
if (!curl) {
std::cerr << "[HttpAction] curl_easy_init failed\n";
return false;
}
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url_.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body.c_str());
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms_);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms_ / 2);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
std::cerr << "[HttpAction] request failed: " << curl_easy_strerror(res) << "\n";
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return res == CURLE_OK;
#else
// Stub implementation for Windows without curl
std::cout << "[HttpAction] HTTP POST to " << url_ << "\n"
<< " Body: " << json_body << "\n";
return true;
#endif
}
} // namespace rk3588

View File

@ -0,0 +1,37 @@
#pragma once
#include "action_base.h"
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
namespace rk3588 {
class HttpAction : public IAlarmAction {
public:
~HttpAction();
bool Init(const SimpleJson& config) override;
void Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) override;
void Drain() override;
std::string Name() const override { return "http"; }
private:
void WorkerLoop();
bool SendRequest(const std::string& json_body);
std::string url_;
int timeout_ms_ = 3000;
bool include_media_url_ = true;
std::string method_ = "POST";
std::atomic<bool> running_{false};
std::thread worker_;
std::mutex queue_mutex_;
std::condition_variable queue_cv_;
std::queue<std::string> request_queue_;
};
} // namespace rk3588

View File

@ -0,0 +1,55 @@
#include "log_action.h"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
namespace rk3588 {
bool LogAction::Init(const SimpleJson& config) {
level_ = config.ValueOr<std::string>("level", "info");
include_detections_ = config.ValueOr<bool>("include_detections", true);
std::cout << "[LogAction] initialized, level=" << level_ << "\n";
return true;
}
void LogAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> /*frame*/) {
auto now = std::chrono::system_clock::now();
auto time_t_now = std::chrono::system_clock::to_time_t(now);
std::tm* tm_now = std::localtime(&time_t_now);
std::ostringstream oss;
oss << "[ALARM][" << level_ << "] "
<< std::put_time(tm_now, "%Y-%m-%d %H:%M:%S")
<< " node=" << event.node_id
<< " rule=" << event.rule_name
<< " frame=" << event.frame_id;
if (include_detections_ && !event.detections.empty()) {
oss << " detections=[";
for (size_t i = 0; i < event.detections.size(); ++i) {
const auto& det = event.detections[i];
if (i > 0) oss << ", ";
oss << "{cls=" << det.cls_id
<< " score=" << std::fixed << std::setprecision(2) << det.score
<< " bbox=(" << static_cast<int>(det.bbox.x) << ","
<< static_cast<int>(det.bbox.y) << ","
<< static_cast<int>(det.bbox.w) << ","
<< static_cast<int>(det.bbox.h) << ")}";
}
oss << "]";
}
if (!event.snapshot_url.empty()) {
oss << " snapshot=" << event.snapshot_url;
}
if (!event.clip_url.empty()) {
oss << " clip=" << event.clip_url;
}
std::cout << oss.str() << "\n";
}
} // namespace rk3588

View File

@ -0,0 +1,18 @@
#pragma once
#include "action_base.h"
namespace rk3588 {
class LogAction : public IAlarmAction {
public:
bool Init(const SimpleJson& config) override;
void Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) override;
std::string Name() const override { return "log"; }
private:
std::string level_ = "info";
bool include_detections_ = true;
};
} // namespace rk3588

View File

@ -0,0 +1,193 @@
#include "snapshot_action.h"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#if defined(RK3588_ENABLE_FFMPEG)
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#define HAS_FFMPEG 1
#else
#define HAS_FFMPEG 0
#endif
namespace rk3588 {
bool SnapshotAction::Init(const SimpleJson& config) {
format_ = config.ValueOr<std::string>("format", "jpg");
quality_ = config.ValueOr<int>("quality", 85);
if (const SimpleJson* upload_cfg = config.Find("upload")) {
uploader_ = CreateUploader(*upload_cfg);
if (!uploader_) {
std::cerr << "[SnapshotAction] failed to create uploader\n";
return false;
}
} else {
// Default to local storage
SimpleJson default_cfg;
uploader_ = CreateUploader(default_cfg);
}
std::cout << "[SnapshotAction] initialized, format=" << format_
<< " quality=" << quality_ << "\n";
return true;
}
void SnapshotAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) {
if (!frame || !frame->data) {
std::cerr << "[SnapshotAction] no frame data\n";
return;
}
auto jpeg_data = EncodeJpeg(frame);
if (jpeg_data.empty()) {
std::cerr << "[SnapshotAction] failed to encode JPEG\n";
return;
}
std::string key = GenerateKey(event);
auto result = uploader_->Upload(key, jpeg_data.data(), jpeg_data.size(), "image/jpeg");
if (result.success) {
event.snapshot_url = result.url;
std::cout << "[SnapshotAction] uploaded: " << result.url << "\n";
} else {
std::cerr << "[SnapshotAction] upload failed: " << result.error << "\n";
}
}
std::string SnapshotAction::GenerateKey(const AlarmEvent& event) {
auto now = std::chrono::system_clock::now();
auto time_t_now = std::chrono::system_clock::to_time_t(now);
std::tm* tm = std::localtime(&time_t_now);
std::ostringstream oss;
oss << std::put_time(tm, "%Y%m%d") << "/"
<< event.node_id << "_"
<< std::put_time(tm, "%H%M%S") << "_"
<< event.frame_id << "." << format_;
return oss.str();
}
std::vector<uint8_t> SnapshotAction::EncodeJpeg(const std::shared_ptr<Frame>& frame) {
std::vector<uint8_t> output;
#if HAS_FFMPEG
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
if (!codec) {
std::cerr << "[SnapshotAction] MJPEG encoder not found\n";
return output;
}
AVCodecContext* ctx = avcodec_alloc_context3(codec);
if (!ctx) return output;
ctx->width = frame->width;
ctx->height = frame->height;
ctx->time_base = AVRational{1, 25};
ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;
// Set quality (1-31, lower is better for MJPEG)
int q = 31 - (quality_ * 30 / 100);
ctx->qmin = q;
ctx->qmax = q;
if (avcodec_open2(ctx, codec, nullptr) < 0) {
avcodec_free_context(&ctx);
return output;
}
AVFrame* av_frame = av_frame_alloc();
av_frame->width = frame->width;
av_frame->height = frame->height;
av_frame->format = AV_PIX_FMT_YUVJ420P;
if (av_frame_get_buffer(av_frame, 32) < 0) {
av_frame_free(&av_frame);
avcodec_free_context(&ctx);
return output;
}
// Convert input frame to YUV420P
AVPixelFormat src_fmt = AV_PIX_FMT_NONE;
if (frame->format == PixelFormat::RGB) {
src_fmt = AV_PIX_FMT_RGB24;
} else if (frame->format == PixelFormat::BGR) {
src_fmt = AV_PIX_FMT_BGR24;
} else if (frame->format == PixelFormat::NV12) {
src_fmt = AV_PIX_FMT_NV12;
} else if (frame->format == PixelFormat::YUV420) {
src_fmt = AV_PIX_FMT_YUV420P;
}
if (src_fmt == AV_PIX_FMT_NONE) {
av_frame_free(&av_frame);
avcodec_free_context(&ctx);
return output;
}
SwsContext* sws = sws_getContext(
frame->width, frame->height, src_fmt,
frame->width, frame->height, AV_PIX_FMT_YUVJ420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!sws) {
av_frame_free(&av_frame);
avcodec_free_context(&ctx);
return output;
}
// Set up source pointers
uint8_t* src_data[4] = {nullptr};
int src_linesize[4] = {0};
if (frame->format == PixelFormat::RGB || frame->format == PixelFormat::BGR) {
src_data[0] = frame->data;
src_linesize[0] = frame->width * 3;
} else if (frame->format == PixelFormat::NV12) {
src_data[0] = frame->planes[0].data ? frame->planes[0].data : frame->data;
src_data[1] = frame->planes[1].data ? frame->planes[1].data
: frame->data + frame->width * frame->height;
src_linesize[0] = frame->planes[0].stride > 0 ? frame->planes[0].stride : frame->width;
src_linesize[1] = frame->planes[1].stride > 0 ? frame->planes[1].stride : frame->width;
} else if (frame->format == PixelFormat::YUV420) {
src_data[0] = frame->planes[0].data ? frame->planes[0].data : frame->data;
src_data[1] = frame->planes[1].data;
src_data[2] = frame->planes[2].data;
src_linesize[0] = frame->planes[0].stride > 0 ? frame->planes[0].stride : frame->width;
src_linesize[1] = frame->planes[1].stride > 0 ? frame->planes[1].stride : frame->width / 2;
src_linesize[2] = frame->planes[2].stride > 0 ? frame->planes[2].stride : frame->width / 2;
}
sws_scale(sws, src_data, src_linesize, 0, frame->height,
av_frame->data, av_frame->linesize);
sws_freeContext(sws);
av_frame->pts = 0;
AVPacket* pkt = av_packet_alloc();
if (avcodec_send_frame(ctx, av_frame) == 0) {
if (avcodec_receive_packet(ctx, pkt) == 0) {
output.assign(pkt->data, pkt->data + pkt->size);
}
}
av_packet_free(&pkt);
av_frame_free(&av_frame);
avcodec_free_context(&ctx);
#else
// Stub: just create a minimal valid JPEG header for testing
std::cerr << "[SnapshotAction] FFmpeg not enabled, cannot encode JPEG\n";
#endif
return output;
}
} // namespace rk3588

View File

@ -0,0 +1,25 @@
#pragma once
#include "action_base.h"
#include "../uploaders/uploader_base.h"
#include <memory>
namespace rk3588 {
class SnapshotAction : public IAlarmAction {
public:
bool Init(const SimpleJson& config) override;
void Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) override;
std::string Name() const override { return "snapshot"; }
private:
std::vector<uint8_t> EncodeJpeg(const std::shared_ptr<Frame>& frame);
std::string GenerateKey(const AlarmEvent& event);
std::string format_ = "jpg";
int quality_ = 85;
std::unique_ptr<IUploader> uploader_;
};
} // namespace rk3588

View File

@ -0,0 +1,205 @@
#include <atomic>
#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include "node.h"
#include "rule_engine.h"
#include "frame_ring_buffer.h"
#include "actions/action_base.h"
#include "actions/log_action.h"
#include "actions/http_action.h"
#include "actions/snapshot_action.h"
#include "actions/clip_action.h"
namespace rk3588 {
class AlarmNode : public INode {
public:
std::string Id() const override { return id_; }
std::string Type() const override { return "alarm"; }
bool Init(const SimpleJson& config, const NodeContext& ctx) override {
id_ = config.ValueOr<std::string>("id", "alarm");
// Parse labels for class name mapping
if (const SimpleJson* labels_cfg = config.Find("labels")) {
for (const auto& label : labels_cfg->AsArray()) {
labels_.push_back(label.AsString(""));
}
}
// Initialize rule engine
if (const SimpleJson* rules_cfg = config.Find("rules")) {
if (!rule_engine_.Init(*rules_cfg, labels_)) {
std::cerr << "[alarm] failed to init rule engine\n";
return false;
}
}
// Get pre-event buffer settings from clip action config
int pre_sec = 5;
int fps_hint = 25;
if (const SimpleJson* actions_cfg = config.Find("actions")) {
if (const SimpleJson* clip_cfg = actions_cfg->Find("clip")) {
pre_sec = clip_cfg->ValueOr<int>("pre_sec", 5);
fps_hint = clip_cfg->ValueOr<int>("fps", 25);
}
}
frame_buffer_ = std::make_shared<FrameRingBuffer>(pre_sec, fps_hint);
// Initialize actions
if (const SimpleJson* actions_cfg = config.Find("actions")) {
// Log action
if (const SimpleJson* log_cfg = actions_cfg->Find("log")) {
if (log_cfg->ValueOr<bool>("enable", true)) {
auto action = std::make_unique<LogAction>();
if (action->Init(*log_cfg)) {
actions_.push_back(std::move(action));
}
}
}
// Snapshot action (must be before HTTP to fill snapshot_url)
if (const SimpleJson* snap_cfg = actions_cfg->Find("snapshot")) {
if (snap_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<SnapshotAction>();
if (action->Init(*snap_cfg)) {
actions_.push_back(std::move(action));
}
}
}
// Clip action (must be before HTTP to fill clip_url)
if (const SimpleJson* clip_cfg = actions_cfg->Find("clip")) {
if (clip_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<ClipAction>();
if (action->Init(*clip_cfg)) {
auto* clip_ptr = static_cast<ClipAction*>(action.get());
clip_ptr->SetFrameBuffer(frame_buffer_);
clip_action_ = clip_ptr;
actions_.push_back(std::move(action));
}
}
}
// HTTP action (should be last to include media URLs)
if (const SimpleJson* http_cfg = actions_cfg->Find("http")) {
if (http_cfg->ValueOr<bool>("enable", false)) {
auto action = std::make_unique<HttpAction>();
if (action->Init(*http_cfg)) {
actions_.push_back(std::move(action));
}
}
}
} else {
// Default: just log action
auto action = std::make_unique<LogAction>();
SimpleJson empty_cfg;
action->Init(empty_cfg);
actions_.push_back(std::move(action));
}
input_queue_ = ctx.input_queue;
if (!input_queue_) {
std::cerr << "[alarm] no input queue for node " << id_ << "\n";
return false;
}
std::cout << "[alarm] initialized with " << actions_.size() << " actions\n";
return true;
}
bool Start() override {
if (!input_queue_) return false;
running_.store(true);
worker_ = std::thread(&AlarmNode::WorkerLoop, this);
std::cout << "[alarm] started\n";
return true;
}
void Stop() override {
running_.store(false);
if (input_queue_) input_queue_->Stop();
if (worker_.joinable()) worker_.join();
// Drain all actions
for (auto& action : actions_) {
action->Drain();
}
std::cout << "[alarm] stopped, processed " << processed_frames_
<< " frames, triggered " << alarm_count_ << " alarms\n";
}
void Drain() override {
for (auto& action : actions_) {
action->Drain();
}
}
private:
void WorkerLoop() {
using namespace std::chrono;
FramePtr frame;
while (running_.load()) {
if (!input_queue_->Pop(frame, milliseconds(200))) continue;
if (!frame) continue;
// Always push to ring buffer for pre-event recording
frame_buffer_->Push(frame);
// Push to clip action for post-event collection if active
if (clip_action_) {
clip_action_->PushPostEventFrame(frame);
}
// Evaluate rules
auto result = rule_engine_.Evaluate(frame);
if (result.matched) {
TriggerAlarm(result, frame);
}
++processed_frames_;
}
}
void TriggerAlarm(const RuleMatchResult& result, FramePtr frame) {
++alarm_count_;
AlarmEvent event;
event.node_id = id_;
event.rule_name = result.rule_name;
event.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
event.frame_id = frame->frame_id;
event.detections = result.matched_detections;
// Execute all actions in order
// Snapshot and Clip actions will fill snapshot_url and clip_url
// HTTP action should be last to include all URLs
for (auto& action : actions_) {
action->Execute(event, frame);
}
}
std::string id_;
std::vector<std::string> labels_;
RuleEngine rule_engine_;
std::shared_ptr<FrameRingBuffer> frame_buffer_;
std::vector<std::unique_ptr<IAlarmAction>> actions_;
ClipAction* clip_action_ = nullptr;
std::atomic<bool> running_{false};
std::shared_ptr<SpscQueue<FramePtr>> input_queue_;
std::thread worker_;
uint64_t processed_frames_ = 0;
uint64_t alarm_count_ = 0;
};
REGISTER_NODE(AlarmNode, "alarm");
} // namespace rk3588

View File

@ -0,0 +1,36 @@
#include "frame_ring_buffer.h"
namespace rk3588 {
FrameRingBuffer::FrameRingBuffer(int pre_event_sec, int fps_hint)
: pre_event_sec_(pre_event_sec), fps_hint_(fps_hint > 0 ? fps_hint : 25) {
max_frames_ = static_cast<size_t>(pre_event_sec_ * fps_hint_);
if (max_frames_ < 10) max_frames_ = 10;
}
void FrameRingBuffer::Push(std::shared_ptr<Frame> frame) {
if (!frame) return;
std::lock_guard<std::mutex> lock(mutex_);
buffer_.push_back(frame);
while (buffer_.size() > max_frames_) {
buffer_.pop_front();
}
}
std::vector<std::shared_ptr<Frame>> FrameRingBuffer::GetPreEventFrames() const {
std::lock_guard<std::mutex> lock(mutex_);
return std::vector<std::shared_ptr<Frame>>(buffer_.begin(), buffer_.end());
}
void FrameRingBuffer::Clear() {
std::lock_guard<std::mutex> lock(mutex_);
buffer_.clear();
}
size_t FrameRingBuffer::Size() const {
std::lock_guard<std::mutex> lock(mutex_);
return buffer_.size();
}
} // namespace rk3588

View File

@ -0,0 +1,30 @@
#pragma once
#include <chrono>
#include <deque>
#include <memory>
#include <mutex>
#include <vector>
#include "frame/frame.h"
namespace rk3588 {
class FrameRingBuffer {
public:
explicit FrameRingBuffer(int pre_event_sec = 5, int fps_hint = 25);
void Push(std::shared_ptr<Frame> frame);
std::vector<std::shared_ptr<Frame>> GetPreEventFrames() const;
void Clear();
size_t Size() const;
private:
mutable std::mutex mutex_;
std::deque<std::shared_ptr<Frame>> buffer_;
size_t max_frames_;
int pre_event_sec_;
int fps_hint_;
};
} // namespace rk3588

View File

@ -0,0 +1,206 @@
#include "rule_engine.h"
#include <algorithm>
#include <ctime>
#include <iostream>
#include <sstream>
namespace rk3588 {
bool RuleEngine::Init(const SimpleJson& rules_config, const std::vector<std::string>& labels) {
labels_ = labels;
rules_.clear();
duration_start_.clear();
last_trigger_.clear();
if (!rules_config.IsArray()) {
std::cerr << "[RuleEngine] rules must be an array\n";
return false;
}
for (const auto& rule_json : rules_config.AsArray()) {
AlarmRule rule;
rule.name = rule_json.ValueOr<std::string>("name", "unnamed_rule");
rule.min_duration_ms = rule_json.ValueOr<int>("min_duration_ms", 0);
rule.cooldown_ms = rule_json.ValueOr<int>("cooldown_ms", 5000);
rule.schedule = rule_json.ValueOr<std::string>("schedule", "");
// Parse class_ids
if (const SimpleJson* ids = rule_json.Find("class_ids")) {
for (const auto& id_val : ids->AsArray()) {
rule.class_ids.insert(id_val.AsInt(-1));
}
}
// Parse objects (class names) and convert to class_ids
if (const SimpleJson* objects = rule_json.Find("objects")) {
for (const auto& obj_val : objects->AsArray()) {
std::string obj_name = obj_val.AsString("");
for (size_t i = 0; i < labels_.size(); ++i) {
if (labels_[i] == obj_name) {
rule.class_ids.insert(static_cast<int>(i));
break;
}
}
}
}
// Parse ROI
if (const SimpleJson* roi_json = rule_json.Find("roi")) {
rule.roi.x = roi_json->ValueOr<float>("x", 0.0f);
rule.roi.y = roi_json->ValueOr<float>("y", 0.0f);
rule.roi.w = roi_json->ValueOr<float>("w", 1.0f);
rule.roi.h = roi_json->ValueOr<float>("h", 1.0f);
}
rules_.push_back(rule);
std::cout << "[RuleEngine] loaded rule: " << rule.name
<< " class_ids=" << rule.class_ids.size()
<< " roi=(" << rule.roi.x << "," << rule.roi.y << ","
<< rule.roi.w << "," << rule.roi.h << ")"
<< " min_duration=" << rule.min_duration_ms << "ms"
<< " cooldown=" << rule.cooldown_ms << "ms\n";
}
return true;
}
RuleMatchResult RuleEngine::Evaluate(const std::shared_ptr<Frame>& frame) {
RuleMatchResult result;
if (!frame || !frame->det) return result;
const auto& detections = frame->det->items;
int img_w = frame->det->img_w > 0 ? frame->det->img_w : frame->width;
int img_h = frame->det->img_h > 0 ? frame->det->img_h : frame->height;
for (auto& rule : rules_) {
// Check schedule
if (!rule.schedule.empty() && !IsInSchedule(rule.schedule)) {
duration_start_.erase(rule.name);
continue;
}
// Check cooldown
if (!CheckCooldown(rule.name)) {
continue;
}
// Find matching detections
std::vector<Detection> matched;
for (const auto& det : detections) {
// Check class_id (if empty, match all)
if (!rule.class_ids.empty() &&
rule.class_ids.find(det.cls_id) == rule.class_ids.end()) {
continue;
}
// Check ROI
if (!IsInRoi(det.bbox, rule.roi, img_w, img_h)) {
continue;
}
matched.push_back(det);
}
bool currently_matched = !matched.empty();
// Check duration
if (CheckDuration(rule.name, currently_matched)) {
result.matched = true;
result.rule_name = rule.name;
result.matched_detections = matched;
TriggerCooldown(rule.name);
duration_start_.erase(rule.name);
return result;
}
}
return result;
}
void RuleEngine::Reset() {
duration_start_.clear();
last_trigger_.clear();
}
bool RuleEngine::IsInRoi(const Rect& bbox, const RoiRect& roi, int img_w, int img_h) const {
// Convert bbox to normalized coordinates
float bbox_cx = (bbox.x + bbox.w / 2.0f) / img_w;
float bbox_cy = (bbox.y + bbox.h / 2.0f) / img_h;
// Check if center is in ROI
return bbox_cx >= roi.x && bbox_cx <= (roi.x + roi.w) &&
bbox_cy >= roi.y && bbox_cy <= (roi.y + roi.h);
}
bool RuleEngine::IsInSchedule(const std::string& schedule) const {
// Parse "HH:MM-HH:MM" format
if (schedule.length() != 11 || schedule[5] != '-') {
return true; // Invalid format, assume always active
}
int start_h = 0, start_m = 0, end_h = 0, end_m = 0;
std::sscanf(schedule.c_str(), "%d:%d-%d:%d", &start_h, &start_m, &end_h, &end_m);
std::time_t now = std::time(nullptr);
std::tm* local = std::localtime(&now);
int curr_min = local->tm_hour * 60 + local->tm_min;
int start_min = start_h * 60 + start_m;
int end_min = end_h * 60 + end_m;
if (start_min <= end_min) {
return curr_min >= start_min && curr_min <= end_min;
} else {
// Overnight schedule (e.g., "22:00-06:00")
return curr_min >= start_min || curr_min <= end_min;
}
}
bool RuleEngine::CheckDuration(const std::string& rule_name, bool currently_matched) {
auto it = std::find_if(rules_.begin(), rules_.end(),
[&](const AlarmRule& r) { return r.name == rule_name; });
if (it == rules_.end()) return false;
int min_duration_ms = it->min_duration_ms;
auto now = std::chrono::steady_clock::now();
if (!currently_matched) {
duration_start_.erase(rule_name);
return false;
}
auto start_it = duration_start_.find(rule_name);
if (start_it == duration_start_.end()) {
duration_start_[rule_name] = now;
if (min_duration_ms <= 0) {
return true; // No duration requirement
}
return false;
}
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - start_it->second);
return elapsed.count() >= min_duration_ms;
}
bool RuleEngine::CheckCooldown(const std::string& rule_name) {
auto it = std::find_if(rules_.begin(), rules_.end(),
[&](const AlarmRule& r) { return r.name == rule_name; });
if (it == rules_.end()) return false;
auto last_it = last_trigger_.find(rule_name);
if (last_it == last_trigger_.end()) {
return true; // Never triggered
}
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - last_it->second);
return elapsed.count() >= it->cooldown_ms;
}
void RuleEngine::TriggerCooldown(const std::string& rule_name) {
last_trigger_[rule_name] = std::chrono::steady_clock::now();
}
} // namespace rk3588

View File

@ -0,0 +1,59 @@
#pragma once
#include <chrono>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "frame/frame.h"
#include "utils/simple_json.h"
namespace rk3588 {
struct RoiRect {
float x = 0.0f;
float y = 0.0f;
float w = 1.0f;
float h = 1.0f;
};
struct AlarmRule {
std::string name;
std::set<int> class_ids;
RoiRect roi;
int min_duration_ms = 0;
int cooldown_ms = 5000;
std::string schedule; // "HH:MM-HH:MM" format, empty = always active
};
struct RuleMatchResult {
bool matched = false;
std::string rule_name;
std::vector<Detection> matched_detections;
};
class RuleEngine {
public:
bool Init(const SimpleJson& rules_config, const std::vector<std::string>& labels);
RuleMatchResult Evaluate(const std::shared_ptr<Frame>& frame);
void Reset();
private:
bool IsInRoi(const Rect& bbox, const RoiRect& roi, int img_w, int img_h) const;
bool IsInSchedule(const std::string& schedule) const;
bool CheckDuration(const std::string& rule_name, bool currently_matched);
bool CheckCooldown(const std::string& rule_name);
void TriggerCooldown(const std::string& rule_name);
std::vector<AlarmRule> rules_;
std::vector<std::string> labels_;
// Duration tracking: rule_name -> first match time
std::map<std::string, std::chrono::steady_clock::time_point> duration_start_;
// Cooldown tracking: rule_name -> last trigger time
std::map<std::string, std::chrono::steady_clock::time_point> last_trigger_;
};
} // namespace rk3588

View File

@ -0,0 +1,62 @@
#include "local_uploader.h"
#include <filesystem>
#include <fstream>
#include <iostream>
namespace rk3588 {
bool LocalUploader::Init(const SimpleJson& config) {
base_path_ = config.ValueOr<std::string>("path", "/tmp/alarms");
url_prefix_ = config.ValueOr<std::string>("url_prefix", "file://");
try {
std::filesystem::create_directories(base_path_);
} catch (const std::exception& e) {
std::cerr << "[LocalUploader] failed to create directory: " << e.what() << "\n";
return false;
}
std::cout << "[LocalUploader] initialized, path=" << base_path_ << "\n";
return true;
}
UploadResult LocalUploader::Upload(const std::string& key,
const uint8_t* data,
size_t size,
const std::string& /*content_type*/) {
UploadResult result;
std::string full_path = base_path_ + "/" + key;
// Create parent directories if needed
try {
std::filesystem::path file_path(full_path);
if (file_path.has_parent_path()) {
std::filesystem::create_directories(file_path.parent_path());
}
} catch (const std::exception& e) {
result.error = std::string("failed to create directory: ") + e.what();
return result;
}
std::ofstream file(full_path, std::ios::binary);
if (!file) {
result.error = "failed to open file: " + full_path;
return result;
}
file.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
file.close();
if (!file) {
result.error = "failed to write file: " + full_path;
return result;
}
result.success = true;
result.url = url_prefix_ + full_path;
return result;
}
} // namespace rk3588

View File

@ -0,0 +1,21 @@
#pragma once
#include "uploader_base.h"
namespace rk3588 {
class LocalUploader : public IUploader {
public:
bool Init(const SimpleJson& config) override;
UploadResult Upload(const std::string& key,
const uint8_t* data,
size_t size,
const std::string& content_type = "") override;
std::string Type() const override { return "local"; }
private:
std::string base_path_;
std::string url_prefix_;
};
} // namespace rk3588

View File

@ -0,0 +1,137 @@
#include "minio_uploader.h"
#include <cstdlib>
#include <iostream>
#include <sstream>
#if defined(__linux__) || defined(__APPLE__)
#include <curl/curl.h>
#define HAS_CURL 1
#else
#define HAS_CURL 0
#endif
namespace rk3588 {
namespace {
std::string GetEnvOrDefault(const std::string& env_name, const std::string& default_val) {
// Handle ${ENV_VAR} syntax
if (default_val.size() > 3 && default_val.substr(0, 2) == "${" &&
default_val.back() == '}') {
std::string var_name = default_val.substr(2, default_val.size() - 3);
const char* val = std::getenv(var_name.c_str());
return val ? std::string(val) : "";
}
return default_val;
}
} // namespace
bool MinioUploader::Init(const SimpleJson& config) {
endpoint_ = config.ValueOr<std::string>("endpoint", "http://localhost:9000");
bucket_ = config.ValueOr<std::string>("bucket", "alarms");
region_ = config.ValueOr<std::string>("region", "us-east-1");
std::string ak = config.ValueOr<std::string>("access_key", "");
std::string sk = config.ValueOr<std::string>("secret_key", "");
access_key_ = GetEnvOrDefault("MINIO_ACCESS_KEY", ak);
secret_key_ = GetEnvOrDefault("MINIO_SECRET_KEY", sk);
use_ssl_ = endpoint_.find("https://") == 0;
if (access_key_.empty() || secret_key_.empty()) {
std::cerr << "[MinioUploader] warning: access_key or secret_key not set\n";
}
std::cout << "[MinioUploader] initialized, endpoint=" << endpoint_
<< " bucket=" << bucket_ << "\n";
return true;
}
UploadResult MinioUploader::Upload(const std::string& key,
const uint8_t* data,
size_t size,
const std::string& content_type) {
UploadResult result;
#if HAS_CURL
// Simple S3 PUT request (works with MinIO)
// For production, use proper AWS4 signing
std::string url = endpoint_;
if (url.back() != '/') url += '/';
url += bucket_ + "/" + key;
CURL* curl = curl_easy_init();
if (!curl) {
result.error = "curl_easy_init failed";
return result;
}
struct curl_slist* headers = nullptr;
std::string ct = content_type.empty() ? "application/octet-stream" : content_type;
headers = curl_slist_append(headers, ("Content-Type: " + ct).c_str());
// Simple auth (MinIO supports this for internal use)
// For proper S3 compatibility, implement AWS4 signing
if (!access_key_.empty()) {
// This is a simplified approach - real implementation should use AWS4 signing
std::string auth = "Authorization: AWS " + access_key_ + ":" + secret_key_;
headers = curl_slist_append(headers, auth.c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(size));
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
// Set up data to send
struct ReadData {
const uint8_t* data;
size_t remaining;
} read_data{data, size};
curl_easy_setopt(curl, CURLOPT_READFUNCTION,
+[](char* buffer, size_t s, size_t n, void* userp) -> size_t {
auto* rd = static_cast<ReadData*>(userp);
size_t to_copy = std::min(s * n, rd->remaining);
if (to_copy > 0) {
std::memcpy(buffer, rd->data, to_copy);
rd->data += to_copy;
rd->remaining -= to_copy;
}
return to_copy;
});
curl_easy_setopt(curl, CURLOPT_READDATA, &read_data);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
result.error = curl_easy_strerror(res);
} else {
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code >= 200 && http_code < 300) {
result.success = true;
result.url = url;
} else {
result.error = "HTTP " + std::to_string(http_code);
}
}
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
#else
// Stub for Windows
std::cout << "[MinioUploader] would upload " << size << " bytes to "
<< endpoint_ << "/" << bucket_ << "/" << key << "\n";
result.success = true;
result.url = endpoint_ + "/" + bucket_ + "/" + key;
#endif
return result;
}
} // namespace rk3588

View File

@ -0,0 +1,25 @@
#pragma once
#include "uploader_base.h"
namespace rk3588 {
class MinioUploader : public IUploader {
public:
bool Init(const SimpleJson& config) override;
UploadResult Upload(const std::string& key,
const uint8_t* data,
size_t size,
const std::string& content_type = "") override;
std::string Type() const override { return "minio"; }
private:
std::string endpoint_;
std::string bucket_;
std::string access_key_;
std::string secret_key_;
std::string region_;
bool use_ssl_ = false;
};
} // namespace rk3588

View File

@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "utils/simple_json.h"
namespace rk3588 {
struct UploadResult {
bool success = false;
std::string url;
std::string error;
};
class IUploader {
public:
virtual ~IUploader() = default;
virtual bool Init(const SimpleJson& config) = 0;
virtual UploadResult Upload(const std::string& key,
const uint8_t* data,
size_t size,
const std::string& content_type = "") = 0;
virtual std::string Type() const = 0;
};
std::unique_ptr<IUploader> CreateUploader(const SimpleJson& config);
} // namespace rk3588

View File

@ -0,0 +1,27 @@
#include "uploader_base.h"
#include "local_uploader.h"
#include "minio_uploader.h"
#include <iostream>
namespace rk3588 {
std::unique_ptr<IUploader> CreateUploader(const SimpleJson& config) {
std::string type = config.ValueOr<std::string>("type", "local");
std::unique_ptr<IUploader> uploader;
if (type == "minio" || type == "s3") {
uploader = std::make_unique<MinioUploader>();
} else {
uploader = std::make_unique<LocalUploader>();
}
if (uploader && !uploader->Init(config)) {
std::cerr << "[CreateUploader] failed to init uploader type=" << type << "\n";
return nullptr;
}
return uploader;
}
} // namespace rk3588

View File

@ -0,0 +1,502 @@
#include <atomic>
#include <chrono>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <thread>
#include "node.h"
#if defined(RK3588_ENABLE_FFMPEG)
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
}
#define HAS_FFMPEG 1
#else
#define HAS_FFMPEG 0
#endif
#if defined(RK3588_ENABLE_MPP)
extern "C" {
#include <rockchip/mpp_buffer.h>
#include <rockchip/mpp_packet.h>
#include <rockchip/rk_mpi.h>
}
#define HAS_MPP 1
#else
#define HAS_MPP 0
#endif
namespace rk3588 {
namespace {
inline int Align16(int v) { return (v + 15) & ~15; }
std::string FormatTime(const std::string& pattern) {
auto now = std::chrono::system_clock::now();
auto time_t_now = std::chrono::system_clock::to_time_t(now);
std::tm* tm = std::localtime(&time_t_now);
std::ostringstream oss;
for (size_t i = 0; i < pattern.size(); ++i) {
if (pattern[i] == '%' && i + 1 < pattern.size()) {
char spec = pattern[++i];
switch (spec) {
case 'Y': oss << std::put_time(tm, "%Y"); break;
case 'm': oss << std::put_time(tm, "%m"); break;
case 'd': oss << std::put_time(tm, "%d"); break;
case 'H': oss << std::put_time(tm, "%H"); break;
case 'M': oss << std::put_time(tm, "%M"); break;
case 'S': oss << std::put_time(tm, "%S"); break;
default: oss << '%' << spec; break;
}
} else {
oss << pattern[i];
}
}
return oss.str();
}
} // namespace
#if HAS_MPP
class MppStorageEncoder {
public:
~MppStorageEncoder() { Shutdown(); }
bool Init(int width, int height, PixelFormat fmt, const std::string& codec,
int fps, int bitrate_kbps) {
width_ = width;
height_ = height;
hor_stride_ = Align16(width);
ver_stride_ = Align16(height);
fps_ = fps > 0 ? fps : 25;
bitrate_bps_ = (bitrate_kbps > 0 ? bitrate_kbps : 2000) * 1000;
if (fmt == PixelFormat::NV12) {
mpp_fmt_ = MPP_FMT_YUV420SP;
} else if (fmt == PixelFormat::YUV420) {
mpp_fmt_ = MPP_FMT_YUV420P;
} else {
std::cerr << "[storage] unsupported pixel format for MPP encoder\n";
return false;
}
coding_ = (codec == "h265" || codec == "hevc") ? MPP_VIDEO_CodingHEVC
: MPP_VIDEO_CodingAVC;
if (mpp_create(&ctx_, &mpi_) != MPP_OK) return false;
if (mpp_init(ctx_, MPP_CTX_ENC, coding_) != MPP_OK) return false;
if (mpp_enc_cfg_init(&cfg_) != MPP_OK) return false;
if (mpi_->control(ctx_, MPP_ENC_GET_CFG, cfg_) != MPP_OK) return false;
mpp_enc_cfg_set_s32(cfg_, "prep:width", width_);
mpp_enc_cfg_set_s32(cfg_, "prep:height", height_);
mpp_enc_cfg_set_s32(cfg_, "prep:hor_stride", hor_stride_);
mpp_enc_cfg_set_s32(cfg_, "prep:ver_stride", ver_stride_);
mpp_enc_cfg_set_s32(cfg_, "prep:format", mpp_fmt_);
mpp_enc_cfg_set_s32(cfg_, "rc:mode", MPP_ENC_RC_MODE_CBR);
mpp_enc_cfg_set_s32(cfg_, "rc:gop", fps_ * 2);
mpp_enc_cfg_set_s32(cfg_, "rc:fps_in_num", fps_);
mpp_enc_cfg_set_s32(cfg_, "rc:fps_in_denorm", 1);
mpp_enc_cfg_set_s32(cfg_, "rc:fps_out_num", fps_);
mpp_enc_cfg_set_s32(cfg_, "rc:fps_out_denorm", 1);
mpp_enc_cfg_set_s32(cfg_, "rc:bps_target", bitrate_bps_);
mpp_enc_cfg_set_s32(cfg_, "rc:bps_max", bitrate_bps_ * 3 / 2);
mpp_enc_cfg_set_s32(cfg_, "rc:bps_min", bitrate_bps_ / 2);
mpp_enc_cfg_set_s32(cfg_, "codec:type", coding_);
if (mpi_->control(ctx_, MPP_ENC_SET_CFG, cfg_) != MPP_OK) return false;
MppEncHeaderMode header_mode = MPP_ENC_HEADER_MODE_EACH_IDR;
mpi_->control(ctx_, MPP_ENC_SET_HEADER_MODE, &header_mode);
if (mpp_buffer_group_get_internal(&frm_grp_, MPP_BUFFER_TYPE_DRM) != MPP_OK) {
mpp_buffer_group_get_internal(&frm_grp_, MPP_BUFFER_TYPE_NORMAL);
}
// Get header
MppPacket hdr = nullptr;
if (mpi_->control(ctx_, MPP_ENC_GET_HDR_SYNC, &hdr) == MPP_OK && hdr) {
auto* data = static_cast<uint8_t*>(mpp_packet_get_pos(hdr));
size_t len = mpp_packet_get_length(hdr);
header_.assign(data, data + len);
mpp_packet_deinit(&hdr);
}
initialized_ = true;
return true;
}
void Shutdown() {
if (frm_grp_) { mpp_buffer_group_put(frm_grp_); frm_grp_ = nullptr; }
if (cfg_) { mpp_enc_cfg_deinit(cfg_); cfg_ = nullptr; }
if (ctx_) {
if (mpi_) mpi_->reset(ctx_);
mpp_destroy(ctx_);
ctx_ = nullptr; mpi_ = nullptr;
}
initialized_ = false;
}
bool Encode(const FramePtr& frame, std::vector<uint8_t>& out, bool& is_key) {
out.clear();
is_key = false;
if (!initialized_ || !frame) return false;
size_t frame_size = static_cast<size_t>(hor_stride_) * ver_stride_ * 3 / 2;
MppBuffer buf = nullptr;
if (frame->dma_fd >= 0 && frame->data_size > 0) {
MppBufferInfo info{};
info.type = MPP_BUFFER_TYPE_EXT_DMA;
info.size = frame->data_size;
info.fd = frame->dma_fd;
mpp_buffer_import(&buf, &info);
}
if (!buf) {
if (!frame->data) return false;
if (mpp_buffer_get(frm_grp_, &buf, frame_size) != MPP_OK) return false;
auto* dst = static_cast<uint8_t*>(mpp_buffer_get_ptr(buf));
std::memset(dst, 0, frame_size);
// Copy Y plane
for (int row = 0; row < height_; ++row) {
const uint8_t* src = frame->planes[0].data
? frame->planes[0].data + row * frame->planes[0].stride
: frame->data + row * frame->width;
std::memcpy(dst + row * hor_stride_, src, width_);
}
// Copy UV plane
uint8_t* dst_uv = dst + hor_stride_ * ver_stride_;
int uv_rows = height_ / 2;
for (int row = 0; row < uv_rows; ++row) {
const uint8_t* src = frame->planes[1].data
? frame->planes[1].data + row * frame->planes[1].stride
: frame->data + width_ * height_ + row * width_;
std::memcpy(dst_uv + row * hor_stride_, src, width_);
}
}
MppFrame mpp_frame = nullptr;
mpp_frame_init(&mpp_frame);
mpp_frame_set_width(mpp_frame, width_);
mpp_frame_set_height(mpp_frame, height_);
mpp_frame_set_hor_stride(mpp_frame, hor_stride_);
mpp_frame_set_ver_stride(mpp_frame, ver_stride_);
mpp_frame_set_fmt(mpp_frame, mpp_fmt_);
mpp_frame_set_pts(mpp_frame, frame->pts);
mpp_frame_set_buffer(mpp_frame, buf);
if (mpi_->encode_put_frame(ctx_, mpp_frame) != MPP_OK) {
mpp_frame_deinit(&mpp_frame);
mpp_buffer_put(buf);
return false;
}
mpp_frame_deinit(&mpp_frame);
MppPacket packet = nullptr;
while (true) {
MPP_RET ret = mpi_->encode_get_packet(ctx_, &packet);
if (ret == MPP_ERR_TIMEOUT) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
continue;
}
if (ret != MPP_OK || !packet) break;
auto* pos = static_cast<uint8_t*>(mpp_packet_get_pos(packet));
size_t len = mpp_packet_get_length(packet);
out.assign(pos, pos + len);
RK_U32 flag = mpp_packet_get_flag(packet);
is_key = (flag & 0x08) != 0;
mpp_packet_deinit(&packet);
break;
}
mpp_buffer_put(buf);
return !out.empty();
}
const std::vector<uint8_t>& Header() const { return header_; }
bool IsH265() const { return coding_ == MPP_VIDEO_CodingHEVC; }
private:
MppCtx ctx_ = nullptr;
MppApi* mpi_ = nullptr;
MppEncCfg cfg_ = nullptr;
MppBufferGroup frm_grp_ = nullptr;
MppCodingType coding_ = MPP_VIDEO_CodingAVC;
MppFrameFormat mpp_fmt_ = MPP_FMT_YUV420SP;
int width_ = 0, height_ = 0;
int hor_stride_ = 0, ver_stride_ = 0;
int fps_ = 25;
int bitrate_bps_ = 2000000;
std::vector<uint8_t> header_;
bool initialized_ = false;
};
#endif
class StorageNode : public INode {
public:
std::string Id() const override { return id_; }
std::string Type() const override { return "storage"; }
bool Init(const SimpleJson& config, const NodeContext& ctx) override {
id_ = config.ValueOr<std::string>("id", "storage");
mode_ = config.ValueOr<std::string>("mode", "continuous");
format_ = config.ValueOr<std::string>("format", "mp4");
codec_ = config.ValueOr<std::string>("codec", "h264");
segment_sec_ = config.ValueOr<int>("segment_sec", 300);
base_path_ = config.ValueOr<std::string>("path", "/rec");
filename_pattern_ = config.ValueOr<std::string>("filename_pattern", "%Y%m%d/%H%M%S");
fps_ = config.ValueOr<int>("fps", 25);
bitrate_kbps_ = config.ValueOr<int>("bitrate_kbps", 2000);
input_queue_ = ctx.input_queue;
if (!input_queue_) {
std::cerr << "[storage] no input queue\n";
return false;
}
try {
std::filesystem::create_directories(base_path_);
} catch (const std::exception& e) {
std::cerr << "[storage] failed to create directory: " << e.what() << "\n";
return false;
}
std::cout << "[storage] initialized, path=" << base_path_
<< " segment=" << segment_sec_ << "s format=" << format_ << "\n";
return true;
}
bool Start() override {
running_.store(true);
worker_ = std::thread(&StorageNode::WorkerLoop, this);
std::cout << "[storage] started\n";
return true;
}
void Stop() override {
running_.store(false);
if (input_queue_) input_queue_->Stop();
if (worker_.joinable()) worker_.join();
CloseCurrentFile();
std::cout << "[storage] stopped, recorded " << total_frames_ << " frames\n";
}
void Drain() override {
CloseCurrentFile();
}
private:
void WorkerLoop() {
using namespace std::chrono;
FramePtr frame;
while (running_.load()) {
if (!input_queue_->Pop(frame, milliseconds(200))) continue;
if (!frame) continue;
ProcessFrame(frame);
++total_frames_;
}
}
void ProcessFrame(FramePtr frame) {
// Check if we need to start a new segment
auto now = std::chrono::steady_clock::now();
bool need_new_segment = false;
if (!file_open_) {
need_new_segment = true;
} else if (segment_sec_ > 0) {
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - segment_start_);
if (elapsed.count() >= segment_sec_) {
need_new_segment = true;
}
}
if (need_new_segment) {
CloseCurrentFile();
OpenNewFile(frame);
}
WriteFrame(frame);
}
void OpenNewFile(FramePtr frame) {
#if HAS_FFMPEG
std::string filename = FormatTime(filename_pattern_) + "." + format_;
current_path_ = base_path_ + "/" + filename;
try {
std::filesystem::path p(current_path_);
if (p.has_parent_path()) {
std::filesystem::create_directories(p.parent_path());
}
} catch (...) {}
const char* fmt_name = format_ == "ts" ? "mpegts" : "mp4";
if (avformat_alloc_output_context2(&fmt_ctx_, nullptr, fmt_name,
current_path_.c_str()) < 0) {
std::cerr << "[storage] failed to create output context\n";
return;
}
AVCodecID codec_id = (codec_ == "h265" || codec_ == "hevc")
? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264;
const AVCodec* encoder = avcodec_find_encoder(codec_id);
stream_ = avformat_new_stream(fmt_ctx_, encoder);
if (!stream_) {
avformat_free_context(fmt_ctx_);
fmt_ctx_ = nullptr;
return;
}
stream_->time_base = AVRational{1, fps_};
stream_->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream_->codecpar->codec_id = codec_id;
stream_->codecpar->width = frame->width;
stream_->codecpar->height = frame->height;
stream_->codecpar->format = AV_PIX_FMT_YUV420P;
#if HAS_MPP
if (!mpp_encoder_) {
mpp_encoder_ = std::make_unique<MppStorageEncoder>();
if (!mpp_encoder_->Init(frame->width, frame->height, frame->format,
codec_, fps_, bitrate_kbps_)) {
mpp_encoder_.reset();
}
}
if (mpp_encoder_ && !mpp_encoder_->Header().empty()) {
const auto& hdr = mpp_encoder_->Header();
stream_->codecpar->extradata_size = static_cast<int>(hdr.size());
stream_->codecpar->extradata = static_cast<uint8_t*>(
av_mallocz(hdr.size() + AV_INPUT_BUFFER_PADDING_SIZE));
std::memcpy(stream_->codecpar->extradata, hdr.data(), hdr.size());
}
#endif
if (!(fmt_ctx_->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&fmt_ctx_->pb, current_path_.c_str(), AVIO_FLAG_WRITE) < 0) {
avformat_free_context(fmt_ctx_);
fmt_ctx_ = nullptr;
return;
}
}
if (avformat_write_header(fmt_ctx_, nullptr) < 0) {
if (fmt_ctx_->pb) avio_closep(&fmt_ctx_->pb);
avformat_free_context(fmt_ctx_);
fmt_ctx_ = nullptr;
return;
}
file_open_ = true;
segment_start_ = std::chrono::steady_clock::now();
segment_frames_ = 0;
std::cout << "[storage] opened: " << current_path_ << "\n";
#else
std::cerr << "[storage] FFmpeg not enabled\n";
#endif
}
void WriteFrame(FramePtr frame) {
#if HAS_FFMPEG && HAS_MPP
if (!file_open_ || !fmt_ctx_ || !mpp_encoder_) return;
std::vector<uint8_t> encoded;
bool is_key = false;
if (!mpp_encoder_->Encode(frame, encoded, is_key)) return;
if (encoded.empty()) return;
AVPacket* pkt = av_packet_alloc();
if (av_new_packet(pkt, static_cast<int>(encoded.size())) < 0) {
av_packet_free(&pkt);
return;
}
std::memcpy(pkt->data, encoded.data(), encoded.size());
pkt->stream_index = stream_->index;
pkt->flags = is_key ? AV_PKT_FLAG_KEY : 0;
pkt->pts = segment_frames_;
pkt->dts = segment_frames_;
pkt->duration = 1;
av_packet_rescale_ts(pkt, AVRational{1, fps_}, stream_->time_base);
av_interleaved_write_frame(fmt_ctx_, pkt);
av_packet_free(&pkt);
++segment_frames_;
#elif HAS_FFMPEG
// Software encoding fallback would go here
(void)frame;
#endif
}
void CloseCurrentFile() {
#if HAS_FFMPEG
if (!file_open_ || !fmt_ctx_) return;
av_write_trailer(fmt_ctx_);
if (!(fmt_ctx_->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&fmt_ctx_->pb);
}
avformat_free_context(fmt_ctx_);
fmt_ctx_ = nullptr;
stream_ = nullptr;
file_open_ = false;
std::cout << "[storage] closed: " << current_path_
<< " (" << segment_frames_ << " frames)\n";
#endif
}
std::string id_;
std::string mode_;
std::string format_;
std::string codec_;
int segment_sec_ = 300;
std::string base_path_;
std::string filename_pattern_;
int fps_ = 25;
int bitrate_kbps_ = 2000;
std::atomic<bool> running_{false};
std::shared_ptr<SpscQueue<FramePtr>> input_queue_;
std::thread worker_;
uint64_t total_frames_ = 0;
// Current file state
bool file_open_ = false;
std::string current_path_;
std::chrono::steady_clock::time_point segment_start_;
int64_t segment_frames_ = 0;
#if HAS_FFMPEG
AVFormatContext* fmt_ctx_ = nullptr;
AVStream* stream_ = nullptr;
#endif
#if HAS_MPP
std::unique_ptr<MppStorageEncoder> mpp_encoder_;
#endif
};
REGISTER_NODE(StorageNode, "storage");
} // namespace rk3588