safesight/plugins/alarm/actions/clip_action.cpp
sladro 80e7abcd08
Some checks are pending
CI / host-build (push) Waiting to run
CI / rk3588-cross-build (push) Waiting to run
优化性能
2026-01-06 16:26:39 +08:00

250 lines
8.1 KiB
C++

#include "clip_action.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <thread>
#if defined(RK3588_ENABLE_FFMPEG)
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}
#define HAS_FFMPEG 1
#else
#define HAS_FFMPEG 0
#endif
namespace rk3588 {
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);
}
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) {
if (!packet_buffer_) {
std::cerr << "[ClipAction] packet buffer not set\n";
return;
}
if (!frame) return;
const int64_t trigger_pts_ms = frame->pts > 0 ? static_cast<int64_t>(frame->pts / 1000ULL) : 0;
if (trigger_pts_ms <= 0) {
std::cerr << "[ClipAction] invalid trigger pts\n";
return;
}
const int64_t pre_ms = static_cast<int64_t>(std::max(0, pre_sec_)) * 1000LL;
const int64_t post_ms = static_cast<int64_t>(std::max(0, post_sec_)) * 1000LL;
const int64_t start_pts = std::max<int64_t>(0, trigger_pts_ms - pre_ms);
const int64_t end_pts = trigger_pts_ms + post_ms;
// Best-effort wait for post window to arrive.
if (post_sec_ > 0) {
using namespace std::chrono;
const auto deadline = steady_clock::now() + seconds(post_sec_);
while (steady_clock::now() < deadline) {
if (packet_buffer_->LatestPtsMs() >= end_pts) break;
std::this_thread::sleep_for(milliseconds(20));
}
}
auto metas = packet_buffer_->GetPacketsInRange(start_pts, end_pts);
if (metas.empty()) {
// Fallback: at least try the latest packet.
metas = packet_buffer_->GetPacketsInRange(trigger_pts_ms, end_pts);
}
if (metas.empty()) {
std::cerr << "[ClipAction] no packets in range\n";
return;
}
std::string url = ProcessClipFromPackets(metas, event);
if (!url.empty()) {
event.clip_url = url;
std::cout << "[ClipAction] clip uploaded: " << url << "\n";
}
}
std::string ClipAction::ProcessClipFromPackets(const std::vector<std::shared_ptr<EncodedVideoFrameMeta>>& metas,
const AlarmEvent& event) {
#if HAS_FFMPEG
std::shared_ptr<EncodedVideoFrameMeta> first;
for (const auto& m : metas) {
if (m && m->magic == EncodedVideoFrameMeta::kMagic && m->codec && !m->pkt.data.empty()) {
first = m;
break;
}
}
if (!first || !first->codec) return "";
AVCodecID cid = AV_CODEC_ID_NONE;
const auto codec = first->codec->codec;
if (codec == VideoCodec::H264) cid = AV_CODEC_ID_H264;
else if (codec == VideoCodec::H265) cid = AV_CODEC_ID_HEVC;
else {
std::cerr << "[ClipAction] unsupported codec\n";
return "";
}
const int width = first->codec->width;
const int height = first->codec->height;
if (width <= 0 || height <= 0) {
std::cerr << "[ClipAction] invalid codec size\n";
return "";
}
std::filesystem::path tmp_dir;
try {
tmp_dir = std::filesystem::temp_directory_path();
} catch (...) {
tmp_dir = ".";
}
std::filesystem::path tmp_path = tmp_dir / ("clip_" + std::to_string(event.timestamp_ms) + "." + format_);
AVFormatContext* fmt_ctx = nullptr;
if (avformat_alloc_output_context2(&fmt_ctx, nullptr, format_.c_str(), tmp_path.string().c_str()) < 0 || !fmt_ctx) {
std::cerr << "[ClipAction] failed to create output context\n";
return "";
}
AVStream* stream = avformat_new_stream(fmt_ctx, nullptr);
if (!stream) {
avformat_free_context(fmt_ctx);
return "";
}
stream->time_base = AVRational{1, 1000};
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream->codecpar->codec_id = cid;
stream->codecpar->width = width;
stream->codecpar->height = height;
stream->codecpar->format = AV_PIX_FMT_YUV420P;
if (!first->codec->extradata.empty()) {
stream->codecpar->extradata_size = static_cast<int>(first->codec->extradata.size());
stream->codecpar->extradata = static_cast<uint8_t*>(av_mallocz(first->codec->extradata.size() + AV_INPUT_BUFFER_PADDING_SIZE));
std::memcpy(stream->codecpar->extradata, first->codec->extradata.data(), first->codec->extradata.size());
}
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&fmt_ctx->pb, tmp_path.string().c_str(), AVIO_FLAG_WRITE) < 0) {
avformat_free_context(fmt_ctx);
return "";
}
}
if (avformat_write_header(fmt_ctx, nullptr) < 0) {
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) avio_closep(&fmt_ctx->pb);
avformat_free_context(fmt_ctx);
return "";
}
int64_t first_pts = 0;
bool have_first = false;
int64_t last_pts = -1;
const int64_t frame_dur = (fps_ > 0) ? (1000LL / std::max(1, fps_)) : 40;
for (const auto& m : metas) {
if (!m || m->magic != EncodedVideoFrameMeta::kMagic) continue;
if (!m->codec || m->pkt.data.empty()) continue;
if (m->pkt.pts_ms <= 0) continue;
if (!have_first) {
first_pts = m->pkt.pts_ms;
have_first = true;
}
int64_t pts = m->pkt.pts_ms - first_pts;
if (pts < 0) pts = 0;
if (pts <= last_pts) pts = last_pts + 1;
last_pts = pts;
AVPacket* pkt = av_packet_alloc();
if (!pkt) continue;
if (av_new_packet(pkt, static_cast<int>(m->pkt.data.size())) < 0) {
av_packet_free(&pkt);
continue;
}
std::memcpy(pkt->data, m->pkt.data.data(), m->pkt.data.size());
pkt->stream_index = stream->index;
pkt->pts = pts;
pkt->dts = pts;
pkt->duration = frame_dur;
if (m->pkt.key) pkt->flags |= AV_PKT_FLAG_KEY;
(void)av_interleaved_write_frame(fmt_ctx, pkt);
av_packet_free(&pkt);
}
av_write_trailer(fmt_ctx);
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) avio_closep(&fmt_ctx->pb);
avformat_free_context(fmt_ctx);
std::ifstream file(tmp_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ClipAction] failed to read temp file\n";
return "";
}
const size_t file_size = static_cast<size_t>(file.tellg());
file.seekg(0);
std::vector<uint8_t> file_data(file_size);
file.read(reinterpret_cast<char*>(file_data.data()), static_cast<std::streamsize>(file_data.size()));
file.close();
std::string key = GenerateKey(event);
auto result = uploader_->Upload(key, file_data.data(), file_data.size(), "video/mp4");
std::error_code ec;
std::filesystem::remove(tmp_path, ec);
if (result.success) return result.url;
std::cerr << "[ClipAction] upload failed: " << result.error << "\n";
return "";
#else
(void)metas;
(void)event;
std::cerr << "[ClipAction] FFmpeg not enabled\n";
return "";
#endif
}
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