#include "clip_action.h" #include #include #include #include #include #include #include #include #include #include #if defined(RK3588_ENABLE_FFMPEG) extern "C" { #include #include #include } #define HAS_FFMPEG 1 #else #define HAS_FFMPEG 0 #endif namespace rk3588 { namespace { static bool HasAnnexBStartCode(const uint8_t* d, size_t n) { if (!d || n < 4) return false; for (size_t i = 0; i + 3 < n; ++i) { if (d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 1) return true; if (i + 4 < n && d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 0 && d[i + 3] == 1) return true; } return false; } static size_t FindStartCode(const uint8_t* d, size_t n, size_t from, size_t* sc_len) { for (size_t i = from; i + 3 < n; ++i) { if (d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 1) { if (sc_len) *sc_len = 3; return i; } if (i + 4 < n && d[i] == 0 && d[i + 1] == 0 && d[i + 2] == 0 && d[i + 3] == 1) { if (sc_len) *sc_len = 4; return i; } } if (sc_len) *sc_len = 0; return n; } static std::vector> SplitAnnexBNals(const uint8_t* d, size_t n) { std::vector> out; if (!d || n < 4) return out; size_t pos = 0; while (true) { size_t sc_len = 0; size_t sc = FindStartCode(d, n, pos, &sc_len); if (sc >= n) break; size_t nal_start = sc + sc_len; size_t next_sc = FindStartCode(d, n, nal_start, nullptr); size_t nal_end = (next_sc >= n) ? n : next_sc; // Trim trailing zeros before next start code. while (nal_end > nal_start && d[nal_end - 1] == 0) --nal_end; if (nal_end > nal_start) { out.emplace_back(d + nal_start, d + nal_end); } pos = nal_end; } return out; } static bool BuildAvccFromAnnexB(const std::vector& annexb, std::vector& avcc_out) { avcc_out.clear(); if (annexb.empty()) return false; auto nals = SplitAnnexBNals(annexb.data(), annexb.size()); const uint8_t* sps = nullptr; size_t sps_len = 0; const uint8_t* pps = nullptr; size_t pps_len = 0; for (const auto& nal : nals) { if (nal.empty()) continue; const uint8_t t = nal[0] & 0x1F; if (t == 7 && !sps) { sps = nal.data(); sps_len = nal.size(); } else if (t == 8 && !pps) { pps = nal.data(); pps_len = nal.size(); } } if (!sps || sps_len < 4 || !pps || pps_len < 1) return false; // AVCDecoderConfigurationRecord // https://developer.apple.com/documentation/quicktime-file-format/avcdecoderconfigurationrecord avcc_out.reserve(11 + sps_len + pps_len); avcc_out.push_back(1); // configurationVersion avcc_out.push_back(sps[1]); // AVCProfileIndication avcc_out.push_back(sps[2]); // profile_compatibility avcc_out.push_back(sps[3]); // AVCLevelIndication avcc_out.push_back(0xFF); // 6 bits reserved + lengthSizeMinusOne(3 => 4 bytes) avcc_out.push_back(0xE1); // 3 bits reserved + numOfSequenceParameterSets(1) avcc_out.push_back(static_cast((sps_len >> 8) & 0xFF)); avcc_out.push_back(static_cast((sps_len) & 0xFF)); avcc_out.insert(avcc_out.end(), sps, sps + sps_len); avcc_out.push_back(1); // numOfPictureParameterSets avcc_out.push_back(static_cast((pps_len >> 8) & 0xFF)); avcc_out.push_back(static_cast((pps_len) & 0xFF)); avcc_out.insert(avcc_out.end(), pps, pps + pps_len); return true; } static std::vector AnnexBToLengthPrefixed(const uint8_t* d, size_t n) { std::vector out; auto nals = SplitAnnexBNals(d, n); // Each NAL becomes [len_be32][nal_bytes] size_t total = 0; for (const auto& nal : nals) total += 4 + nal.size(); out.reserve(total); for (const auto& nal : nals) { const uint32_t len = static_cast(nal.size()); out.push_back(static_cast((len >> 24) & 0xFF)); out.push_back(static_cast((len >> 16) & 0xFF)); out.push_back(static_cast((len >> 8) & 0xFF)); out.push_back(static_cast((len) & 0xFF)); out.insert(out.end(), nal.begin(), nal.end()); } return out; } } // 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 (!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(frame->pts / 1000ULL) : 0; if (trigger_pts_ms <= 0) { std::cerr << "[ClipAction] invalid trigger pts\n"; return; } const int64_t pre_ms = static_cast(std::max(0, pre_sec_)) * 1000LL; const int64_t post_ms = static_cast(std::max(0, post_sec_)) * 1000LL; const int64_t start_pts = std::max(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>& metas, const AlarmEvent& event) { #if HAS_FFMPEG std::shared_ptr 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 ""; } const int fps_eff = std::max(1, (first->codec->fps > 0 ? first->codec->fps : fps_)); // Use a stable timescale to avoid rounding drift and "fast playback" caused by bad source PTS. stream->time_base = AVRational{1, 90000}; stream->avg_frame_rate = AVRational{fps_eff, 1}; stream->r_frame_rate = stream->avg_frame_rate; 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; // MP4 requires AVCC/HVCC style extradata and length-prefixed samples. std::vector mp4_extradata; if (!first->codec->extradata.empty()) { const auto& ex = first->codec->extradata; if (cid == AV_CODEC_ID_H264) { if (!ex.empty() && ex[0] == 1) { mp4_extradata = ex; // AVCC } else if (HasAnnexBStartCode(ex.data(), ex.size())) { if (!BuildAvccFromAnnexB(ex, mp4_extradata)) { std::cerr << "[ClipAction] failed to build AVCC from AnnexB extradata\n"; } } } } if (cid == AV_CODEC_ID_H264 && mp4_extradata.empty()) { // As a fallback, try extracting SPS/PPS from the first keyframe packet. for (const auto& m : metas) { if (!m || m->magic != EncodedVideoFrameMeta::kMagic) continue; if (!m->pkt.key) continue; if (!HasAnnexBStartCode(m->pkt.data.data(), m->pkt.data.size())) continue; if (BuildAvccFromAnnexB(m->pkt.data, mp4_extradata)) break; } } if (!mp4_extradata.empty()) { stream->codecpar->extradata_size = static_cast(mp4_extradata.size()); stream->codecpar->extradata = static_cast(av_mallocz(mp4_extradata.size() + AV_INPUT_BUFFER_PADDING_SIZE)); std::memcpy(stream->codecpar->extradata, mp4_extradata.data(), mp4_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 ""; } // Start from the first keyframe inside the window to ensure decodability. size_t start_idx = 0; for (size_t i = 0; i < metas.size(); ++i) { if (metas[i] && metas[i]->pkt.key) { start_idx = i; break; } } const int64_t frame_dur = av_rescale_q(1, AVRational{1, fps_eff}, stream->time_base); int64_t frame_idx = 0; for (size_t i = start_idx; i < metas.size(); ++i) { const auto& m = metas[i]; if (!m || m->magic != EncodedVideoFrameMeta::kMagic) continue; if (!m->codec || m->pkt.data.empty()) continue; if (m->pkt.pts_ms <= 0) continue; const int64_t pts = av_rescale_q(frame_idx, AVRational{1, fps_eff}, stream->time_base); std::vector sample = m->pkt.data; if (cid == AV_CODEC_ID_H264 && HasAnnexBStartCode(sample.data(), sample.size())) { sample = AnnexBToLengthPrefixed(sample.data(), sample.size()); } if (sample.empty()) continue; AVPacket* pkt = av_packet_alloc(); if (!pkt) continue; if (av_new_packet(pkt, static_cast(sample.size())) < 0) { av_packet_free(&pkt); continue; } std::memcpy(pkt->data, sample.data(), sample.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); ++frame_idx; } 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(file.tellg()); file.seekg(0); std::vector file_data(file_size); file.read(reinterpret_cast(file_data.data()), static_cast(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