generated from guanyuankai/bonus-edge-proxy
feat(video Service): 完成了测试用的视频加载。
This commit is contained in:
parent
021133c0b4
commit
6c1e6a119c
4
TODO.md
4
TODO.md
|
|
@ -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
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,35 +98,33 @@ 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)) {
|
||||||
spdlog::warn("Frame read failed. Reconnecting...");
|
if (isFileSource) {
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
spdlog::info("End of file reached, looping...");
|
||||||
cap.release();
|
cap.set(cv::CAP_PROP_POS_FRAMES, 0);
|
||||||
cap.open(inputUrl);
|
continue;
|
||||||
continue;
|
} else {
|
||||||
|
spdlog::warn("Frame read failed. Reconnecting...");
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
cap.release();
|
||||||
|
cap.open(inputUrl);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.empty())
|
if (frame.empty())
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue