first commit

This commit is contained in:
陈赣
2026-06-03 12:43:14 +08:00
commit ba76cfae28
608 changed files with 120791 additions and 0 deletions

20
nodes/record/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Summary
`vp_record_node` is used to record video and image, save them to local disk after it finished. It's a middle node but works asynchronously, so recording would not block the pipeline.
```
record
┣ README.md
┣ vp_image_record_task.cpp
┣ vp_image_record_task.h // image record task
┣ vp_record_node.cpp
┣ vp_record_node.h // record node
┣ vp_record_task.cpp
┣ vp_record_task.h // base class for record task, work async
┣ vp_video_record_task.cpp
┗ vp_video_record_task.h // video record task
```
below is showing how to record video:
![](../../doc/p5.png)

View File

@@ -0,0 +1,62 @@
#include "vp_image_record_task.h"
namespace vp_nodes {
vp_image_record_task::vp_image_record_task(int channel_index,
std::string file_name_without_ext,
std::string save_dir,
bool auto_sub_dir,
bool osd,
vp_objects::vp_size resolution_w_h,
std::string host_node_name,
bool auto_start):
vp_record_task(channel_index, file_name_without_ext, save_dir, auto_sub_dir, resolution_w_h, osd, host_node_name) {
// start automatically when initializing
if (auto_start) {
start();
}
}
vp_image_record_task::~vp_image_record_task() {
stop_task();
}
void vp_image_record_task::record_task_run() {
/* Below Code Run In A Separate Thread! */
// get valid path
auto full_record_path = get_full_record_path();
while (status == vp_record_task_status::STARTED) {
// it is a consumer
cache_semaphore.wait();
auto frame_to_record = frames_to_record.front();
if (frame_to_record == nullptr) {
//dead flag
continue;
}
cv::Mat frame_data;
// preprocess, vp_frame_meta -> cv::Mat
preprocess(frame_to_record, frame_data);
frames_to_record.pop_front();
// write to disk
cv::imwrite(full_record_path, frame_data);
VP_DEBUG(vp_utils::string_format("[%s] [record] already written frame for `%s`", host_node_name.c_str(), get_full_record_path().c_str()));
/* for image recording, just saving only 1 frame and then complete
* refer to vp_video_record_task
*/
vp_record_info record_info;
record_info.record_type = vp_record_type::IMAGE;
notify_task_complete(record_info);
// loop end
}
}
std::string vp_image_record_task::get_file_ext() {
// save as jpg
return ".jpg";
}
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include "vp_record_task.h"
namespace vp_nodes {
// image record task, each task instance responsible for recording only 1 image file.
// create multi instances if multi images need to be record at the same time, and maintain these tasks in a list.
// note, the cost of image record is very low but still let it work asynchronously (derived from vp_record_task).
class vp_image_record_task: public vp_record_task {
private:
protected:
// define how to record image
virtual void record_task_run() override;
// retrive .jpg as file extension
virtual std::string get_file_ext() override;
public:
vp_image_record_task(int channel_index,
std::string file_name_without_ext,
std::string save_dir,
bool auto_sub_dir,
bool osd,
vp_objects::vp_size resolution_w_h,
std::string host_node_name = "host_node_not_specified",
bool auto_start = true);
~vp_image_record_task();
};
}

View File

@@ -0,0 +1,149 @@
#include "vp_record_node.h"
namespace vp_nodes {
vp_record_node::vp_record_node(std::string node_name,
std::string video_save_dir,
std::string image_save_dir,
vp_objects::vp_size resolution_w_h,
bool osd,
int pre_record_video_duration,
int record_video_duration,
bool auto_sub_dir,
int bitrate):
vp_node(node_name),
video_save_dir(video_save_dir),
image_save_dir(image_save_dir),
resolution_w_h(resolution_w_h),
osd(osd),
pre_record_video_duration(pre_record_video_duration),
record_video_duration(record_video_duration),
auto_sub_dir(auto_sub_dir),
bitrate(bitrate) {
this->initialized();
}
vp_record_node::~vp_record_node() {
deinitialized();
}
std::shared_ptr<vp_objects::vp_meta> vp_record_node::handle_frame_meta(std::shared_ptr<vp_objects::vp_frame_meta> meta) {
// cache fps for current channel
if (all_fps.count(meta->channel_index) == 0) {
all_fps[meta->channel_index] = meta->fps;
}
auto& fps = all_fps[meta->channel_index];
// first time for current channel
if (all_pre_records.count(meta->channel_index) == 0) {
all_pre_records[meta->channel_index] = std::deque<std::shared_ptr<vp_objects::vp_frame_meta>>();
}
auto& pre_records = all_pre_records[meta->channel_index];
// update pre_records for current channel
pre_records.push_back(meta);
auto frames_need_pre_record = fps * pre_record_video_duration;
// keep max frames
if (pre_records.size() > frames_need_pre_record) {
pre_records.pop_front();
}
// first time for current channel
if (all_record_tasks.count(meta->channel_index) == 0) {
all_record_tasks[meta->channel_index] = std::list<std::shared_ptr<vp_nodes::vp_record_task>>();
}
auto& record_tasks = all_record_tasks[meta->channel_index];
// then append data to all tasks of current channel
for (auto i = record_tasks.begin(); i != record_tasks.end();) {
if ((*i)->status == vp_nodes::vp_record_task_status::COMPLETE) {
i = record_tasks.erase(i); // remove task which is complete already
}
else {
(*i)->append_async(meta); // no block here
i++;
}
}
// done
return meta;
}
std::shared_ptr<vp_objects::vp_meta> vp_record_node::handle_control_meta(std::shared_ptr<vp_objects::vp_control_meta> meta) {
if (meta->control_type == vp_objects::vp_control_type::IMAGE_RECORD ||
meta->control_type == vp_objects::vp_control_type::VIDEO_RECORD) {
/* code */
// create record task, it will start asynchronously.
auto_new_record_task(meta);
/* no return since already handle it, do not need pass to next nodes. */
return nullptr;
}
else {
return vp_node::handle_control_meta(meta);
}
}
void vp_record_node::auto_new_record_task(std::shared_ptr<vp_objects::vp_control_meta>& meta) {
auto& record_tasks = all_record_tasks[meta->channel_index];
auto& pre_records = all_pre_records[meta->channel_index];
auto& fps = all_fps[meta->channel_index];
// image record
if (meta->control_type == vp_objects::vp_control_type::IMAGE_RECORD) {
auto image_record_control_meta = std::dynamic_pointer_cast<vp_objects::vp_image_record_control_meta>(meta);
// create image record task
auto file_name_without_ext = image_record_control_meta->image_file_name_without_ext;
auto _osd = image_record_control_meta->osd;
VP_INFO(vp_utils::string_format("[%s] [record] create new image record task, file_name_without_ext is: `%s`", node_name.c_str(), file_name_without_ext.c_str()));
auto image_record_task = std::make_shared<vp_nodes::vp_image_record_task>(meta->channel_index,
file_name_without_ext,
image_save_dir,
auto_sub_dir,
_osd,
resolution_w_h, node_name);
image_record_task->set_task_complete_hooker([this](int channel_index, vp_record_info record_info) {
// just notify hooker which has attached on the node
if (this->image_record_complete_hooker) {
this->image_record_complete_hooker(channel_index, record_info);
}
VP_INFO(vp_utils::string_format("[%s] [record] image record task completed, file_name_without_ext is: `%s`", node_name.c_str(), record_info.file_name_without_ext.c_str()));
});
record_tasks.push_back(image_record_task);
}
// video record
if (meta->control_type == vp_objects::vp_control_type::VIDEO_RECORD) {
auto video_record_control_meta = std::dynamic_pointer_cast<vp_objects::vp_video_record_control_meta>(meta);
// create video record task
auto file_name_without_ext = video_record_control_meta->video_file_name_without_ext;
auto _osd = video_record_control_meta->osd;
auto _record_video_duration = video_record_control_meta->record_video_duration;
if (_record_video_duration == 0) {
// use default value
_record_video_duration = record_video_duration;
}
VP_INFO(vp_utils::string_format("[%s] [record] create new video record task, file_name_without_ext is: `%s`", node_name.c_str(), file_name_without_ext.c_str()));
auto video_record_task = std::make_shared<vp_nodes::vp_video_record_task>(meta->channel_index,
pre_records,
file_name_without_ext,
video_save_dir,
auto_sub_dir,
_osd,
resolution_w_h,
bitrate, fps, pre_record_video_duration, _record_video_duration, node_name);
video_record_task->set_task_complete_hooker([this](int channel_index, vp_record_info record_info) {
// just notify hooker which has attached on the node
if (this->video_record_complete_hooker) {
this->video_record_complete_hooker(channel_index, record_info);
}
VP_INFO(vp_utils::string_format("[%s] [record] video record task completed, file_name_without_ext is: `%s`", node_name.c_str(), record_info.file_name_without_ext.c_str()));
});
record_tasks.push_back(video_record_task);
}
}
}

View File

@@ -0,0 +1,72 @@
#pragma once
#include <list>
#include <deque>
#include <map>
#include "../vp_node.h"
#include "../../objects/vp_image_record_control_meta.h"
#include "../../objects/vp_video_record_control_meta.h"
#include "vp_image_record_task.h"
#include "vp_video_record_task.h"
#include "vp_record_status_hookable.h"
namespace vp_nodes {
// video/image recording node, save it to local disk.
// it is a middle node but works asynchronously, so recording would not block the pipeline.
// note record node could work on multi channels at the same time.
class vp_record_node: public vp_node, public vp_record_status_hookable
{
private:
/* config data */
// video save directory
std::string video_save_dir;
// image save directory
std::string image_save_dir;
// pre record time for video (seconds)
int pre_record_video_duration;
// record time for video (seconds), not including pre_record_video_duration
int record_video_duration;
// auto create sub directory by date and channel or not, such as `./video_save_dir/2022-10-8/1/**.mp4`
bool auto_sub_dir;
// width and height
vp_objects::vp_size resolution_w_h = {};
// bitrate for video record
int bitrate = 1024;
// record osd frame or not
bool osd = false;
// fps for current video
// int fps = 0;
std::map<int, int> all_fps;
/* record task list */
// std::list<std::shared_ptr<vp_nodes::vp_record_task>> record_tasks;
std::map<int, std::list<std::shared_ptr<vp_nodes::vp_record_task>>> all_record_tasks;
/* pre-record for video */
// std::deque<std::shared_ptr<vp_objects::vp_frame_meta>> pre_records;
std::map<int, std::deque<std::shared_ptr<vp_objects::vp_frame_meta>>> all_pre_records;
// new record task
void auto_new_record_task(std::shared_ptr<vp_objects::vp_control_meta>& meta);
protected:
// re-implementation
virtual std::shared_ptr<vp_objects::vp_meta> handle_frame_meta(std::shared_ptr<vp_objects::vp_frame_meta> meta) override;
// re-implementation
virtual std::shared_ptr<vp_objects::vp_meta> handle_control_meta(std::shared_ptr<vp_objects::vp_control_meta> meta) override;
public:
vp_record_node(std::string node_name,
std::string video_save_dir,
std::string image_save_dir,
vp_objects::vp_size resolution_w_h = {},
bool osd = false,
int pre_record_video_duration = 5,
int record_video_duration = 20,
bool auto_sub_dir = true,
int bitrate = 1024);
~vp_record_node();
};
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include "vp_record_task.h"
namespace vp_nodes {
// callback when record task complete.
class vp_record_status_hookable
{
protected:
vp_record_task_complete_hooker image_record_complete_hooker;
vp_record_task_complete_hooker video_record_complete_hooker;
public:
vp_record_status_hookable(/* args */) {}
~vp_record_status_hookable() {}
void set_image_record_complete_hooker(vp_record_task_complete_hooker image_record_complete_hooker) {
this->image_record_complete_hooker = image_record_complete_hooker;
}
void set_video_record_complete_hooker(vp_record_task_complete_hooker video_record_complete_hooker) {
this->video_record_complete_hooker = video_record_complete_hooker;
}
};
}

View File

@@ -0,0 +1,144 @@
#include "vp_record_task.h"
namespace vp_nodes {
vp_record_task::vp_record_task(int channel_index,
std::string file_name_without_ext,
std::string save_dir,
bool auto_sub_dir,
vp_objects::vp_size resolution_w_h,
bool osd,
std::string host_node_name):
channel_index(channel_index),
file_name_without_ext(file_name_without_ext),
save_dir(save_dir),
auto_sub_dir(auto_sub_dir),
resolution_w_h(resolution_w_h),
osd(osd),
host_node_name(host_node_name) {
}
vp_record_task::~vp_record_task() {
}
void vp_record_task::stop_task() {
status = vp_record_task_status::NOSTRAT;
frames_to_record.push_back(nullptr);
cache_semaphore.signal();
if (record_task_th.joinable()) {
record_task_th.join();
}
}
std::string vp_record_task::get_full_record_path() {
// full_record_path already generated
if (!full_record_path.empty()) {
return full_record_path;
}
// check save dir
if (!std::experimental::filesystem::exists(save_dir)) {
VP_INFO(vp_utils::string_format("[%s] [record] save dir not exists, now creating save dir: `%s`", host_node_name.c_str(), save_dir.c_str()));
std::experimental::filesystem::create_directories(save_dir);
}
// do not generate sub folder
if (!auto_sub_dir) {
std::experimental::filesystem::path p1(save_dir);
std::experimental::filesystem::path p2(file_name_without_ext + get_file_ext());
// ./video/abc.mp4
auto p = p1 / p2;
if (std::experimental::filesystem::exists(p)) {
// just check once
auto new_file_name = file_name_without_ext + "_" + std::to_string(NOW.time_since_epoch().count()) + get_file_ext();
VP_WARN(vp_utils::string_format("[%s] [record] `%s` already exists, changing to: `%s`", host_node_name.c_str(), p2.string().c_str(), new_file_name.c_str()));
p = p1 / new_file_name;
}
full_record_path = p.string();
}
else {
// generate sub folder by date and channel
std::experimental::filesystem::path p1(save_dir);
// just use year-mon-day
std::experimental::filesystem::path p2(vp_utils::time_format(std::chrono::system_clock::now(), "<year>-<mon>-<day>"));
std::experimental::filesystem::path p3(std::to_string(channel_index));
std::experimental::filesystem::path p4(file_name_without_ext + get_file_ext());
auto p1_3 = p1 / p2 / p3;
if (!std::experimental::filesystem::exists(p1_3)) {
VP_INFO(vp_utils::string_format("[%s] [record] sub dir not exists, now creating sub dir: `%s`", host_node_name.c_str(), p1_3.string().c_str()));
std::experimental::filesystem::create_directories(p1_3);
}
// ./video/2022-10-10/1/abc.mp4
auto p = p1_3 / p4;
if (std::experimental::filesystem::exists(p)) {
// just check once
auto new_file_name = file_name_without_ext + "_" + std::to_string(NOW.time_since_epoch().count()) + get_file_ext();
VP_WARN(vp_utils::string_format("[%s] [record] `%s` already exists, changing to: `%s`", host_node_name.c_str(), p4.string().c_str(), new_file_name.c_str()));
p = p1_3 / new_file_name;
}
full_record_path = p.string();
}
VP_INFO(vp_utils::string_format("[%s] [record] get full record path: `%s`", host_node_name.c_str(), full_record_path.c_str()));
return full_record_path;
}
void vp_record_task::preprocess(std::shared_ptr<vp_objects::vp_frame_meta>& frame_to_record, cv::Mat& data) {
cv::Mat resize_frame;
if (this->resolution_w_h.width != 0 && this->resolution_w_h.height != 0) {
cv::resize((osd && !frame_to_record->osd_frame.empty()) ? frame_to_record->osd_frame : frame_to_record->frame,
resize_frame,
cv::Size(resolution_w_h.width, resolution_w_h.height));
}
else {
resize_frame = (osd && !frame_to_record->osd_frame.empty()) ? frame_to_record->osd_frame : frame_to_record->frame;
}
resize_frame.copyTo(data);
}
void vp_record_task::set_task_complete_hooker(vp_record_task_complete_hooker task_complete_hooker) {
// override if already set
this->task_complete_hooker = task_complete_hooker;
}
void vp_record_task::notify_task_complete(vp_record_info record_info) {
status = vp_record_task_status::COMPLETE;
if (task_complete_hooker) {
// notify to host
// fill fields defined in base class
record_info.channel_index = channel_index;
record_info.file_name_without_ext = file_name_without_ext;
record_info.full_record_path = get_full_record_path();
record_info.osd = osd;
task_complete_hooker(channel_index, record_info);
}
}
void vp_record_task::start() {
// check status
if (status != vp_record_task_status::NOSTRAT) {
return;
}
status = vp_record_task_status::STARTED;
record_task_th = std::thread(&vp_record_task::record_task_run, this);
}
void vp_record_task::append_async(std::shared_ptr<vp_objects::vp_frame_meta> frame_meta) {
// can append data only if NOSTART or STARTED
if (status == vp_record_task_status::COMPLETE) {
return;
}
// just push data into queue, it is a producer
// note, since only one thread push, no lock needed here
frames_to_record.push_back(frame_meta);
cache_semaphore.signal();
}
}

View File

@@ -0,0 +1,111 @@
#pragma once
#include <deque>
#include <thread>
#include <memory>
#include <functional>
// compile tips:
// remove experimental/ if gcc >= 8.0
#include <experimental/filesystem>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/imgcodecs.hpp>
#include "../../objects/vp_frame_meta.h"
#include "../../utils/vp_utils.h"
#include "../../utils/vp_semaphore.h"
#include "../../utils/logger/vp_logger.h"
namespace vp_nodes {
// record type
enum vp_record_type {
IMAGE,
VIDEO
};
// record information, used to notify others.
struct vp_record_info {
int channel_index;
vp_record_type record_type = vp_record_type::IMAGE;
std::string file_name_without_ext;
std::string full_record_path;
bool osd;
int pre_record_video_duration = 0; // ignore for image
int record_video_duration = 0; // ignore for image
};
// hooker for recording complete
typedef std::function<void(int, vp_record_info)> vp_record_task_complete_hooker;
// status of record task
enum vp_record_task_status {
NOSTRAT, // task initialized but have not called start()
STARTED, // called start() and task is working (writing/saving data to file)
COMPLETE // record task is complete. task instance is un-reusable
};
// base class for record task (video & image), works asynchronously and mainly responsible for:
// 1. preprocess frame before recording
// 2. generate valid full record path, including path, name with extension
// 3. run working thread
// 4. notify caller when recording complete
class vp_record_task {
private:
int channel_index;
std::string file_name_without_ext;
std::string save_dir;
bool auto_sub_dir;
vp_objects::vp_size resolution_w_h;
bool osd;
std::string full_record_path = "";
vp_record_task_complete_hooker task_complete_hooker;
protected:
// record thread
std::thread record_task_th;
// record thread func, implemented by child class
virtual void record_task_run() = 0;
// wait thread exit in vp_record_task
void stop_task();
// preprocess, choose frame type (osd or not) and resize
void preprocess(std::shared_ptr<vp_objects::vp_frame_meta>& frame_to_record, cv::Mat& data);
// get file extension override by specific class (for example, .mp4 for video and .jpg for image)
virtual std::string get_file_ext() = 0;
// notify to host when task complete
void notify_task_complete(vp_record_info record_info);
// cache frames to be recorded (video or image)
// 1. include pre-record frames for video
// 2. just one frame enough for image
std::deque<std::shared_ptr<vp_objects::vp_frame_meta>> frames_to_record;
// synchronize for cache
vp_utils::vp_semaphore cache_semaphore;
std::string host_node_name; // the node name of host, vp_record_task is mainly used inside node.
public:
// status
vp_record_task_status status = vp_record_task_status::NOSTRAT;
// get full record path for file, include path, name with extension
std::string get_full_record_path();
// register hooker for recording complete
void set_task_complete_hooker(vp_record_task_complete_hooker task_complete_hooker);
// start task async
void start();
// append asynchronously, just write frame to cache
void append_async(std::shared_ptr<vp_objects::vp_frame_meta> frame_meta);
vp_record_task(int channel_index,
std::string file_name_without_ext,
std::string save_dir,
bool auto_sub_dir,
vp_objects::vp_size resolution_w_h,
bool osd,
std::string host_node_name);
virtual ~vp_record_task(); // keep virtual since we need destruct child class via base pointer
};
}

View File

@@ -0,0 +1,104 @@
#include "vp_video_record_task.h"
#include "../../utils/vp_utils.h"
namespace vp_nodes {
vp_video_record_task::vp_video_record_task(int channel_index,
std::deque<std::shared_ptr<vp_objects::vp_frame_meta>> pre_record_frames,
std::string file_name_without_ext,
std::string save_dir,
bool auto_sub_dir,
bool osd,
vp_objects::vp_size resolution_w_h,
int bitrate,
int fps,
int pre_record_video_duration,
int record_video_duration,
std::string host_node_name,
bool auto_start):
vp_record_task(channel_index, file_name_without_ext, save_dir, auto_sub_dir, resolution_w_h, osd, host_node_name),
bitrate(bitrate),
fps(fps),
pre_record_video_duration(pre_record_video_duration),
record_video_duration(record_video_duration) {
assert(bitrate > 0);
// transfer to inner cache
for (auto i: pre_record_frames) {
frames_to_record.push_back(i);
cache_semaphore.signal();
}
// start automatically when initializing
if (auto_start) {
start();
}
}
vp_video_record_task::~vp_video_record_task() {
stop_task();
}
void vp_video_record_task::record_task_run() {
/* Below Code Run In A Separate Thread! */
// get valid path
auto full_record_path = get_full_record_path();
// format string used by gstreamer
auto gst = vp_utils::string_format(gst_template, bitrate, full_record_path.c_str());
// get total frames to record
frames_need_record = (pre_record_video_duration + record_video_duration) * fps;
frames_already_record = 0;
// video duration at least 1 second
assert(frames_need_record > fps * 1);
// pop data from cache and write into file
while (status == vp_record_task_status::STARTED) {
// it is a consumer
cache_semaphore.wait();
auto frame_to_record = frames_to_record.front();
if (frame_to_record == nullptr) {
// dead flag
continue;
}
cv::Mat frame_data;
// preprocess, vp_frame_meta -> cv::Mat
preprocess(frame_to_record, frame_data);
frames_to_record.pop_front();
// we open video writer in while loop, since we need width and height of frame when open a VideoWriter.
if (!video_writer.isOpened()) {
assert(video_writer.open(gst, cv::CAP_GSTREAMER, 0, fps, {frame_data.cols, frame_data.rows}));
}
// write cv::Mat to file
video_writer.write(frame_data);
frames_already_record++;
VP_DEBUG(vp_utils::string_format("[%s] [record] already written %d frames for `%s`", host_node_name.c_str(), frames_already_record, get_full_record_path().c_str()));
/* for video recording, need saving multi frames
* refer to vp_image_record_task
*/
// check if complete
if (frames_already_record >= frames_need_record) {
// here release writer at once mannually make sure the video file can be used by others.
video_writer.release();
vp_record_info record_info;
record_info.record_type = vp_record_type::VIDEO;
record_info.pre_record_video_duration = pre_record_video_duration;
record_info.record_video_duration = record_video_duration;
notify_task_complete(record_info);
// loop end
}
}
}
std::string vp_video_record_task::get_file_ext() {
// save as mp4
return ".mp4";
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "vp_record_task.h"
namespace vp_nodes {
// video record task, each task instance responsible for recording only 1 video file.
// create multi instances if multi videos need to be record at the same time, and maintain these tasks in a list.
class vp_video_record_task: public vp_record_task {
private:
// video writer
cv::VideoWriter video_writer;
// gst template
std::string gst_template = "appsrc ! videoconvert ! x264enc bitrate=%d ! mp4mux ! filesink location=%s";
int frames_already_record = -1;
int frames_need_record = 0;
int bitrate;
int fps;
int record_video_duration;
int pre_record_video_duration;
protected:
// define how to record video
virtual void record_task_run() override;
// retrive .mp4 as file extension
virtual std::string get_file_ext() override;
public:
vp_video_record_task(int channel_index,
std::deque<std::shared_ptr<vp_objects::vp_frame_meta>> pre_record_frames,
std::string file_name_without_ext,
std::string save_dir,
bool auto_sub_dir,
bool osd,
vp_objects::vp_size resolution_w_h,
int bitrate,
int fps,
int pre_record_video_duration,
int record_video_duration,
std::string host_node_name = "host_node_not_specified",
bool auto_start = true);
~vp_video_record_task();
};
}