/* 本程序用于视频分流 1.推流摄像头画面,使用UDP原生协议进行推流,交由YOLO模型进行处理 2.接收YOLO传来的坐标和深度数据 3.根据获取到的数据绘制边框和相应数据 4.根据距离信息进行报警和图片视频保存 5.输出处理完毕的视频帧 */ #include #include #include #include #include #include #include //队列 #include #include #include //双端队列 #include #include "Netra.hpp" #include #include using namespace std; using namespace QCL; using namespace cv; using namespace chrono_literals; namespace bp = boost::process; // 路径和接口 const string mqtt_url = "tcp://127.0.0.1:1883"; const string clientId = "video_subData"; const string Topic = "/video/PersonData"; const string filePath = "/home/orangepi/RKApp/InitAuth/conf/.env"; // 配置保存路径 const string warningPath = "/mnt/save/warning/"; // 报警图片保存路径 const string videoPath = "/mnt/save/video/"; // 报警视频保存路径 // 保存检测结果 struct Dection { double x, y, w, h; // 支持归一化(0~1)或像素值 double distance; }; // 保存报警距离 struct dangerDistance { int danger; int warn; int safe; } dis; struct Point2N { double x, y; }; // 归一化坐标 struct ZoneBox { string name; array vertices; // 存 0..1 }; // 报警框坐标 ZoneBox g_safe, g_warn, g_dang; // 全局变量和对象 VideoCapture cap; // 在 videoInit 中用 V4L2 打开 Mat handleFrame; // 存放处理后的帧 const int Qos = 0; mqtt::async_client client(mqtt_url, clientId); mutex detMutex; // 保护latestDection的互斥锁 vector latestDection; // 保存最新接收到的检测结果 mutex alertMutex; // 保护alertQueue的互斥锁 condition_variable alertcv; // 通知报警线程有新任务 queue alertQueue; // 存放解析后的数据 std::atomic alertWorkerRunning{false}; // 工作线程运行标志 atomic outPutMode = false; // 保存报警输出false--电平 bool mainRunning = true; // 视频相关 const int FPS = 30; // 帧率 const int PRE_RECORD_SECONDS = 10; // 预录制时长 const int MAX_BUFFER_SIZE = FPS * PRE_RECORD_SECONDS; // 缓冲区最大帧数 mutex bufferMutex; // 保护缓冲区的锁 deque videoDeque; // 环形缓冲区,存储最近十秒的画面帧 atomic isRecording{false}; // 是否正在写入文件 atomic mediaMirror{false}; // 是否镜像 atomic mediaFlip{false}; // 是否翻转 // 用于报警线程的“最新一帧检测结果”(丢弃旧帧,保证低延迟) static std::mutex latestAlertMutex; static std::condition_variable latestAlertCv; static std::optional> latestAlertDets; // 仅存最新 static std::atomic latestAlertSeq{0}; // 把阈值/输出模式缓存到内存,避免频繁读文件 struct RuntimeConfig { std::atomic danger{0}; std::atomic warn{0}; std::atomic safe{0}; std::atomic outPutMode{false}; }; static RuntimeConfig g_cfg; // mqtt初始化 void MqttInit(); // 摄像头管道初始化 bool videoInit(VideoCapture &cap); // ffmpeg管道初始化 FILE *pipeInit(); // 对单个帧进行处理 bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, int64 &count, chrono::steady_clock::time_point &t0); // 资源清理 void cleanup(FILE *pipe, VideoCapture &cap); // 主循环 void mainLoop(VideoCapture &cap, FILE *pipe); // mqtt接收订阅消息的回调 void getMsgCallback(mqtt::const_message_ptr msg); // 绘制矩形方框和深度信息 void drawRect(double x, double y, double w, double h, double distance); // 报警线程 void warnThread(); // 获取报警距离和输出模式 bool GetDistance(); // 调用报警输出程序 void setGPIOLevel(int level); // 获取当前时间字符串做文件名 string getCurrentTimeStr(); // 保存图片 void saveAlarmImage(const cv::Mat &frame); // 保存视频 void saveAlarmVideo(std::deque bufferSnapshot); // 从配置文件读取 zones(SAFE/WARN/DANG 的 4 点,归一化值) bool LoadZonesFromEnv(); // 绘制 zones 多边形(将归一化坐标按当前帧尺寸缩放) void drawZones(Mat &img); // 检测框底边是否接触 danger 多边形(用缩放后的像素点) bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox); // 读取配置,获取翻转镜像设置 void loadMirrerSet(); // 进行镜像和翻转 void SetMirror(Mat &frame); // 只在 .env 文件发生修改后才重新加载 zones 和 镜像配置 void ReloadConfigIfChanged(); // 退出信号 void Exit(int sig); int main() { // 确保服务器启动 this_thread::sleep_for(5s); // 初始化摄像头 if (!videoInit(cap)) { return -1; } // 初始化FFmpeg管道 FILE *pipe = pipeInit(); if (!pipe) { return -1; } // 先加载一次区域,避免首帧没有框 LoadZonesFromEnv(); signal(SIGINT, Exit); // 初始化mqtt:订阅,回调函数 MqttInit(); // 主处理循环 mainLoop(cap, pipe); // 清理资源 cleanup(pipe, cap); return 0; } // 只在 .env 文件发生修改后才重新加载 zones 和 镜像配置 void ReloadConfigIfChanged() { static std::filesystem::file_time_type lastEnvWriteTime{}; static bool first = true; std::error_code ec; auto curWriteTime = std::filesystem::last_write_time(filePath, ec); if (ec) { // 获取失败就跳过,避免频繁输出 return; } // 首次调用:直接加载一次并记录时间 if (first) { first = false; lastEnvWriteTime = curWriteTime; LoadZonesFromEnv(); loadMirrerSet(); return; } // 文件未修改则直接返回 if (curWriteTime <= lastEnvWriteTime) { return; } // 文件有更新:重新读取配置 lastEnvWriteTime = curWriteTime; LoadZonesFromEnv(); loadMirrerSet(); } // 进行镜像和翻转 void SetMirror(Mat &frame) { bool mirror = mediaMirror.load(); bool flipV = mediaFlip.load(); if (mirror && flipV) { // 同时镜像和翻转 cv::flip(frame, frame, -1); } else if (mirror) { // 只镜像 cv::flip(frame, frame, 1); } else if (flipV) { // 只翻转 cv::flip(frame, frame, 0); } } // 读取配置,获取翻转镜像设置 void loadMirrerSet() { ReadFile rf(filePath); if (!rf.Open()) { cerr << "文件打开失败" << endl; return; } else { // 解析文件 auto lines = rf.ReadLines(); rf.Close(); auto getBool([&](const string &key, bool &out) { out = false; for(auto&line:lines) { if(line.rfind(key+"=",0)==0) { // 兼容大小写与可能的空格 auto val = line.substr(key.size() + 1); for (auto &c : val) c = ::tolower(c); val.erase(remove_if(val.begin(), val.end(), ::isspace), val.end()); if(val=="true") { out = true; } return out; } } return out; }); bool mirror = false, flip = false; getBool("MEDIA_MIRROR", mirror); getBool("MEDIA_FLIP", flip); mediaMirror.store(mirror); mediaFlip.store(flip); } } // 退出信号 void Exit(int sig) { cout << "Exiting....." << endl; // 停止主循环与报警线程 mainRunning = false; alertWorkerRunning = false; // 唤醒报警线程从 wait/ wait_for 中返回 latestAlertCv.notify_all(); alertcv.notify_all(); } // 检测框底边是否接触 danger 多边形(用缩放后的像素点) bool bottomTouchesDanger(const Dection &d, const ZoneBox &dangerBox) { vector poly; poly.reserve(4); for (auto &p : dangerBox.vertices) { poly.emplace_back(static_cast(p.x * handleFrame.cols), static_cast(p.y * handleFrame.rows)); } auto toPixX = [&](double v) -> int { return (v <= 1.0) ? static_cast(v * handleFrame.cols) : static_cast(v); }; auto toPixY = [&](double v) -> int { return (v <= 1.0) ? static_cast(v * handleFrame.rows) : static_cast(v); }; int x0 = toPixX(d.x); int y0 = toPixY(d.y); int wpx = toPixX(d.w); int hpx = toPixY(d.h); int x1 = x0 + wpx; int yb = y0 + hpx; int samples = max(5, wpx / 20); for (int i = 0; i <= samples; ++i) { int x = x0 + (i * (x1 - x0)) / samples; Point pt(x, yb); double res = pointPolygonTest(poly, pt, false); if (res >= 0) return true; } return false; } // 从配置文件读取 zones(SAFE/WARN/DANG 的 4 点,归一化值) bool LoadZonesFromEnv() { ReadFile rf(filePath); if (!rf.Open()) { cerr << "文件打开失败: " << filePath << endl; return false; } auto lines = rf.ReadLines(); rf.Close(); auto getDouble = [&](const string &key, double &out) { for (auto &line : lines) { if (line.rfind(key + "=", 0) == 0) { try { out = stod(line.substr(key.size() + 1)); return true; } catch (...) { return false; } } } return false; }; auto loadBox = [&](ZoneBox &box, const string &prefix) { box.name = prefix; for (int i = 0; i < 4; ++i) { double x = 0.0, y = 0.0; getDouble(QCL::format("{}_{}_X", prefix, i + 1), x); getDouble(QCL::format("{}_{}_Y", prefix, i + 1), y); box.vertices[i] = {x, y}; // 归一化值 } }; loadBox(g_safe, "SAFE"); loadBox(g_warn, "WARN"); loadBox(g_dang, "DANG"); return true; } // 绘制 zones 多边形(将归一化坐标按当前帧尺寸缩放) void drawZones(Mat &img) { auto drawPoly = [&](const ZoneBox &box, const Scalar &color) { vector pts; pts.reserve(4); for (auto &p : box.vertices) { int px = static_cast(p.x * img.cols); int py = static_cast(p.y * img.rows); pts.emplace_back(px, py); } polylines(img, pts, true, color, 2); }; drawPoly(g_safe, Scalar(0, 255, 0)); // 绿色 drawPoly(g_warn, Scalar(0, 255, 255)); // 黄色 drawPoly(g_dang, Scalar(0, 0, 255)); // 红色 } // 保存图片 void saveAlarmImage(const Mat &frame) { if (frame.empty()) { cerr << "报警图片保存跳过: 帧为空" << endl; return; } string fileName = warningPath + "alarm_" + getCurrentTimeStr() + ".jpg"; cout << "imgpath = " << fileName << endl; if (!imwrite(fileName, frame)) cerr << "图片保存失败" << endl; } // 保存视频 void saveAlarmVideo(deque bufferSnapshot) { if (bufferSnapshot.empty() || bufferSnapshot.front().empty()) { cerr << "报警视频保存跳过: 缓冲为空" << endl; return; } thread([bufferSnapshot]() { string fileName = videoPath + "alarm_" + getCurrentTimeStr() + ".mp4"; VideoWriter write; int codec = write.fourcc('H', '2', '6', '4'); Size size = bufferSnapshot.front().size(); bool color = bufferSnapshot.front().channels() == 3; if (!write.open(fileName, codec, FPS, size, color)) { cerr << "视频文件打开失败: " << fileName << endl; return; } for (auto &ii : bufferSnapshot) { if (!ii.empty()) write.write(ii); } write.release(); }) .detach(); } // 获取当前时间字符串做文件名 string getCurrentTimeStr() { auto now = chrono::system_clock::now(); auto time_t_now = chrono::system_clock::to_time_t(now); stringstream ss; ss << put_time(localtime(&time_t_now), "%Y%m%d_%H%M%S"); return ss.str(); } // 调用报警输出程序 void setGPIOLevel(int level) { string cmd = "echo 'orangepi' | sudo -S /home/orangepi/RKApp/GPIOSignal/bin/sendGpioSignal " + to_string(level); system(cmd.c_str()); } static bool RefreshDistanceConfig() { ReadFile rf(filePath); if (!rf.Open()) return false; auto lines = rf.ReadLines(); rf.Close(); int danger = g_cfg.danger.load(); int warn = g_cfg.warn.load(); int safe = g_cfg.safe.load(); bool opm = g_cfg.outPutMode.load(); for (auto &line : lines) { if (line.find("NEAR_THRESHOLD=") != string::npos) danger = stoi(line.substr(sizeof("NEAR_THRESHOLD=") - 1)); else if (line.find("MID_THRESHOLD=") != string::npos) warn = stoi(line.substr(sizeof("MID_THRESHOLD=") - 1)); else if (line.find("MAX_DISTANCE=") != string::npos) safe = stoi(line.substr(sizeof("MAX_DISTANCE=") - 1)); else if (line.find("outPutMode:") != string::npos) { // 注意:你这里的 key/value 格式看起来不像 ENV 的 KEY=VALUE,确认一下 string val = line.substr(sizeof("outPutMode:")); opm = (val == "true"); } } g_cfg.danger.store(danger); g_cfg.warn.store(warn); g_cfg.safe.store(safe); g_cfg.outPutMode.store(opm); return true; } // 报警线程 void warnThread() { thread([]() { bool isAlarming = false; auto lastDangerTime = chrono::steady_clock::now(); // 初次加载配置 RefreshDistanceConfig(); // 初次设置电平 int normalLevel = g_cfg.outPutMode.load() ? 0 : 1; int alarmLevel = g_cfg.outPutMode.load() ? 1 : 0; setGPIOLevel(normalLevel); // 配置低频刷新(例如 1s 一次),避免每帧 IO auto lastCfgRefresh = chrono::steady_clock::now(); uint64_t seenSeq = latestAlertSeq.load(); while (alertWorkerRunning.load()) { // 等待新数据到来(带超时:即使YOLO不发消息,也能走“离开后2秒恢复”) std::unique_lock lk(latestAlertMutex); bool gotNew = latestAlertCv.wait_for(lk, std::chrono::milliseconds(50), [&] { return !alertWorkerRunning.load() || latestAlertSeq.load() != seenSeq; }); if (!alertWorkerRunning.load()) break; // 低频刷新配置(避免抖动) auto now = chrono::steady_clock::now(); if (now - lastCfgRefresh >= chrono::seconds(1)) { RefreshDistanceConfig(); lastCfgRefresh = now; normalLevel = g_cfg.outPutMode.load() ? 0 : 1; alarmLevel = g_cfg.outPutMode.load() ? 1 : 0; } // 如果超时没收到新消息:视为“当前无检测=离开危险区” if (!gotNew) { if (isAlarming) { auto dur = chrono::duration_cast( chrono::steady_clock::now() - lastDangerTime).count(); if (dur >= 2000) { isAlarming = false; setGPIOLevel(normalLevel); } } continue; } // 有新消息:取最新检测 seenSeq = latestAlertSeq.load(); auto detsOpt = latestAlertDets; lk.unlock(); if (!detsOpt.has_value()) continue; bool currentFrameHasDanger = false; const int dangerTh = g_cfg.danger.load(); // 判定是否危险(距离优先,否则用多边形) for (const auto &d : detsOpt.value()) { if (d.distance > 0.0 && d.distance <= dangerTh) { currentFrameHasDanger = true; break; } if (d.distance == 0.0) { if (bottomTouchesDanger(d, g_dang)) { currentFrameHasDanger = true; break; } } } // 状态机:危险->保持报警;不危险->延迟2秒恢复 if (currentFrameHasDanger) { lastDangerTime = chrono::steady_clock::now(); if (!isAlarming) { isAlarming = true; setGPIOLevel(alarmLevel); // 保存媒体(你原逻辑保留) { lock_guard lk2(bufferMutex); Mat framToSave; deque bufferToSave; if (!videoDeque.empty()) { framToSave = videoDeque.back().clone(); bufferToSave = videoDeque; } else if (!handleFrame.empty()) { framToSave = handleFrame.clone(); } if (!framToSave.empty()) saveAlarmImage(framToSave); if (!bufferToSave.empty()) saveAlarmVideo(bufferToSave); } } } else { if (isAlarming) { auto dur = chrono::duration_cast( chrono::steady_clock::now() - lastDangerTime).count(); if (dur >= 2000) { isAlarming = false; setGPIOLevel(normalLevel); } } } } }) .detach(); } // 获取报警距离 bool GetDistance() { // 获取距离信息 ReadFile rf(filePath); if (rf.Open() == false) { cerr << "文件打开失败" << endl; return false; } auto lines = rf.ReadLines(); string str; for (auto &line : lines) { if (line.find("NEAR_THRESHOLD=") != string::npos) dis.danger = stoi(line.substr(sizeof("NEAR_THRESHOLD=") - 1)); else if (line.find("MID_THRESHOLD=") != string::npos) dis.warn = stoi(line.substr(sizeof("MID_THRESHOLD=") - 1)); else if (line.find("MAX_DISTANCE=") != string::npos) dis.safe = stoi(line.substr(sizeof("MAX_DISTANCE=") - 1)); else if (line.find("outPutMode:") != string::npos) { // 确认输电平模式 string val = line.substr(sizeof("outPutMode:")); outPutMode = (val == "true"); } } rf.Close(); return true; } // 绘制矩形方框和深度信息 void drawRect(double x, double y, double w, double h, double distance) { // 将归一化(0~1)或像素值统一转换为像素 auto toPixX = [&](double v) -> int { return (v <= 1.0) ? static_cast(v * handleFrame.cols) : static_cast(v); }; auto toPixY = [&](double v) -> int { return (v <= 1.0) ? static_cast(v * handleFrame.rows) : static_cast(v); }; int px = toPixX(x); int py = toPixY(y); int pw = toPixX(w); int ph = toPixY(h); // 边界裁剪,避免越界 px = std::max(0, std::min(px, handleFrame.cols - 1)); py = std::max(0, std::min(py, handleFrame.rows - 1)); pw = std::max(1, std::min(pw, handleFrame.cols - px)); ph = std::max(1, std::min(ph, handleFrame.rows - py)); Rect r(px, py, pw, ph); Scalar sca(0, 255, 0); if (!GetDistance()) { sca = Scalar(0, 0, 0); } else if (distance <= dis.danger) sca = Scalar(0, 0, 255); else if (distance <= dis.warn) sca = Scalar(0, 255, 255); rectangle(handleFrame, r, sca, 2); putText(handleFrame, to_string(distance), Point(px, py), FONT_HERSHEY_SIMPLEX, 0.35, Scalar(0, 0, 0)); } // mqtt初始化 void MqttInit() { // 设置回调 client.set_connected_handler([](const string &cause) { cout << "连接成功" << endl; }); client.set_message_callback(getMsgCallback); client.connect()->wait(); client.subscribe(Topic, Qos)->wait(); alertWorkerRunning = true; // 开启报警线程 warnThread(); } // mqtt接收订阅消息的回调:不要起线程,直接更新“最新结果” void getMsgCallback(mqtt::const_message_ptr msg) { const std::string payload = msg->to_string(); try { auto json = nlohmann::json::parse(payload); std::vector dets; dets.reserve(json.size()); for (const auto &ii : json) { Dection d; d.x = static_cast(ii.value("x", 0.0)); d.y = static_cast(ii.value("y", 0.0)); d.w = static_cast(ii.value("w", 0.0)); d.h = static_cast(ii.value("h", 0.0)); d.distance = static_cast(ii.value("distance", 0.0)); dets.push_back(d); } // 更新绘制用的 latestDection(你原来的逻辑) { lock_guard lk(detMutex); latestDection = dets; // 保留给画框使用;如要极致可改成 move + 双缓冲 } // 更新报警用的 “最新结果”(覆盖旧的,避免队列堆积导致延迟) { std::lock_guard lk(latestAlertMutex); latestAlertDets = std::move(dets); latestAlertSeq.fetch_add(1, std::memory_order_relaxed); } latestAlertCv.notify_one(); } catch (const nlohmann::json::parse_error &e) { cerr << "JSON 解析错误: " << e.what() << "\n原始 payload: " << payload << "\n"; } catch (const std::exception &e) { cerr << "处理消息异常: " << e.what() << "\n"; } } // 摄像头初始化 bool videoInit(VideoCapture &cap) { // 显式使用 V4L2,避免走 GStreamer if (cap.isOpened()) cap.release(); if (!cap.open("/dev/video10", cv::CAP_V4L2)) { cerr << "摄像头打开失败:/dev/video10" << endl; return false; } cap.set(CAP_PROP_FRAME_WIDTH, 640); cap.set(CAP_PROP_FRAME_HEIGHT, 480); cap.set(CAP_PROP_FPS, 30); cap.set(CAP_PROP_BUFFERSIZE, 1); // 尝试 MJPG,若不支持则忽略 cap.set(CAP_PROP_FOURCC, VideoWriter::fourcc('M', 'J', 'P', 'G')); double fccv = cap.get(CAP_PROP_FOURCC); char fcc[5] = {(char)((int)fccv & 0xFF), (char)(((int)fccv >> 8) & 0xFF), (char)(((int)fccv >> 16) & 0xFF), (char)(((int)fccv >> 24) & 0xFF), 0}; cout << "摄像头初始化成功 分辨率=" << cap.get(CAP_PROP_FRAME_WIDTH) << "x" << cap.get(CAP_PROP_FRAME_HEIGHT) << " FPS=" << cap.get(CAP_PROP_FPS) << " FOURCC=" << fcc << endl; return true; } // FFmpeg管道初始化 FILE *pipeInit() { FILE *pipe = popen( "ffmpeg " "-nostats -hide_banner -loglevel error " "-f rawvideo -pixel_format bgr24 -video_size 640x480 -framerate 30 -i - " "-c:v h264_rkmpp -rc_mode 2 -qp_init 32 -profile:v baseline -g 1 -bf 0 " "-fflags nobuffer -flags low_delay " "-rtsp_transport tcp -f rtsp rtsp://127.0.0.1:8554/stream", "w"); if (!pipe) { cerr << "FFmpeg管道打开失败" << endl; return nullptr; } // 设置无缓冲模式 setvbuf(pipe, NULL, _IONBF, 0); cout << "FFmpeg管道初始化成功" << endl; return pipe; } // 处理单帧 bool processFrame(VideoCapture &cap, FILE *pipe, Mat &frame, int64 &count, chrono::steady_clock::time_point &t0) { // 读取帧(失败不退出,短休眠重试) if (!cap.read(frame) || frame.empty()) { cerr << "读取帧失败,重试中..." << endl; this_thread::sleep_for(50ms); return true; } handleFrame = frame.clone(); vector destCopy; // 读取最新检测:短锁获取并将结果拷贝到本地变量 { lock_guard lk(detMutex); destCopy = latestDection; // 复制到本地 latestDection.clear(); } // 在主线程上进行绘制 for (const auto &ii : destCopy) { drawRect(ii.x, ii.y, ii.w, ii.h, ii.distance); } // 每3秒刷新一次区域坐标并绘制 static auto lastZonesRefresh = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now(); if (now - lastZonesRefresh >= std::chrono::seconds(5)) { ReloadConfigIfChanged(); lastZonesRefresh = now; } // 绘制三类区域框并按需镜像/翻转(推流输出处理后画面) SetMirror(handleFrame); drawZones(handleFrame); // 确保输出给 FFmpeg 的尺寸与像素格式(BGR24, 640x480) Mat outFrame; if (handleFrame.cols != 640 || handleFrame.rows != 480) resize(handleFrame, outFrame, Size(640, 480)); else outFrame = handleFrame; // 写入管道(推流处理后画面) fwrite(outFrame.data, 1, outFrame.total() * outFrame.elemSize(), pipe); fflush(pipe); // 短锁进行保存 { lock_guard lk(bufferMutex); videoDeque.push_back(handleFrame.clone()); // 确保存入深拷贝 if (videoDeque.size() > MAX_BUFFER_SIZE) videoDeque.pop_front(); } return true; } // 主处理循环 void mainLoop(VideoCapture &cap, FILE *pipe) { int64 count = 0; auto t0 = chrono::steady_clock::now(); Mat frame; cout << "开始视频处理循环..." << endl; // 创建全屏窗口 namedWindow("处理后的画面", WINDOW_NORMAL); setWindowProperty("处理后的画面", WND_PROP_FULLSCREEN, WINDOW_FULLSCREEN); // 获取屏幕尺寸(通过获取全屏窗口的实际大小) cv::Rect windowRect = getWindowImageRect("处理后的画面"); Display *display = XOpenDisplay(nullptr); int screen = DefaultScreen(display); int width = DisplayWidth(display, screen); int height = DisplayHeight(display, screen); Mat displayFrame; // 用于存储缩放后的画面 while (mainRunning) { if (!processFrame(cap, pipe, frame, count, t0)) { break; } // 将 handleFrame 缩放到全屏尺寸 resize(handleFrame, displayFrame, Size(width, height)); imshow("处理后的画面", displayFrame); // imshow("csv", handleFrame); // 检测退出键 if (cv::waitKey(1) == 'q') { cout << "用户请求退出" << endl; alertWorkerRunning = false; break; } } } // 资源清理 void cleanup(FILE *pipe, VideoCapture &cap) { // 防御式确保报警线程条件被唤醒 alertWorkerRunning = false; latestAlertCv.notify_all(); alertcv.notify_all(); try { client.disconnect()->wait(); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; } if (pipe) { pclose(pipe); cout << "FFmpeg管道已关闭" << endl; } if (cap.isOpened()) { cap.release(); cout << "摄像头已释放" << endl; } destroyAllWindows(); cout << "所有资源已清理完毕" << endl; }