feat(video Service): 完成了测试用的视频加载。

This commit is contained in:
GuanYuankai 2026-01-04 14:52:14 +08:00
parent 021133c0b4
commit 6c1e6a119c
5 changed files with 52 additions and 53 deletions

View File

@ -2,8 +2,6 @@
## 当前任务 (Doing) ## 当前任务 (Doing)
- [ ] 验证 OpenCV 在 RK3588 容器内的调用 (/dev/video0)
- [ ] 接入摄像头设备 rtsp://admin:123456@192.168.1.57:554/stream0
- [ ] 接入 RKNN 模型 - [ ] 接入 RKNN 模型
## 待办 (To Do) ## 待办 (To Do)
@ -17,3 +15,5 @@
- [x] 项目重命名 (EdgeProxy -> RoadCounter) - [x] 项目重命名 (EdgeProxy -> RoadCounter)
- [x] 解决 Git 权限问题 - [x] 解决 Git 权限问题
- [x] 跑通 MQTT 上报基础流程 - [x] 跑通 MQTT 上报基础流程
- [x] 验证 OpenCV 在 RK3588 容器内的调用 (/dev/video0)
- [x] 接入摄像头设备 rtsp://admin:123456@192.168.1.57:554/stream0

BIN
data/test_car.mp4 Normal file

Binary file not shown.

View File

