first commit
This commit is contained in:
20
nodes/record/README.md
Normal file
20
nodes/record/README.md
Normal 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:
|
||||
|
||||

|
||||
62
nodes/record/vp_image_record_task.cpp
Normal file
62
nodes/record/vp_image_record_task.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
28
nodes/record/vp_image_record_task.h
Normal file
28
nodes/record/vp_image_record_task.h
Normal 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();
|
||||
};
|
||||
}
|
||||
149
nodes/record/vp_record_node.cpp
Normal file
149
nodes/record/vp_record_node.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
nodes/record/vp_record_node.h
Normal file
72
nodes/record/vp_record_node.h
Normal 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();
|
||||
};
|
||||
}
|
||||
24
nodes/record/vp_record_status_hookable.h
Normal file
24
nodes/record/vp_record_status_hookable.h
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
144
nodes/record/vp_record_task.cpp
Normal file
144
nodes/record/vp_record_task.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
111
nodes/record/vp_record_task.h
Normal file
111
nodes/record/vp_record_task.h
Normal 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
|
||||
};
|
||||
|
||||
}
|
||||
104
nodes/record/vp_video_record_task.cpp
Normal file
104
nodes/record/vp_video_record_task.cpp
Normal 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";
|
||||
}
|
||||
}
|
||||
43
nodes/record/vp_video_record_task.h
Normal file
43
nodes/record/vp_video_record_task.h
Normal 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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user