generated from guanyuankai/bonus-edge-proxy
161 lines
4.7 KiB
C++
161 lines
4.7 KiB
C++
|
|
#include "video_pipeline.hpp"
|
|||
|
|
|
|||
|
|
#include <chrono>
|
|||
|
|
|
|||
|
|
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<DetectionResult> VideoPipeline::mockInference(const cv::Mat& frame) {
|
|||
|
|
std::vector<DetectionResult> 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<DetectionResult>& 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<double, std::milli> elapsed = end - start;
|
|||
|
|
|
|||
|
|
// 如果处理太快(例如只是简单的画框,几毫秒就完了),
|
|||
|
|
// 这里的 cap.read 会自动阻塞等待下一帧,所以不需要手动 sleep。
|
|||
|
|
// 只要输入是 20fps,这个循环就会被输入流“带”着以 20fps 运行。
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cap.release();
|
|||
|
|
writer.release();
|
|||
|
|
}
|