399 lines
14 KiB
C++
399 lines
14 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 {
|
|
|
|
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<std::vector<uint8_t>> SplitAnnexBNals(const uint8_t* d, size_t n) {
|
|
std::vector<std::vector<uint8_t>> 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<uint8_t>& annexb, std::vector<uint8_t>& 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<uint8_t>((sps_len >> 8) & 0xFF));
|
|
avcc_out.push_back(static_cast<uint8_t>((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<uint8_t>((pps_len >> 8) & 0xFF));
|
|
avcc_out.push_back(static_cast<uint8_t>((pps_len) & 0xFF));
|
|
avcc_out.insert(avcc_out.end(), pps, pps + pps_len);
|
|
return true;
|
|
}
|
|
|
|
static std::vector<uint8_t> AnnexBToLengthPrefixed(const uint8_t* d, size_t n) {
|
|
std::vector<uint8_t> 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<uint32_t>(nal.size());
|
|
out.push_back(static_cast<uint8_t>((len >> 24) & 0xFF));
|
|
out.push_back(static_cast<uint8_t>((len >> 16) & 0xFF));
|
|
out.push_back(static_cast<uint8_t>((len >> 8) & 0xFF));
|
|
out.push_back(static_cast<uint8_t>((len) & 0xFF));
|
|
out.insert(out.end(), nal.begin(), nal.end());
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // 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 (!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 "";
|
|
}
|
|
|
|
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<uint8_t> 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<int>(mp4_extradata.size());
|
|
stream->codecpar->extradata = static_cast<uint8_t*>(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<uint8_t> 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<int>(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<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
|