322 lines
9.7 KiB
C++
322 lines
9.7 KiB
C++
#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
|