230 lines
7.7 KiB
C++
230 lines
7.7 KiB
C++
#include "snapshot_action.h"
|
|
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
|
|
#if defined(RK3588_ENABLE_FFMPEG)
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavutil/imgutils.h>
|
|
#include <libswscale/swscale.h>
|
|
}
|
|
#define HAS_FFMPEG 1
|
|
#else
|
|
#define HAS_FFMPEG 0
|
|
#endif
|
|
|
|
namespace rk3588 {
|
|
|
|
bool SnapshotAction::Init(const SimpleJson& config) {
|
|
format_ = config.ValueOr<std::string>("format", "jpg");
|
|
quality_ = config.ValueOr<int>("quality", 85);
|
|
|
|
if (const SimpleJson* upload_cfg = config.Find("upload")) {
|
|
uploader_ = CreateUploader(*upload_cfg);
|
|
if (!uploader_) {
|
|
std::cerr << "[SnapshotAction] failed to create uploader\n";
|
|
return false;
|
|
}
|
|
} else {
|
|
// Default to local storage
|
|
SimpleJson default_cfg;
|
|
uploader_ = CreateUploader(default_cfg);
|
|
}
|
|
|
|
std::cout << "[SnapshotAction] initialized, format=" << format_
|
|
<< " quality=" << quality_ << "\n";
|
|
return true;
|
|
}
|
|
|
|
void SnapshotAction::Execute(AlarmEvent& event, std::shared_ptr<Frame> frame) {
|
|
if (!frame || !frame->data) {
|
|
std::cerr << "[SnapshotAction] no frame data\n";
|
|
return;
|
|
}
|
|
|
|
auto jpeg_data = EncodeJpeg(frame);
|
|
if (jpeg_data.empty()) {
|
|
std::cerr << "[SnapshotAction] failed to encode JPEG\n";
|
|
return;
|
|
}
|
|
|
|
std::string key = GenerateKey(event);
|
|
auto result = uploader_->Upload(key, jpeg_data.data(), jpeg_data.size(), "image/jpeg");
|
|
|
|
if (result.success) {
|
|
event.snapshot_url = result.url;
|
|
std::cout << "[SnapshotAction] uploaded: " << result.url << "\n";
|
|
} else {
|
|
std::cerr << "[SnapshotAction] upload failed: " << result.error << "\n";
|
|
}
|
|
}
|
|
|
|
std::string SnapshotAction::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();
|
|
}
|
|
|
|
std::vector<uint8_t> SnapshotAction::EncodeJpeg(const std::shared_ptr<Frame>& frame) {
|
|
std::vector<uint8_t> output;
|
|
|
|
#if HAS_FFMPEG
|
|
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
|
|
if (!codec) {
|
|
std::cerr << "[SnapshotAction] MJPEG encoder not found\n";
|
|
return output;
|
|
}
|
|
|
|
AVCodecContext* ctx = avcodec_alloc_context3(codec);
|
|
if (!ctx) return output;
|
|
|
|
ctx->width = frame->width;
|
|
ctx->height = frame->height;
|
|
ctx->time_base = AVRational{1, 25};
|
|
ctx->pix_fmt = AV_PIX_FMT_YUVJ420P;
|
|
|
|
// Set quality (1-31, lower is better for MJPEG)
|
|
int q = 31 - (quality_ * 30 / 100);
|
|
ctx->qmin = q;
|
|
ctx->qmax = q;
|
|
|
|
if (avcodec_open2(ctx, codec, nullptr) < 0) {
|
|
avcodec_free_context(&ctx);
|
|
return output;
|
|
}
|
|
|
|
AVFrame* av_frame = av_frame_alloc();
|
|
av_frame->width = frame->width;
|
|
av_frame->height = frame->height;
|
|
av_frame->format = AV_PIX_FMT_YUVJ420P;
|
|
|
|
if (av_frame_get_buffer(av_frame, 32) < 0) {
|
|
av_frame_free(&av_frame);
|
|
avcodec_free_context(&ctx);
|
|
return output;
|
|
}
|
|
|
|
// Convert input frame to YUV420P
|
|
AVPixelFormat src_fmt = AV_PIX_FMT_NONE;
|
|
if (frame->format == PixelFormat::RGB) {
|
|
src_fmt = AV_PIX_FMT_RGB24;
|
|
} else if (frame->format == PixelFormat::BGR) {
|
|
src_fmt = AV_PIX_FMT_BGR24;
|
|
} else if (frame->format == PixelFormat::NV12) {
|
|
src_fmt = AV_PIX_FMT_NV12;
|
|
} else if (frame->format == PixelFormat::YUV420) {
|
|
src_fmt = AV_PIX_FMT_YUV420P;
|
|
}
|
|
|
|
if (src_fmt == AV_PIX_FMT_NONE) {
|
|
av_frame_free(&av_frame);
|
|
avcodec_free_context(&ctx);
|
|
return output;
|
|
}
|
|
|
|
SwsContext* sws = sws_getContext(
|
|
frame->width, frame->height, src_fmt,
|
|
frame->width, frame->height, AV_PIX_FMT_YUVJ420P,
|
|
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
|
|
|
if (!sws) {
|
|
av_frame_free(&av_frame);
|
|
avcodec_free_context(&ctx);
|
|
return output;
|
|
}
|
|
|
|
// Set up source pointers
|
|
uint8_t* src_data[4] = {nullptr};
|
|
int src_linesize[4] = {0};
|
|
|
|
auto plane_ptr = [&](int idx) -> uint8_t* {
|
|
if (idx < 0 || idx >= frame->plane_count) return nullptr;
|
|
if (frame->planes[idx].data) return frame->planes[idx].data;
|
|
if (!frame->data) return nullptr;
|
|
const int off = frame->planes[idx].offset;
|
|
if (off < 0 || static_cast<size_t>(off) >= frame->data_size) return nullptr;
|
|
return frame->data + off;
|
|
};
|
|
auto plane_stride = [&](int idx, int fallback) -> int {
|
|
if (idx >= 0 && idx < frame->plane_count && frame->planes[idx].stride > 0) {
|
|
return frame->planes[idx].stride;
|
|
}
|
|
if (frame->stride > 0) return frame->stride;
|
|
return fallback;
|
|
};
|
|
auto validate_bounds = [&](uint8_t* ptr, int stride, int h) -> bool {
|
|
if (!frame->data || frame->data_size == 0) return true;
|
|
if (!ptr || stride <= 0 || h <= 0) return false;
|
|
const uint8_t* base = frame->data;
|
|
const uint8_t* end = base + frame->data_size;
|
|
if (ptr < base || ptr >= end) return true; // Unknown ownership; cannot validate.
|
|
const size_t off = static_cast<size_t>(ptr - base);
|
|
const size_t need = off + static_cast<size_t>(stride) * static_cast<size_t>(h);
|
|
return need <= frame->data_size;
|
|
};
|
|
|
|
if (frame->format == PixelFormat::RGB || frame->format == PixelFormat::BGR) {
|
|
src_data[0] = plane_ptr(0) ? plane_ptr(0) : frame->data;
|
|
src_linesize[0] = plane_stride(0, frame->width * 3);
|
|
if (!validate_bounds(src_data[0], src_linesize[0], frame->height)) {
|
|
sws_freeContext(sws);
|
|
av_frame_free(&av_frame);
|
|
avcodec_free_context(&ctx);
|
|
std::cerr << "[SnapshotAction] invalid RGB buffer size/stride (data_size=" << frame->data_size
|
|
<< ", stride=" << src_linesize[0] << ", h=" << frame->height << ")\n";
|
|
return output;
|
|
}
|
|
} else if (frame->format == PixelFormat::NV12) {
|
|
src_data[0] = plane_ptr(0) ? plane_ptr(0) : frame->data;
|
|
src_data[1] = plane_ptr(1);
|
|
if (!src_data[1] && frame->data) {
|
|
src_data[1] = frame->data + static_cast<size_t>(frame->width) * frame->height;
|
|
}
|
|
src_linesize[0] = frame->planes[0].stride > 0 ? frame->planes[0].stride : frame->width;
|
|
src_linesize[1] = frame->planes[1].stride > 0 ? frame->planes[1].stride : frame->width;
|
|
} else if (frame->format == PixelFormat::YUV420) {
|
|
src_data[0] = plane_ptr(0) ? plane_ptr(0) : frame->data;
|
|
src_data[1] = plane_ptr(1);
|
|
src_data[2] = plane_ptr(2);
|
|
src_linesize[0] = frame->planes[0].stride > 0 ? frame->planes[0].stride : frame->width;
|
|
src_linesize[1] = frame->planes[1].stride > 0 ? frame->planes[1].stride : frame->width / 2;
|
|
src_linesize[2] = frame->planes[2].stride > 0 ? frame->planes[2].stride : frame->width / 2;
|
|
}
|
|
|
|
sws_scale(sws, src_data, src_linesize, 0, frame->height,
|
|
av_frame->data, av_frame->linesize);
|
|
sws_freeContext(sws);
|
|
|
|
av_frame->pts = 0;
|
|
|
|
AVPacket* pkt = av_packet_alloc();
|
|
if (avcodec_send_frame(ctx, av_frame) == 0) {
|
|
if (avcodec_receive_packet(ctx, pkt) == 0) {
|
|
output.assign(pkt->data, pkt->data + pkt->size);
|
|
}
|
|
}
|
|
|
|
av_packet_free(&pkt);
|
|
av_frame_free(&av_frame);
|
|
avcodec_free_context(&ctx);
|
|
#else
|
|
// Stub: just create a minimal valid JPEG header for testing
|
|
std::cerr << "[SnapshotAction] FFmpeg not enabled, cannot encode JPEG\n";
|
|
#endif
|
|
|
|
return output;
|
|
}
|
|
|
|
} // namespace rk3588
|