进行第四阶段开发
This commit is contained in:
parent
5ee7061624
commit
af4cdf3934
116
Readme.md
116
Readme.md
@ -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` | CPU(IO) |
|
||||
| `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)
|
||||
|
||||
151
configs/sample_security_pipeline.json
Normal file
151
configs/sample_security_pipeline.json
Normal 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"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
|
||||
31
plugins/alarm/actions/action_base.h
Normal file
31
plugins/alarm/actions/action_base.h
Normal 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
|
||||
321
plugins/alarm/actions/clip_action.cpp
Normal file
321
plugins/alarm/actions/clip_action.cpp
Normal 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
|
||||
62
plugins/alarm/actions/clip_action.h
Normal file
62
plugins/alarm/actions/clip_action.h
Normal 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
|
||||
145
plugins/alarm/actions/http_action.cpp
Normal file
145
plugins/alarm/actions/http_action.cpp
Normal 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
|
||||
37
plugins/alarm/actions/http_action.h
Normal file
37
plugins/alarm/actions/http_action.h
Normal 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
|
||||
55
plugins/alarm/actions/log_action.cpp
Normal file
55
plugins/alarm/actions/log_action.cpp
Normal 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
|
||||
18
plugins/alarm/actions/log_action.h
Normal file
18
plugins/alarm/actions/log_action.h
Normal 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
|
||||
193
plugins/alarm/actions/snapshot_action.cpp
Normal file
193
plugins/alarm/actions/snapshot_action.cpp
Normal 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
|
||||
25
plugins/alarm/actions/snapshot_action.h
Normal file
25
plugins/alarm/actions/snapshot_action.h
Normal 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
|
||||
205
plugins/alarm/alarm_node.cpp
Normal file
205
plugins/alarm/alarm_node.cpp
Normal 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
|
||||
36
plugins/alarm/frame_ring_buffer.cpp
Normal file
36
plugins/alarm/frame_ring_buffer.cpp
Normal 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
|
||||
30
plugins/alarm/frame_ring_buffer.h
Normal file
30
plugins/alarm/frame_ring_buffer.h
Normal 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
|
||||
206
plugins/alarm/rule_engine.cpp
Normal file
206
plugins/alarm/rule_engine.cpp
Normal 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
|
||||
59
plugins/alarm/rule_engine.h
Normal file
59
plugins/alarm/rule_engine.h
Normal 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
|
||||
62
plugins/alarm/uploaders/local_uploader.cpp
Normal file
62
plugins/alarm/uploaders/local_uploader.cpp
Normal 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
|
||||
21
plugins/alarm/uploaders/local_uploader.h
Normal file
21
plugins/alarm/uploaders/local_uploader.h
Normal 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
|
||||
137
plugins/alarm/uploaders/minio_uploader.cpp
Normal file
137
plugins/alarm/uploaders/minio_uploader.cpp
Normal 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
|
||||
25
plugins/alarm/uploaders/minio_uploader.h
Normal file
25
plugins/alarm/uploaders/minio_uploader.h
Normal 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
|
||||
30
plugins/alarm/uploaders/uploader_base.h
Normal file
30
plugins/alarm/uploaders/uploader_base.h
Normal 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
|
||||
27
plugins/alarm/uploaders/uploader_factory.cpp
Normal file
27
plugins/alarm/uploaders/uploader_factory.cpp
Normal 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
|
||||
502
plugins/storage/storage_node.cpp
Normal file
502
plugins/storage/storage_node.cpp
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user