简介:这份源码包面向在 Linux 环境下开发海康监控视频应用的 C++/Qt 开发者,定位为可运行的 QTdemo 示例工程,适合具备 Qt 基础、希望理解网络视频流接入与播放流程的系统集成商、安防监控从业者及嵌入式学习者。压缩包共 220 个文件,约 1.2MB,以 h 头文件、cpp 源文件、ui 界面文件和 png 图标资源为主,辅以 pri/pro 工程配置、qrc 资源脚本及少量 bmp、txt、qm 等文件,便于直接编译、拆解与二次改造。已有 1154 人学习,具备一定参考热度。工程围绕 RTSP/HTTP 视频流获取、多媒体播放、视频画面显示、设备发现与参数配置、多线程任务拆分、错误处理与日志记录等环节展开,涵盖登录、实时预览、录像回放、通道参数和远程参数等模块。读者可据此梳理 Linux 下海康摄像头接入监控应用的目录组织、类职责划分与关键调用关系,作为课程设计、项目原型或定制化监控系统开发的参考底稿。
1. 先看定位:Linux 监控视频 QT demo 源码到底解决哪段链路
一位做边缘盒子的开发者拿到一份 Linux 监控视频 QT demo 源码,第一反应通常不是看界面,而是先确认它能不能在目标机器上把某品牌网络摄像机的实时画面稳定拉出来。这份源码的价值不在 UI 多漂亮,而在它把 Linux 下的设备登录、取流回调、画面渲染、抓图和退出释放串成了一条最小闭环,适合需要快速验证监控视频链路的 C++/Qt 开发者。它更像一块底板:新手能照着跑通,熟手能沿着源码拆出设备适配层、线程边界和二次开发接口。判断它是否适合你,就看三点:目标架构有没有对应 SDK 动态库、Qt 版本能不能编过、回调到界面的线程模型是否清晰。
2. 跑通环境:Qt5 + 厂商SDK动态库在 Linux 上的最小闭环
2.1 目录结构与文件清单
先别急着改代码,拿到 Linux 源码包后第一件事是把目录看明白。监控视频 QT demo 通常不会只有一个main.cpp,它至少要把界面、设备会话、取流回调和厂商 SDK 的头文件/动态库分开,否则后面换设备、加通道、做重连都会缠在一起。一个合理的骨架大致如下,实际源码包可能命名不同,但分层逻辑不会差太远。
monitor_qt_demo/ ├── app/ │ ├── main.cpp │ ├── MainWindow.cpp/.h │ └── PreviewWidget.cpp/.h ├── core/ │ ├── DeviceSession.cpp/.h │ ├── StreamWorker.cpp/.h │ └── FrameQueue.h ├── sdk/ │ ├── include/vendor_sdk.h │ └── lib/libvendorsdk.so ├── scripts/ │ └── run.sh └── monitor_qt_demo.pro| 目录/文件 | 作用 | 跑通时要盯的点 |
|---|---|---|
app/ | Qt 窗口、预览控件、程序入口 | 主线程只做 UI,不要在这里直接调阻塞式取流 |
core/ | 设备登录、预览句柄、回调队列 | 句柄生命周期和线程边界最容易出问题 |
sdk/include | 厂商 SDK 头文件 | 头文件版本要和.so对得上 |
sdk/lib | 厂商 SDK 动态库 | 架构要匹配,x86_64 和 aarch64 不能混用 |
scripts/run.sh | 启动脚本 | 里面通常要设置动态库搜索路径 |
这份结构的好处是:界面层只关心“有一帧画面来了”,设备层关心“登录、开始预览、停止预览、登出”,SDK 适配层关心“厂商函数怎么调”。后面你要换品牌、加多通道、做录像,基本都在这三层之间扩展,而不是把代码全塞进MainWindow。
如果源码包里的.pro文件已经把sdk/include和sdk/lib写死成绝对路径,先改成相对路径。常见写法是$$PWD/sdk/include和-L$$PWD/sdk/lib。这一步看起来小,换一台机器编译就会翻车,尤其是把源码包从开发机拷到现场盒子时,绝对路径基本必坏。
2.2 依赖安装与 .pro 配置
Linux 下跑 Qt 监控视频 demo,编译依赖通常分成三块:C++ 编译工具链、Qt5 Widgets 开发包、厂商 SDK 运行库。不同发行版的包名不一样,常见做法是先装build-essential、qtbase5-dev、qt5-qmake,再用ldd检查 SDK 动态库依赖有没有缺。不要一上来就改业务代码,先把编译和动态库加载跑通。
# 常见 Linux 发行版上的编译依赖,包名按实际系统调整 sudo apt install build-essential qtbase5-dev qt5-qmake # 进入源码目录后先看 SDK 动态库架构,避免 x86_64 机器拿了 arm 库 file sdk/lib/libvendorsdk.so uname -m # 生成 Makefile 并编译 qmake monitor_qt_demo.pro make -j$(nproc)逻辑说明:qmake读取.pro生成 Makefile,make按依赖编译。file和uname -m用来确认动态库架构与当前机器一致,监控视频 SDK 经常分 x86_64、aarch64 两个包,拿错包时编译可能过,运行才报cannot open shared object file。参数说明:-j$(nproc)按 CPU 核数并行编译,现场机器核数少时可以改成-j2,避免内存不够把编译进程杀掉。
.pro里要把 Qt 模块、C++ 标准、头文件路径和链接库写清楚。下面是一个可抄的骨架,库名按实际 SDK 替换:
QT += core gui widgets CONFIG += c++11 TEMPLATE = app TARGET = monitor_qt_demo INCLUDEPATH += $$PWD/sdk/include $$PWD/core $$PWD/app LIBS += -L$$PWD/sdk/lib -lvendorsdk -lpthread SOURCES += \ app/main.cpp \ app/MainWindow.cpp \ app/PreviewWidget.cpp \ core/DeviceSession.cpp \ core/StreamWorker.cpp HEADERS += \ app/MainWindow.h \ app/PreviewWidget.h \ core/DeviceSession.h \ core/StreamWorker.h # 让可执行文件优先从同目录 sdk/lib 找库,减少现场配置成本 QMAKE_LFLAGS += -Wl,-rpath,\'\$$ORIGIN/sdk/lib\'逻辑说明:QT += widgets是界面必需,CONFIG += c++11保证 C++11 语法能编过。LIBS里的-lvendorsdk对应libvendorsdk.so,如果源码里库名不同,只改这一处。QMAKE_LFLAGS写入rpath,运行时优先从可执行文件旁边的sdk/lib找库,比每次手动export LD_LIBRARY_PATH稳。参数说明:$$PWD是.pro所在目录,$$ORIGIN是运行时可执行文件目录,写错会导致 rpath 不生效。
提示:如果编译报头文件找不到,先看
INCLUDEPATH是否指到sdk/include;如果链接报undefined reference,先确认LIBS里的库名和实际.so文件名是否一致。
2.3 首次编译与运行步骤
编译通过不等于能出画面。监控视频 demo 的首次运行要按“动态库加载 → 设备登录 → 开始预览 → 回调出帧 → 界面刷新”的顺序逐步确认。常见启动脚本会设置动态库路径,再启动 Qt 程序。下面这种写法适合先排掉环境问题:
#!/usr/bin/env bash set -e # 当前脚本所在目录,避免从其他路径启动时找不到 sdk/lib APP_DIR="$(cd "$(dirname "$0")" && pwd)" export LD_LIBRARY_PATH="$APP_DIR/sdk/lib:$LD_LIBRARY_PATH" # 如果界面起不来,先打开 Qt 插件调试,看缺哪个 xcb 依赖 # export QT_DEBUG_PLUGINS=1 cd "$APP_DIR" ./monitor_qt_demo逻辑说明:LD_LIBRARY_PATH只影响当前进程,适合临时排查;长期部署更建议用rpath或系统库目录。QT_DEBUG_PLUGINS=1是 Qt 界面起不来时的排查开关,能看到平台插件加载到哪一步失败。参数说明:APP_DIR通过dirname "$0"得到脚本所在目录,cd过去再启动,避免资源文件相对路径错位。
运行后先看控制台有没有 SDK 初始化日志,再看登录返回值,最后看预览回调有没有帧。如果窗口出来了但画面黑屏,不要先怀疑界面控件,先确认回调有没有进、帧宽高和格式对不对。监控视频链路里“窗口正常、画面黑”是最常见的假象,实际卡在取流或颜色空间转换上。
3. 核心链路拆解:从设备登录到回调取流的线程模型
3.1 登录与预览句柄的封装
监控视频 demo 的核心不是按钮,而是句柄。设备登录成功后拿到一个会话句柄,开始预览再拿到预览句柄,抓图、云台、停止预览都依赖这两个句柄。如果直接把句柄散落在窗口类里,后面加多通道、断线重连就会变成全局变量大乱斗。常见做法是抽一个DeviceSession,把登录、开始预览、停止预览、登出收在一处,外部只拿状态和帧回调。
// core/DeviceSession.h #pragma once #include <string> #include <functional> struct DeviceInfo { std::string ip; int port = 8000; std::string user; std::string password; }; struct FrameData { int width = 0; int height = 0; int format = 0; // 由 SDK 适配层定义,例如 0=YUV420P int stride = 0; // 每行字节数,不能默认等于 width unsigned char* data = nullptr; int size = 0; }; class DeviceSession { public: using FrameCallback = std::function<void(const FrameData&)>; bool login(const DeviceInfo& info); bool startPreview(int channel, int streamType, FrameCallback cb); void stopPreview(); void logout(); private: long sessionHandle_ = -1; long previewHandle_ = -1; FrameCallback frameCallback_; };逻辑说明:DeviceInfo把 IP、端口、用户名、密码收在一起,避免登录函数参数过长。FrameData里保留stride,是因为很多 SDK 回调的一行字节数不等于画面宽度,直接按width * height拷贝会花屏。FrameCallback用std::function接住上层传入的回调,适配层不需要知道 Qt 控件。参数说明:channel是通道号,streamType通常分主码流和子码流,预览阶段先用子码流降压力。
实现登录时要注意 SDK 全局初始化和反初始化。常见做法是进程启动时初始化一次,退出时清理一次,不要每个窗口都 init。登录失败要打日志,把返回码和错误信息带出来,否则现场只能看到“登录失败”四个字,排查全靠猜。
// core/DeviceSession.cpp 片段,vendor_* 为抽象后的 SDK 适配函数 bool DeviceSession::login(const DeviceInfo& info) { if (sessionHandle_ >= 0) { return true; // 已经登录,避免重复拿句柄 } vendor_login_param_t param{}; param.ip = info.ip.c_str(); param.port = info.port; param.user = info.user.c_str(); param.password = info.password.c_str(); sessionHandle_ = vendor_login(¶m); if (sessionHandle_ < 0) { // 把 SDK 返回码带出来,现场排查比一句“失败”有用 fprintf(stderr, "vendor_login failed, code=%ld\n", sessionHandle_); return false; } return true; }逻辑说明:先判断sessionHandle_是否已经有效,避免重复登录把旧句柄覆盖掉。vendor_login是抽象名,实际替换成厂商 SDK 的登录函数,参数结构按头文件填。参数说明:ip和user/password用c_str()传指针,注意info生命周期要覆盖调用期间;端口常见是设备服务端口,不是 RTSP 端口,填错会登录超时。
开始预览时,预览句柄要和会话句柄分开管理。停止预览只释放预览句柄,登出才释放会话句柄,顺序反了容易出现退出崩溃。
bool DeviceSession::startPreview(int channel, int streamType, FrameCallback cb) { if (sessionHandle_ < 0 || previewHandle_ >= 0) { return false; } frameCallback_ = std::move(cb); vendor_preview_param_t p{}; p.channel = channel; p.streamType = streamType; p.onFrame = [](long, const vendor_frame_t* f, void* user) { auto* self = static_cast<DeviceSession*>(user); if (!self || !self->frameCallback_ || !f) return; FrameData frame{}; frame.width = f->width; frame.height = f->height; frame.format = f->format; frame.stride = f->stride; frame.data = const_cast<unsigned char*>(f->data); frame.size = f->size; self->frameCallback_(frame); // 注意:这里仍在 SDK 回调线程 }; p.user = this; previewHandle_ = vendor_start_preview(sessionHandle_, &p); return previewHandle_ >= 0; }逻辑说明:onFrame是 SDK 回调线程,不能在里面直接操作 Qt 控件,所以这里只把帧结构转给上层,由上层决定入队还是丢帧。user传this,回调里再转回DeviceSession。参数说明:streamType主码流清晰但带宽高,子码流适合多路预览;channel从 1 开始还是从 0 开始要看 SDK 定义,不要照抄别的项目。
3.2 回调线程与 UI 线程的边界
监控视频 demo 最容易翻车的地方,是回调线程直接刷界面。SDK 回调频率可能 25 帧每秒甚至更高,如果每一帧都在回调里创建QImage、调setPixmap,界面线程会被拖死,严重时直接崩溃。血泪经验是:回调线程只做“拷贝最新帧 + 入队”,UI 线程定时取最新帧渲染,中间队列允许丢旧帧,保证画面实时性。
// core/StreamWorker.h 简化片段 #pragma once #include <deque> #include <mutex> #include <QObject> #include "DeviceSession.h" class StreamWorker : public QObject { Q_OBJECT public: void pushFrame(const FrameData& f) { std::lock_guard<std::mutex> lock(mutex_); // 只保留最新两帧,解码/渲染慢时丢旧帧,避免内存上涨 if (frames_.size() >= 2) { frames_.pop_front(); } FrameData copy = f; copy.data = new unsigned char[f.size]; memcpy(copy.data, f.data, f.size); frames_.push_back(copy); emit frameReady(); } signals: void frameReady(); private: std::deque<FrameData> frames_; std::mutex mutex_; };逻辑说明:pushFrame在 SDK 回调线程执行,先加锁,再控制队列长度。这里用深拷贝,因为 SDK 回调里的f->data生命周期通常只在回调期间有效,直接存指针下一帧就变成野指针。emit frameReady()发出信号,Qt 默认跨线程连接会走队列,槽函数在 UI 线程执行。参数说明:队列长度 2 是常见折中,太小会卡顿,太大延迟高;如果做录像,不能在这里丢帧,要另走一条落盘队列。
UI 侧取帧时不要每帧都update()多次,可以合并刷新。下面这种写法把“取最新帧”和“触发重绘”分开,槽函数只标记有新帧,paintEvent里再转换和绘制。
void PreviewWidget::onFrameReady() { // 槽函数在 UI 线程,只取走最新一帧,避免在绘制里长时间持锁 std::lock_guard<std::mutex> lock(worker_->mutex_); if (worker_->frames_.empty()) return; latestFrame_ = worker_->frames_.back(); worker_->frames_.clear(); hasNewFrame_ = true; update(); }逻辑说明:onFrameReady由信号触发,运行在 UI 线程;取帧时加锁时间要短,取完清空队列。hasNewFrame_用来避免无意义的重复绘制。参数说明:latestFrame_保存的是深拷贝数据,绘制完要释放;如果使用QImage外部缓冲,要确保缓冲生命周期覆盖绘制过程。
注意:不要在 SDK 回调里直接调用
QLabel::setPixmap、QWidget::update或任何 Qt 控件方法,跨线程访问 UI 是监控 demo 最常见的崩溃来源。
3.3 画面渲染:QImage 与 QPainter 的落地写法
回调数据常见是 YUV420P,Qt 显示通常要转成 RGB。demo 里可以写一个简化的转换函数,先把画面跑出来,再考虑性能和硬件加速。转换时一定要按stride取行,不能假设每行正好等于宽度。
// 简化版 YUV420P 转 RGB888,只用于跑通链路 QImage yuv420pToRgb(const FrameData& f) { QImage img(f.width, f.height, QImage::Format_RGB888); const unsigned char* yPlane = f.data; const unsigned char* uPlane = f.data + f.stride * f.height; const unsigned char* vPlane = uPlane + (f.stride / 2) * (f.height / 2); for (int y = 0; y < f.height; ++y) { unsigned char* dst = img.scanLine(y); for (int x = 0; x < f.width; ++x) { int Y = yPlane[y * f.stride + x]; int U = uPlane[(y / 2) * (f.stride / 2) + (x / 2)] - 128; int V = vPlane[(y / 2) * (f.stride / 2) + (x / 2)] - 128; int R = Y + 1.402 * V; int G = Y - 0.344 * U - 0.714 * V; int B = Y + 1.772 * U; dst[x * 3 + 0] = qBound(0, R, 255); dst[x * 3 + 1] = qBound(0, G, 255); dst[x * 3 + 2] = qBound(0, B, 255); } } return img; }逻辑说明:yPlane指向 Y 分量,uPlane和vPlane按 YUV420P 的平面排列偏移。每行用f.stride取 Y,UV 平面宽度按stride / 2算。qBound防止 RGB 越界。参数说明:f.format要确认是 YUV420P,如果 SDK 回调的是 RGB 或 NV12,这个函数不能直接用;width和height建议是偶数,奇数分辨率要补边。
绘制时用QPainter把QImage缩放到控件区域,保持宽高比,避免画面拉伸。监控预览一般还要叠加通道名、时间戳和状态文字,这些都在paintEvent里画,不要改原始帧数据。
void PreviewWidget::paintEvent(QPaintEvent*) { QPainter painter(this); painter.fillRect(rect(), Qt::black); if (!hasNewFrame_ || latestFrame_.empty()) return; QImage img = yuv420pToRgb(latestFrame_); QSize target = img.size().scaled(size(), Qt::KeepAspectRatio); QRect drawRect(QPoint((width() - target.width()) / 2, (height() - target.height()) / 2), target); painter.drawImage(drawRect, img); painter.setPen(Qt::white); painter.drawText(8, 20, QStringLiteral("CH1 %1x%2") .arg(latestFrame_.width).arg(latestFrame_.height)); }逻辑说明:先填黑底,避免无帧时窗口花屏;scaled(..., Qt::KeepAspectRatio)保持比例,drawRect居中。状态文字直接画在控件上,不侵入帧数据。参数说明:latestFrame_必须是有效深拷贝,否则paintEvent里访问会崩;如果帧率高,转换函数要移到工作线程或改用 GPU,别在paintEvent里做重活。
4. 功能模块实战:抓图、录像、云台控制和异常重连
4.1 抓图与录像的落盘策略
抓图是监控 demo 最容易被低估的功能。看起来只是把当前帧存成图片,实际要处理格式、路径、磁盘空间和失败重试。常见做法是在 UI 线程拿到最新帧后转QImage,再用QImage::save落盘;文件名带通道号和时间戳,避免覆盖。路径不要写死到开发机目录,现场可能没有写权限。
bool saveSnapshot(const FrameData& frame, const QString& dir, QString* err) { if (frame.data == nullptr || frame.width <= 0 || frame.height <= 0) { if (err) *err = QStringLiteral("empty frame"); return false; } QDir().mkpath(dir); // 目录不存在时创建,现场部署少一次手工操作 QImage img = yuv420pToRgb(frame); QString name = QStringLiteral("ch1_%1.jpg") .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz")); QString path = QDir(dir).filePath(name); if (!img.save(path, "JPG", 90)) { if (err) *err = QStringLiteral("save failed: %1").arg(path); return false; } return true; }逻辑说明:先校验帧数据,再创建目录,转 RGB 后保存。文件名用毫秒时间戳,避免同一秒多次抓图覆盖。参数说明:dir建议放在可写目录,例如用户目录或挂载盘;JPG质量 90 是清晰度和体积的折中,抓图取证可以用 PNG,但体积更大。
录像比抓图复杂。demo 里如果没有录像模块,常见补法是两条路:一条用厂商 SDK 自带的录像接口,省去编码;另一条是把帧送进 FFmpeg 编码成 MP4。前者依赖 SDK 能力,后者灵活但要处理时间戳、关键帧和封装。无论哪条,落盘都要做分片,比如每 10 分钟一个文件,否则长时间录像一个文件损坏就全丢。
4.2 云台控制接口的参数映射
云台控制不是每个 demo 都有,但监控项目二次开发经常要加。接口一般分方向、速度、通道和停止命令。方向命令不要自己发明数字,按 SDK 头文件映射;速度范围也要看文档,常见是 1 到 7 或 1 到 10,传 0 可能表示停止,也可能无效。
enum class PtzCommand { Up, Down, Left, Right, ZoomIn, ZoomOut, Stop }; bool ptzControl(long sessionHandle, int channel, PtzCommand cmd, int speed) { int sdkCmd = 0; switch (cmd) { case PtzCommand::Up: sdkCmd = VENDOR_PTZ_UP; break; case PtzCommand::Down: sdkCmd = VENDOR_PTZ_DOWN; break; case PtzCommand::Left: sdkCmd = VENDOR_PTZ_LEFT; break; case PtzCommand::Right: sdkCmd = VENDOR_PTZ_RIGHT; break; case PtzCommand::ZoomIn: sdkCmd = VENDOR_PTZ_ZOOM_IN; break; case PtzCommand::ZoomOut: sdkCmd = VENDOR_PTZ_ZOOM_OUT; break; case PtzCommand::Stop: sdkCmd = VENDOR_PTZ_STOP; break; } // 速度范围按 SDK 文档裁剪,避免传越界值导致设备无响应 speed = qBound(1, speed, 7); return vendor_ptz_control(sessionHandle, channel, sdkCmd, speed, 0) == 0; }逻辑说明:把界面按钮映射成枚举,再映射成 SDK 命令,避免界面层直接出现魔法数字。qBound把速度限制在合法范围。参数说明:channel要和预览通道一致;最后一个参数常见是停止标志或速度扩展位,按头文件填。按下按钮发开始命令,松开按钮发停止命令,否则云台会一直转。
4.3 断线重连与状态机
监控视频现场网络不会永远稳定。demo 如果不做重连,网线一拔就要重启程序。常见做法是给会话加一个简单状态机:空闲、登录中、预览中、重连中、停止。回调超过一定时间没有新帧,就认为断流,先停预览、登出,再按退避间隔重登。
enum class SessionState { Idle, LoggingIn, Previewing, Reconnecting, Stopped }; void DeviceSession::checkTimeout(qint64 nowMs) { if (state_ != SessionState::Previewing) return; if (nowMs - lastFrameMs_ < 3000) return; // 3 秒无帧认为断流 state_ = SessionState::Reconnecting; stopPreview(); logout(); // 退避重连,避免设备侧被高频登录打满 QTimer::singleShot(2000, this, [this] { if (login(info_)) { startPreview(channel_, streamType_, frameCallback_); state_ = SessionState::Previewing; } else { state_ = SessionState::Reconnecting; } }); }逻辑说明:每次收到帧更新lastFrameMs_,定时器检查超时。断流后先释放旧句柄再重登,避免句柄泄漏。QTimer::singleShot做延迟重连,参数 2000 是毫秒。参数说明:3 秒阈值适合低帧率场景,如果码流帧间隔大,要调大;重连退避可以做成递增,第一次 2 秒,第二次 5 秒,避免网络恢复瞬间大量登录。
5. 避坑排查:Linux 下跑监控视频 demo 最容易翻车的 5 个点
5.1 动态库加载与运行时路径
现象:编译通过,运行直接报error while loading shared libraries: libvendorsdk.so: cannot open shared object file,窗口都出不来。
原因:可执行文件找不到厂商 SDK 动态库。常见是LD_LIBRARY_PATH没设置、.so架构不对,或者库依赖的次级.so缺失。监控 SDK 往往还依赖系统里的多媒体库,缺一个就整体加载失败。
解决:先用ldd ./monitor_qt_demo | grep "not found"看缺哪个库,再用file sdk/lib/libvendorsdk.so和uname -m确认架构一致。临时跑用export LD_LIBRARY_PATH=$PWD/sdk/lib:$LD_LIBRARY_PATH,长期部署在.pro里写rpath,或者把库放到系统库目录并执行ldconfig。不要只改脚本不改部署文档,现场换机器又会踩一遍。
5.2 回调线程直接刷 UI 的崩溃
现象:预览几秒后程序崩溃,堆栈在 Qt 绘制相关函数;或者画面越跑越卡,最后无响应。
原因:SDK 回调线程里直接调用了QLabel::setPixmap、QWidget::update等 UI 方法。Qt 控件不是线程安全的,跨线程访问属于玄学崩溃,有时能跑,有时一跑就挂。
解决:回调里只做数据拷贝和入队,通过信号槽把“有新帧”通知到 UI 线程,槽函数里再取帧、转图、update()。队列要限制长度,渲染慢时丢旧帧,不要无限堆积。排查时可以在回调里打印线程 ID,和 UI 线程 ID 对比,确认是不是跨线程。
5.3 码流格式与颜色空间错位
现象:画面能出来,但颜色偏绿、偏紫,或者画面斜着花、下半部分错位。
原因:把 YUV 当 RGB 用,或者拷贝时没按stride取行。监控 SDK 回调常见 YUV420P、NV12、RGB 三种格式,分辨率宽度也不是永远等于每行字节数,直接memcpy(width * height)会错位。
解决:先打印回调里的width、height、format、stride,确认格式后再选转换函数。YUV420P 按平面取,NV12 按 Y 平面加 UV 交错取,RGB 直接拷贝。stride大于width时,每行只拷width个有效字节,行间按stride跳。转换函数先写慢速正确版,跑通后再优化。
5.4 多通道释放与重复登录
现象:单通道正常,加第二路后偶发崩溃;退出程序时卡死或报句柄无效。
原因:多个通道共用一个会话句柄但没有做引用计数,或者停止预览、登出顺序反了。还有的代码在窗口关闭时重复调用登出,第二次拿已经释放的句柄去操作。
解决:一个设备一个DeviceSession,一个通道一个预览句柄,停止预览只释放预览句柄,登出才释放会话句柄。析构里按“停预览 → 登出 → SDK 清理”的顺序走,所有释放操作做幂等判断。多通道压测时开 4 路以上,反复开关窗口,看句柄数和内存是否持续上涨。
5.5 防火墙、权限与设备侧编码
现象:登录成功但一直黑屏;或者抓图能出、预览不出;换一台设备又正常。
原因:设备侧端口没通、账号权限不够、编码格式用了 SDK 不支持的 H.265,或者主码流带宽太高导致回调超时。Linux 侧还可能是当前用户没有写抓图目录的权限。
解决:先用厂商工具或ping、端口探测确认网络通,再确认账号有预览权限。设备侧先把编码切到 H.264、子码流、较低分辨率,排除编码和带宽问题。抓图失败先看目录权限和磁盘空间,不要只盯代码。现场排查按“网络 → 登录 → 预览 → 回调 → 渲染”顺序走,别跳步。
6. 二次开发:把 demo 改成可维护的预览组件
6.1 抽一层设备适配接口
demo 能跑之后,第一件事不是加按钮,而是把厂商 SDK 调用收进接口。这样以后换设备、换库、写单元测试都不至于改遍界面。接口只暴露登录、预览、抓图、云台、登出,帧数据用统一结构体传递。
class IVideoDevice { public: virtual ~IVideoDevice() = default; virtual bool login(const DeviceInfo& info) = 0; virtual bool startPreview(int channel, int streamType) = 0; virtual void stopPreview() = 0; virtual bool snapshot(const QString& path) = 0; virtual bool ptz(PtzCommand cmd, int speed) = 0; virtual void logout() = 0; };逻辑说明:界面层只依赖IVideoDevice,具体实现放在VendorDevice里。snapshot和ptz返回布尔值,界面根据结果提示成功或失败。参数说明:streamType用枚举代替数字,path用QString方便 Qt 侧处理中文路径。抽接口时不要一次追求大而全,先把预览链路稳住。
6.2 用状态机和配置表替换硬编码
第二件事是把 IP、端口、通道、码流类型从代码里挪到配置文件。现场改设备信息不应该重新编译。状态机负责登录、预览、重连、停止的迁移,配置表负责多设备多通道。改造清单可以按下面这张表推进:
| 改造点 | demo 常见写法 | 二次开发建议 |
|---|---|---|
| 设备信息 | 写死在MainWindow | 放 JSON/INI,启动加载 |
| 通道管理 | 单通道变量 | 每通道一个会话对象 |
| 重连 | 手动重启程序 | 超时检测 + 退避重连 |
| 渲染 | UI 线程转 YUV | 工作线程转换,UI 只绘制 |
| 日志 | printf | 分级日志 + 文件轮转 |
6.3 验证方法与改造清单
改完之后别只看“能出画面”。至少跑四组验证:单通道连续预览 8 小时看内存;4 路同时预览看 CPU 和回调丢帧;拔网线 30 秒再插回看重连;反复开关窗口 50 次看句柄和线程是否释放。抓图验证要检查文件名、目录权限和图片能否打开。云台验证要确认按下开始、松开停止,不能一直转。
从那以后我每次接监控视频 demo,都强制先跑一遍断网重连和 8 小时空转,再谈业务改造。希望帮到你。
本文还有配套的精品资源,点击获取