#include "clip_action.h" #include #include #include #include #include #include #include #include #include #include #include #if defined(RK3588_ENABLE_FFMPEG) extern "C" { #include #include #include #include } #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(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(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(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(uv_w)); std::memcpy(v + row * v_stride, sv + row * sv_stride, static_cast(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(sy_stride) * static_cast(height); } for (int row = 0; row < height; ++row) { std::memcpy(y + row * y_stride, sy + row * sy_stride, static_cast(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("pre_sec", 5); post_sec_ = config.ValueOr("post_sec", 10); format_ = config.ValueOr("format", "mp4"); fps_ = config.ValueOr("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) { 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(duration_cast(steady_clock::now().time_since_epoch()).count()); } const uint64_t pre_ms = static_cast(std::max(0, pre_sec_)) * 1000ULL; const uint64_t post_ms = static_cast(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>& frames, const AlarmEvent& event) { #if HAS_FFMPEG if (frames.empty()) { std::cerr << "[ClipAction] no frames to process\n"; return ""; } std::shared_ptr 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) { 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 file_data(file_size); file.read(reinterpret_cast(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