#include "video_pipeline.hpp" #include VideoPipeline::VideoPipeline() : running_(false) {} VideoPipeline::~VideoPipeline() { Stop(); } void VideoPipeline::Start(const std::string& inputUrl, const std::string& outputUrl) { if (running_) return; running_ = true; spdlog::info("Starting VideoPipeline with Input: {}", inputUrl); processingThread_ = std::thread(&VideoPipeline::processLoop, this, inputUrl, outputUrl); } void VideoPipeline::Stop() { if (!running_) return; running_ = false; if (processingThread_.joinable()) { processingThread_.join(); } spdlog::info("VideoPipeline Stopped."); } std::vector VideoPipeline::mockInference(const cv::Mat& frame) { std::vector results; static int dummyX = 100; static int direction = 5; // 简单的移动逻辑,模拟每帧的变化 dummyX += direction; if (dummyX > frame.cols - 200 || dummyX < 0) direction *= -1; DetectionResult res; res.x = dummyX; res.y = 200; res.width = 150; res.height = 300; res.label = "EV CAR"; res.confidence = 0.95f; results.push_back(res); return results; } void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector& results) { for (const auto& res : results) { cv::rectangle(frame, cv::Rect(res.x, res.y, res.width, res.height), cv::Scalar(0, 255, 0), 2); std::string text = res.label + " " + std::to_string(res.confidence).substr(0, 4); cv::putText(frame, text, cv::Point(res.x, res.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 255, 0), 2); } cv::putText(frame, "RK3588 H.264 @ 20FPS", cv::Point(20, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 0, 255), 2); } void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl) { cv::VideoCapture cap; // [MOD] 尝试设置 FFmpeg 后端参数以减少延迟(可选,依赖于 OpenCV 版本) // os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" // // C++中通常通过API设置或环境变量 cap.open(inputUrl); if (!cap.isOpened()) { spdlog::error("Failed to open input RTSP stream: {}", inputUrl); running_ = false; return; } // [MOD] 强制指定 20 FPS // 虽然 cap.get 可能读取到 20,但为了稳健性,我们在输出端强制使用 20 const double TARGET_FPS = 20.0; int width = cap.get(cv::CAP_PROP_FRAME_WIDTH); int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT); spdlog::info("Video Source: {}x{} | Input FPS: {} | Target Output FPS: {}", width, height, cap.get(cv::CAP_PROP_FPS), TARGET_FPS); // === [MOD] 优化后的 GStreamer H.264 推流管道 === // 1. appsrc: OpenCV 数据源 // 2. videoconvert: 像素格式转换 // 3. capsfilter: 强制转换为 NV12 (RK3588 编码器首选格式) 并 锁定 20/1 帧率 // 4. mpph264enc: Rockchip 硬件 H.264 编码器 // 5. h264parse: 解析 NALU,对 RTSP 传输至关重要 // 6. rtspclientsink: 推流到 MediaMTX std::stringstream pipeline; pipeline << "appsrc ! " << "videoconvert ! " << "video/x-raw,format=NV12,width=" << width << ",height=" << height << ",framerate=20/1 ! " << "mpph264enc ! " << "h264parse ! " << "rtspclientsink location=" << outputUrl << " protocols=tcp"; // [MOD] 使用 TCP 协议推流更稳定 spdlog::debug("GStreamer Pipeline: {}", pipeline.str()); cv::VideoWriter writer; // [MOD] 在 open 时传入 TARGET_FPS (20.0) writer.open(pipeline.str(), cv::CAP_GSTREAMER, 0, TARGET_FPS, cv::Size(width, height), true); if (!writer.isOpened()) { spdlog::error("Failed to initialize VideoWriter. Check GStreamer plugins."); } cv::Mat frame; // [MOD] 帧率控制辅助 // 如果摄像头实际输出稍微快于或慢于20帧,OpenCV的阻塞读取会自动对齐 // 但如果源断流,我们需要处理 while (running_) { // [MOD] 记录时间以监测实际处理耗时 auto start = std::chrono::steady_clock::now(); if (!cap.read(frame)) { spdlog::warn("Frame read failed. Reconnecting..."); std::this_thread::sleep_for(std::chrono::seconds(1)); cap.release(); cap.open(inputUrl); continue; } if (frame.empty()) continue; // 1. 算法处理 auto results = mockInference(frame); // 2. 绘制叠加 drawOverlay(frame, results); // 3. 硬件编码推流 if (writer.isOpened()) { writer.write(frame); } // 简单监控处理延迟 auto end = std::chrono::steady_clock::now(); std::chrono::duration elapsed = end - start; // 如果处理太快(例如只是简单的画框,几毫秒就完了), // 这里的 cap.read 会自动阻塞等待下一帧,所以不需要手动 sleep。 // 只要输入是 20fps,这个循环就会被输入流“带”着以 20fps 运行。 } cap.release(); writer.release(); }