@ -74,7 +74,8 @@ void poll_system_metrics(boost::asio::steady_timer& timer, SystemMonitor::System
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
// TODO: [GYK] DEV#1: 将 URL 放入 config.json 中读取 // TODO: [GYK] DEV#1: 将 URL 放入 config.json 中读取
std::string cam_rtsp_input = "rtsp://admin:123456@192.168.1.57:554/stream0"; // std::string cam_rtsp_input = "rtsp://admin:123456@192.168.1.57:554/stream0";
std::string cam_rtsp_input = "../data/test_car.mp4";
// std::string cam_rtsp_input = // std::string cam_rtsp_input =
// "rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1901"; // "rtsp://admin:hzx12345@192.168.1.10:554/Streaming/Channels/1901";
@ -112,8 +113,6 @@ int main(int argc, char* argv[]) {
AlarmService alarm_service(g_io_context, mqtt_client); AlarmService alarm_service(g_io_context, mqtt_client);
// VideoPipeline video_pipeline;
if (!alarm_service.load_rules(config.getAlarmRulesPath())) { if (!alarm_service.load_rules(config.getAlarmRulesPath())) {
spdlog::error("Failed to load alarm rules. Alarms may be disabled."); spdlog::error("Failed to load alarm rules. Alarms may be disabled.");
} }
@ -180,7 +179,7 @@ int main(int argc, char* argv[]) {
web_server.start(); web_server.start();
spdlog::info("Starting Video Pipeline Service..."); spdlog::info("Starting Video Pipeline Service...");
video_pipeline.Start(cam_rtsp_input, output_stream_url); video_pipeline.StartTest(cam_rtsp_input, output_stream_url);
// video_pipeline.Start(cam_rtsp_input, algorithm_rtsp_output); // video_pipeline.Start(cam_rtsp_input, algorithm_rtsp_output);
boost::asio::signal_set signals(g_io_context, SIGINT, SIGTERM); boost::asio::signal_set signals(g_io_context, SIGINT, SIGTERM);

View File

@ -14,7 +14,17 @@ void VideoPipeline::Start(const std::string& inputUrl, const std::string& output
running_ = true; running_ = true;
spdlog::info("Starting VideoPipeline with Input: {}", inputUrl); spdlog::info("Starting VideoPipeline with Input: {}", inputUrl);
processingThread_ = std::thread(&VideoPipeline::processLoop, this, inputUrl, outputUrl); processingThread_ = std::thread(&VideoPipeline::processLoop, this, inputUrl, outputUrl, false);
}
void VideoPipeline::StartTest(const std::string& filePath, const std::string& outputUrl) {
if (running_)
return;
running_ = true;
spdlog::info("Starting VideoPipeline (File Test Mode) Input: {}", filePath);
// true 表示是文件源
processingThread_ = std::thread(&VideoPipeline::processLoop, this, filePath, outputUrl, true);
} }
void VideoPipeline::Stop() { void VideoPipeline::Stop() {
@ -42,7 +52,7 @@ std::vector<DetectionResult> VideoPipeline::mockInference(const cv::Mat& frame)
res.y = 200; res.y = 200;
res.width = 150; res.width = 150;
res.height = 300; res.height = 300;
res.label = "EV CAR"; res.label = "TEST_CLIP";
res.confidence = 0.95f; res.confidence = 0.95f;
results.push_back(res); results.push_back(res);
@ -61,38 +71,25 @@ void VideoPipeline::drawOverlay(cv::Mat& frame, const std::vector<DetectionResul
cv::Scalar(0, 0, 255), 2); cv::Scalar(0, 0, 255), 2);
} }
void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl) { void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl, bool isFileSource) {
cv::VideoCapture cap; cv::VideoCapture cap;
cap.open(inputUrl); // 文件路径也是通过 open 打开
// [MOD] 尝试设置 FFmpeg 后端参数以减少延迟(可选,依赖于 OpenCV 版本)
// os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp" //
// C++中通常通过API设置或环境变量
cap.open(inputUrl);
if (!cap.isOpened()) { if (!cap.isOpened()) {
spdlog::error("Failed to open input RTSP stream: {}", inputUrl); spdlog::error("Failed to open input: {}", inputUrl);
running_ = false; running_ = false;
return; return;
} }
// [MOD] 强制指定 20 FPS
// 虽然 cap.get 可能读取到 20但为了稳健性我们在输出端强制使用 20
const double TARGET_FPS = 20.0; const double TARGET_FPS = 20.0;
// 计算每帧的目标耗时 (毫秒) -> 1000 / 20 = 50ms
const double FRAME_DURATION_MS = 1000.0 / TARGET_FPS;
int width = cap.get(cv::CAP_PROP_FRAME_WIDTH); int width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT); int height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
spdlog::info("Video Source: {}x{} | Input FPS: {} | Target Output FPS: {}", width, height, spdlog::info("Source: {}x{} | Mode: {}", width, height,
cap.get(cv::CAP_PROP_FPS), TARGET_FPS); isFileSource ? "FILE LOOP" : "LIVE STREAM");
// === [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; std::stringstream pipeline;
pipeline << "appsrc ! " pipeline << "appsrc ! "
@ -101,36 +98,34 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl) {
<< ",framerate=20/1 ! " << ",framerate=20/1 ! "
<< "mpph264enc ! " << "mpph264enc ! "
<< "h264parse ! " << "h264parse ! "
<< "rtspclientsink location=" << outputUrl << "rtspclientsink location=" << outputUrl << " protocols=tcp";
<< " protocols=tcp"; // [MOD] 使用 TCP 协议推流更稳定
spdlog::debug("GStreamer Pipeline: {}", pipeline.str());
cv::VideoWriter writer; cv::VideoWriter writer;
// [MOD] 在 open 时传入 TARGET_FPS (20.0)
writer.open(pipeline.str(), cv::CAP_GSTREAMER, 0, TARGET_FPS, cv::Size(width, height), true); writer.open(pipeline.str(), cv::CAP_GSTREAMER, 0, TARGET_FPS, cv::Size(width, height), true);
if (!writer.isOpened()) { if (!writer.isOpened()) {
spdlog::error("Failed to initialize VideoWriter. Check GStreamer plugins."); spdlog::error("Failed to initialize VideoWriter.");
} }
cv::Mat frame; cv::Mat frame;
// [MOD] 帧率控制辅助
// 如果摄像头实际输出稍微快于或慢于20帧OpenCV的阻塞读取会自动对齐
// 但如果源断流,我们需要处理
while (running_) { while (running_) {
// [MOD] 记录时间以监测实际处理耗时 // 记录循环开始时间
auto start = std::chrono::steady_clock::now(); auto loop_start = std::chrono::steady_clock::now();
if (!cap.read(frame)) { if (!cap.read(frame)) {
if (isFileSource) {
spdlog::info("End of file reached, looping...");
cap.set(cv::CAP_PROP_POS_FRAMES, 0);
continue;
} else {
spdlog::warn("Frame read failed. Reconnecting..."); spdlog::warn("Frame read failed. Reconnecting...");
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
cap.release(); cap.release();
cap.open(inputUrl); cap.open(inputUrl);
continue; continue;
} }
}
if (frame.empty()) if (frame.empty())
continue; continue;
@ -141,18 +136,22 @@ void VideoPipeline::processLoop(std::string inputUrl, std::string outputUrl) {
// 2. 绘制叠加 // 2. 绘制叠加
drawOverlay(frame, results); drawOverlay(frame, results);
// 3. 硬件编码推流 // 3. 推流
if (writer.isOpened()) { if (writer.isOpened()) {
writer.write(frame); writer.write(frame);
} }
// 简单监控处理延迟 if (isFileSource) {
auto end = std::chrono::steady_clock::now(); auto loop_end = std::chrono::steady_clock::now();
std::chrono::duration<double, std::milli> elapsed = end - start; std::chrono::duration<double, std::milli> elapsed = loop_end - loop_start;
// 如果处理太快(例如只是简单的画框,几毫秒就完了), double elapsed_ms = elapsed.count();
// 这里的 cap.read 会自动阻塞等待下一帧,所以不需要手动 sleep。 double wait_ms = FRAME_DURATION_MS - elapsed_ms;
// 只要输入是 20fps这个循环就会被输入流“带”着以 20fps 运行。
if (wait_ms > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds((int)wait_ms));
}
}
} }
cap.release(); cap.release();

View File

@ -25,12 +25,13 @@ public:
// 启动视频流处理 // 启动视频流处理
void Start(const std::string& inputUrl, const std::string& outputUrl); void Start(const std::string& inputUrl, const std::string& outputUrl);
void StartTest(const std::string& filePath, const std::string& outputUrl);
// 停止处理 // 停止处理
void Stop(); void Stop();
private: private:
void processLoop(std::string inputUrl, std::string outputUrl); void processLoop(std::string inputUrl, std::string outputUrl, bool isFileSource);
// 占位算法函数:模拟推理 // 占位算法函数:模拟推理
std::vector<DetectionResult> mockInference(const cv::Mat& frame); std::vector<DetectionResult> mockInference(const cv::Mat& frame);