safesight-edge/plugins/alarm/actions/clip_action.cpp
sladro 05b8b0319c
Some checks are pending
CI / host-build (push) Waiting to run
CI / rk3588-cross-build (push) Waiting to run
修bug
2026-01-05 18:00:05 +08:00

417 lines
14 KiB
C++

#include "clip_action.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#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/imgutils.h>
#include <libavutil/opt.h>
}
#define HAS_FFMPEG 1
#else
#define HAS_FFMPEG 0
#endif
namespace rk3588 {
namespace {
inline uint8_t ClipU8(int v) {
if (v < 0) return 0;
if (v > 255) return 255;
return static_cast<uint8_t>(v);
}
inline const uint8_t* PlanePtr(const Frame& f, int idx) {
if (idx < 0 || idx >= f.plane_count) return nullptr;
if (f.planes[idx].data) return f.planes[idx].data;
if (!f.data) return nullptr;
const int off = f.planes[idx].offset;
if (off < 0) return nullptr;
return f.data + static_cast<size_t>(off);
}
inline int PlaneStride(const Frame& f, int idx, int fallback) {
if (idx >= 0 && idx < f.plane_count && f.planes[idx].stride > 0) return f.planes[idx].stride;
if (f.stride > 0) return f.stride;
return fallback;
}
bool FillYuv420pFromFrame(const Frame& src, int width, int height, AVFrame* dst) {
if (!dst) return false;
if (dst->format != AV_PIX_FMT_YUV420P) return false;
if (width <= 0 || height <= 0) return false;
uint8_t* y = dst->data[0];
uint8_t* u = dst->data[1];
uint8_t* v = dst->data[2];
const int y_stride = dst->linesize[0];
const int u_stride = dst->linesize[1];
const int v_stride = dst->linesize[2];
if (!y || !u || !v || y_stride <= 0 || u_stride <= 0 || v_stride <= 0) return false;
if (src.width != width || src.height != height) return false;
if (src.format == PixelFormat::YUV420) {
const uint8_t* sy = PlanePtr(src, 0);
const uint8_t* su = PlanePtr(src, 1);
const uint8_t* sv = PlanePtr(src, 2);
if (!sy || !su || !sv) return false;
const int sy_stride = PlaneStride(src, 0, width);
const int su_stride = PlaneStride(src, 1, width / 2);
const int sv_stride = PlaneStride(src, 2, width / 2);
for (int row = 0; row < height; ++row) {
std::memcpy(y + row * y_stride, sy + row * sy_stride, static_cast<size_t>(width));
}
const int uv_h = height / 2;
const int uv_w = width / 2;
for (int row = 0; row < uv_h; ++row) {
std::memcpy(u + row * u_stride, su + row * su_stride, static_cast<size_t>(uv_w));
std::memcpy(v + row * v_stride, sv + row * sv_stride, static_cast<size_t>(uv_w));
}
return true;
}
if (src.format == PixelFormat::NV12) {
const uint8_t* sy = PlanePtr(src, 0);
const uint8_t* suv = PlanePtr(src, 1);
if (!sy) return false;
const int sy_stride = PlaneStride(src, 0, width);
const int suv_stride = PlaneStride(src, 1, width);
if (!suv) {
if (!src.data) return false;
suv = src.data + static_cast<size_t>(sy_stride) * static_cast<size_t>(height);
}
for (int row = 0; row < height; ++row) {
std::memcpy(y + row * y_stride, sy + row * sy_stride, static_cast<size_t>(width));
}
const int uv_h = height / 2;
const int uv_w = width / 2;
for (int row = 0; row < uv_h; ++row) {
const uint8_t* src_uv = suv + row * suv_stride;
uint8_t* dst_u = u + row * u_stride;
uint8_t* dst_v = v + row * v_stride;
for (int col = 0; col < uv_w; ++col) {
dst_u[col] = src_uv[col * 2 + 0];
dst_v[col] = src_uv[col * 2 + 1];
}
}
return true;
}
if (src.format == PixelFormat::RGB || src.format == PixelFormat::BGR) {
const bool is_bgr = (src.format == PixelFormat::BGR);
const uint8_t* s = PlanePtr(src, 0);
if (!s) s = src.data;
if (!s) return false;
const int s_stride = PlaneStride(src, 0, width * 3);
const int uv_w = width / 2;
const int uv_h = height / 2;
for (int row = 0; row < height; row += 2) {
const uint8_t* row0 = s + row * s_stride;
const uint8_t* row1 = (row + 1 < height) ? (s + (row + 1) * s_stride) : row0;
uint8_t* y0 = y + row * y_stride;
uint8_t* y1 = (row + 1 < height) ? (y + (row + 1) * y_stride) : y0;
const int uv_row = row / 2;
uint8_t* uu = (uv_row < uv_h) ? (u + uv_row * u_stride) : nullptr;
uint8_t* vv = (uv_row < uv_h) ? (v + uv_row * v_stride) : nullptr;
for (int col = 0; col < width; col += 2) {
int u_sum = 0;
int v_sum = 0;
int samples = 0;
auto sample = [&](const uint8_t* p, uint8_t* ydst) {
const int b = is_bgr ? p[0] : p[2];
const int g = p[1];
const int r = is_bgr ? p[2] : p[0];
const int yy = (77 * r + 150 * g + 29 * b + 128) >> 8;
*ydst = ClipU8(yy);
u_sum += (-43 * r - 84 * g + 127 * b);
v_sum += (127 * r - 106 * g - 21 * b);
samples += 1;
};
const uint8_t* p00 = row0 + col * 3;
const uint8_t* p01 = (col + 1 < width) ? (row0 + (col + 1) * 3) : p00;
const uint8_t* p10 = row1 + col * 3;
const uint8_t* p11 = (col + 1 < width) ? (row1 + (col + 1) * 3) : p10;
sample(p00, &y0[col]);
if (col + 1 < width) sample(p01, &y0[col + 1]);
if (row + 1 < height) {
sample(p10, &y1[col]);
if (col + 1 < width) sample(p11, &y1[col + 1]);
} else {
sample(p10, &y1[col]);
if (col + 1 < width) sample(p11, &y1[col + 1]);
}
const int denom = samples > 0 ? samples : 1;
const int u_val = ((u_sum / denom) + 128 * 256 + 128) >> 8;
const int v_val = ((v_sum / denom) + 128 * 256 + 128) >> 8;
const int uv_col = col / 2;
if (uu && vv && uv_col >= 0 && uv_col < uv_w) {
uu[uv_col] = ClipU8(u_val);
vv[uv_col] = ClipU8(v_val);
}
}
}
return true;
}
return false;
}
} // namespace
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 (!frame_buffer_) {
std::cerr << "[ClipAction] frame buffer not set\n";
return;
}
if (!frame) return;
uint64_t trigger_ts_ms = 0;
if (!frame_buffer_->FindTimestampByFrameId(frame->frame_id, trigger_ts_ms)) {
using namespace std::chrono;
trigger_ts_ms = static_cast<uint64_t>(duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
}
const uint64_t pre_ms = static_cast<uint64_t>(std::max(0, pre_sec_)) * 1000ULL;
const uint64_t post_ms = static_cast<uint64_t>(std::max(0, post_sec_)) * 1000ULL;
const uint64_t start_ts = (trigger_ts_ms > pre_ms) ? (trigger_ts_ms - pre_ms) : 0;
const uint64_t end_ts = trigger_ts_ms + post_ms;
// Best-effort: wait for post-event window to elapse so frames are available in ring buffer.
if (post_sec_ > 0) {
std::this_thread::sleep_for(std::chrono::seconds(post_sec_));
}
auto frames = frame_buffer_->GetFramesInRange(start_ts, end_ts);
if (frames.empty()) {
frames.push_back(frame);
}
std::string url = ProcessClip(frames, event);
if (!url.empty()) {
event.clip_url = url;
std::cout << "[ClipAction] clip uploaded: " << url << "\n";
}
}
std::string ClipAction::ProcessClip(const std::vector<std::shared_ptr<Frame>>& frames, const AlarmEvent& event) {
#if HAS_FFMPEG
if (frames.empty()) {
std::cerr << "[ClipAction] no frames to process\n";
return "";
}
std::shared_ptr<Frame> sample_frame;
for (const auto& f : frames) {
if (f) { sample_frame = f; break; }
}
if (!sample_frame) return "";
int width = sample_frame->width;
int height = sample_frame->height;
if (width <= 0 || height <= 0) {
std::cerr << "[ClipAction] invalid frame size\n";
return "";
}
// Create temporary file
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_);
// Set up FFmpeg output
AVFormatContext* fmt_ctx = nullptr;
if (avformat_alloc_output_context2(&fmt_ctx, nullptr, format_.c_str(), tmp_path.string().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.string().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 = [&](const std::shared_ptr<Frame>& frame) {
if (!frame) return;
if (!FillYuv420pFromFrame(*frame, width, height, av_frame)) return;
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);
}
};
for (const auto& f : frames) {
encode_frame(f);
}
// 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(event);
auto result = uploader_->Upload(key, file_data.data(), file_data.size(), "video/mp4");
// Clean up temp file
std::error_code ec;
std::filesystem::remove(tmp_path, ec);
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