简介面向Linux音视频与摄像机接入开发场景的实战工程包适合具备一定C/Qt基础、希望打通GUI、设备SDK与视频解码链路的学生和初中级工程师。内容围绕Qt 5.9.1界面构建、大华摄像机SDK设备控制及ffmpeg视频解码展开将三者整合到DH_Camera项目中涉及实时预览、抓图录像、解码数据显示与依赖构建等典型问题。整包约156.52MB共2000个文件类型以C源码、头文件、编译产物、多媒体测试样本、构建脚本与配置说明为主既能反映ffmpeg相关源码与样本组织也能看到Qt工程的构建目录和资源文件。已有1036人学习可作为理解Linux下多媒体应用工程结构、SDK调用边界和Qt与ffmpeg协同方式的参考便于对照梳理模块划分、编译依赖及常见集成环节。1. 从一台总是卡顿的工控机说起Linux Qt 客户端为什么要自己接 FFmpeg去年帮人排过一个现场工控机上跑着自研的预览界面接了两路网络摄像机画面不是卡成 PPT就是隔一会儿绿一片。厂商给的示例程序是 Windows 版的Linux 侧只丢过来一个.so加几个头文件。最初的做法是直接调 SDK 自带的解码显示接口把窗口句柄传进去让它自己画结果整个 UI 线程被拖住按钮点下去半秒才有反应。后来改成 SDK 只负责取裸流FFmpeg 解码Qt 自己渲染问题一次性消掉——这套Linux Qt 摄像机 SDK 取流 FFmpeg 解码的组合就是这份项目的骨架。它要做的事情很具体在一个 Qt 窗口里同时预览多路视频主码流和子码流能切SDK 私有协议和标准 RTSP 两种来源都要吃得下还能把码流落成本地文件、按时间回放。适合谁做安防上位机、工业视觉工位机、机器人图传终端的 Linux C 开发者。前置要求不高会写 Qt 信号槽、看得懂 CMake 就能上手真正需要花时间的是线程边界和 FFmpeg 那套 API 的脾气。2. 整体架构三条取流链路怎么在 Qt 事件循环里共存这个项目最容易被低估的地方不是解码而是“东西往哪儿放”。摄像机 SDK 的回调线程、FFmpeg 的解码线程、Qt 的 UI 线程三者在同一个进程里跑任何一个环节乱来都会以“界面莫名其妙卡一下”的形式报应回来。整套架构的核心约定只有一条数据单向流动跨线程只传值不传指针。SDK 回调线程只负责把字节拷进队列解码线程只负责把字节变成图像UI 线程只负责把最新的一张图贴到窗口上谁都不许越界去碰别人的东西。2.1 目录结构从 src/ 到 third_party/ 的职责切分工程按“来源—解码—呈现”三段切third_party里只放厂商给的东西绝不改它一行代码。厂商 SDK 的头文件基本是 C 风格、命名全大写直接散落在业务代码里会污染整个工程所以统一裹一层sdk_wrapper。camera-client/ ├── CMakeLists.txt ├── config/ │ └── cameras.json # 摄像机 IP、端口、通道、账号运行时可改 ├── src/ │ ├── main.cpp │ ├── ui/ │ │ ├── main_window.cpp # 布局、码流切换、录像按钮 │ │ └── preview_widget.cpp# QWidget 子类只负责画 QImage │ ├── core/ │ │ ├── sdk_wrapper/ # 厂商 so 的唯一入口对外只暴露 C 接口 │ │ ├── stream/ # 取流抽象SDK 源 / RTSP 源同一套接口 │ │ ├── decode/ # H264Decoderparser avcodec │ │ └── record/ # 裸流落盘、按时间戳索引 │ └── common/ │ ├── frame_queue.h # 有界帧队列带丢帧策略 │ └── logger.h └── third_party/ ├── ipc_sdk/{include,lib} # 厂商提供只读 └── ffmpeg/ # 版本固定的头文件避免和生产机不一致stream这一层是整个架构的关键StreamSource是个纯虚基类SdkStreamSource和RtspStreamSource各自实现上层解码器完全不知道自己吃的是私有协议还是标准流。现场遇到只支持 RTSP 的第三方球机时只要新增一个实现类解码和 UI 一行都不用改。这种“多一层抽象”的写法在 Demo 阶段看着啰嗦但接入第二家设备时能省下至少两天。config/cameras.json单独抽出来也是有原因的。现场调试时改 IP、改通道号是高频操作如果编译进二进制每次都要重新交叉编译一遍再拷到工控机上一天下来什么正事都干不了。配置文件热加载改完重启进程即可。2.2 三线程模型谁干活、谁等待、谁绝不阻塞把线程职责列清楚后面写代码时就不容易犯迷糊线程归属职责绝对不能做的事SDK 回调线程厂商库内部创建收到一帧回调深拷贝进队列解码、new QWidget、调用任何 Qt 界面 API解码线程自己std::thread起从队列取帧parser 拆包avcodec 解码转 RGB直接update()界面、长时间持锁UI 线程Qt 主线程定时器驱动重绘只取最新一帧等待网络、等待解码、任何阻塞调用队列用固定容量的有界队列满了丢最旧的帧。这一点是血泪经验早期用的是无界std::deque网络抖动时 SDK 回调猛灌数据队列涨到几万帧内存直接飙到 2G同时延迟累积到十几秒画面里的人在动屏幕上还是十秒前的样子。// core/stream/frame_queue.h // 有界帧队列SDK 回调线程写解码线程读。 // 满了丢最旧的一帧 —— 实时预览里“最新”比“完整”重要得多。 class FrameQueue { public: explicit FrameQueue(size_t cap 90) : cap_(cap) {} void push(Frame f) { std::lock_guardstd::mutex lk(m_); if (q_.size() cap_) q_.pop_front(); // 丢弃最旧不阻塞回调 q_.push_back(std::move(f)); cv_.notify_one(); } bool pop(Frame out, int timeoutMs 200) { std::unique_lockstd::mutex lk(m_); if (!cv_.wait_for(lk, std::chrono::milliseconds(timeoutMs), [] { return !q_.empty() || stop_; })) return false; // 超时返回让解码线程有机会检查退出标志 if (q_.empty()) return false; out std::move(q_.front()); q_.pop_front(); return true; } void stop() { std::lock_guardstd::mutex lk(m_); stop_ true; cv_.notify_all(); } private: std::dequeFrame q_; size_t cap_; bool stop_ false; std::mutex m_; std::condition_variable cv_; };cap取 90 是按 25fps 算的约等于 3.6 秒缓冲现场网络瞬断一下不会立刻花屏但也不会积压太久。pop的timeoutMs不能省否则stop_置位后解码线程还卡在wait上进程退出要等好几秒甚至退不掉。Frame里就三个字段std::vectoruint8_t data、int64_t ptsUs、bool key全部是值语义跨线程传着放心。2.3 编译与运行把 FFmpeg 和厂商 so 都对上依赖版本对不上是新人第一天最容易卡住的地方。Ubuntu 上apt install拿到的 FFmpeg 是发行版锁定的版本头文件和运行库必须来自同一套千万不能出现“编译用 58 的头、运行链到 60 的 so”那种崩溃没有任何提示进程直接消失。cmake_minimum_required(VERSION 3.16) project(camera_client LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) # Q_OBJECT 类必须开忘了就是 vtable 未定义 find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) find_package(PkgConfig REQUIRED) pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libavutil libswscale) set(IPC_SDK_ROOT ${CMAKE_SOURCE_DIR}/third_party/ipc_sdk) add_executable(camera_client src/main.cpp src/core/sdk_wrapper/ipc_sdk.cpp src/core/stream/stream_source.cpp src/core/decode/h264_decoder.cpp src/ui/preview_widget.cpp ) target_include_directories(camera_client PRIVATE ${CMAKE_SOURCE_DIR}/src ${IPC_SDK_ROOT}/include # 厂商头文件 ${FFMPEG_INCLUDE_DIRS}) target_link_directories(camera_client PRIVATE ${IPC_SDK_ROOT}/lib) # 厂商只给 so 所在目录 target_link_libraries(camera_client PRIVATE Qt5::Widgets ipc_sdk # 链接时写 ipc_sdk实际找 libipc_sdk.so ${FFMPEG_LIBRARIES})target_link_directories需要 CMake 3.13 以上这行省不掉——厂商通常不提供.cmake配置文件只能手动指定so目录。链接名去掉了lib前缀和.so后缀写全名反而会失败。# 1. 装编译依赖Ubuntu / Debian 系 sudo apt install -y build-essential cmake pkg-config qtbase5-dev \ libavformat-dev libavcodec-dev libavutil-dev libswscale-dev # 2. 编译 mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc) # 3. 厂商 so 不在系统路径里运行时手动指一下别去改 ld.so.conf export LD_LIBRARY_PATH$PWD/../third_party/ipc_sdk/lib:$LD_LIBRARY_PATH ./camera_client用LD_LIBRARY_PATH而不是往/etc/ld.so.conf.d/里塞配置是因为工控机现场往往要同时装好几个版本的客户端改全局搜索路径迟早出事。-DCMAKE_BUILD_TYPERelease也别省Debug 版下sws_scale的耗时能到 Release 的三倍1080p 四路直接跑不动。3. 摄像机 SDK 取流从登录到回调每一步都在跟线程打交道厂商 SDK 的接口风格通常是“一次初始化、一次登录、一次订阅、剩下全靠回调”回调里给一个指向内部缓冲区的指针。这套模式本身没毛病毛病出在很多人拿到指针就地开始干活——在里面做解码、算时间戳、发信号给 UI。正确姿势只有一句回调里除了 memcpy 和入队什么都别做。下面按调用顺序拆一遍。3.1 初始化与登录参数错一个就是超时登录参数里最容易填错的是端口和通道号。私有协议端口不一定是 HTTP 的 80也不一定是 RTSP 的 554具体数值以厂商文档为准通道号从 0 还是从 1 开始不同厂差异极大接错了表现是登录成功但拉不到流很容易误判成网络问题。参数典型值说明ip192.168.1.64设备地址注意工控机要同网段port按文档私有协议端口填错就是连接超时user/passwd出厂账号密码错误和端口错误的返回码常常是同一个channel0 或 1起始索引因厂而异接不到流先查这个streamType0 主 / 1 子主码流 1080p子码流 720p 或 D1多路预览优先子码流// core/sdk_wrapper/ipc_sdk.cpp // 函数名按厂商头文件替换这里只保留调用顺序和参数形态 bool IpcSdk::login(const CameraInfo info) { // 1. 全局初始化进程内只做一次重复调用多数厂商会返回错误 if (!sdkInited_) { if (IPC_Init() ! IPC_OK) { LOG_E(sdk init failed); return false; } sdkInited_ true; } // 2. 登录拿到设备句柄 IPC_LOGIN_PARAM p{}; std::snprintf(p.ip, sizeof(p.ip), %s, info.ip.c_str()); p.port info.port; p.user const_castchar*(info.user.c_str()); p.passwd const_castchar*(info.passwd.c_str()); handle_ IPC_Login(p); if (handle_ 0) { // 厂商一般用 0 表示失败 LOG_E(login failed, ret%d, handle_); return false; } // 3. 订阅实时流把回调注册进去 IPC_REALPLAY_PARAM rp{}; rp.channel info.channel; rp.streamType info.subStream ? 1 : 0; rp.cb IpcSdk::onStreamCallback; // 必须是静态函数或自由函数 rp.user this; // 把自己传回去回调里还原对象 playHandle_ IPC_RealPlay(handle_, rp); return playHandle_ 0; }回调必须是静态函数这是 C 风格 SDK 的通病它们只认函数指针不认成员函数。惯用解法是登录时通过user字段把this塞进去在静态回调里static_cast回来。另外IPC_Init要在所有登录之前完成如果程序里有多路摄像机务必用一个std::once_flag或者原子标志保护重复初始化在部分厂商的库里会直接导致后续所有登录失败。3.2 回调里只做一件事深拷贝进队列回调函数是被厂商库的内部线程调用的线程优先级、调用频率、是否会重入全部不可控。最保险的写法是校验参数、拷贝数据、入队、立刻返回。整个函数控制在微秒级绝不做任何可能阻塞的事情。// 静态回调运行在 SDK 内部线程上 void IpcSdk::onStreamCallback(int playHandle, IPC_FRAME_INFO* f, void* user) { auto* self static_castIpcSdk*(user); if (!f || f-dataSize 0) return; // 心跳包、空帧直接丢 Frame frame; frame.ptsUs f-ptsUs; // 厂商一般给微秒没有就用本地时钟补 frame.key (f-frameType IPC_FRAME_I); // 必须深拷贝f-data 指向 SDK 复用的内部缓冲区 // 回调返回后随时可能被下一帧覆盖这是花屏的头号来源 frame.data.assign(reinterpret_castuint8_t*(f-data), reinterpret_castuint8_t*(f-data) f-dataSize); self-queue_-push(std::move(frame)); // 内部加锁临界区只有 deque 操作 }assign这一行看着平淡其实是整个取流链路的命门。早期为了省一次内存拷贝直接把f-data指针存进队列解码线程拿到的往往是“下一帧已经写了一半”的缓冲区画面表现是随机位置的花屏一天出现两三次查了整整一周。内存拷贝 1080p 一帧约 200KB25fps 下每秒 5MB 的拷贝量对现代 CPU 来说完全不值一提省这一下毫无意义。ptsUs要留意部分厂商在首帧给0或AV_NOPTS_VALUE之类的无效值需要自己用std::chrono::steady_clock补一个单调递增的时间戳否则后面做录像索引时排序会乱。3.3 RTSP 兼容分支超时和 TCP 传输必须设死现场不可能只有一家设备。甲方后来加了两台第三方球机只支持标准 RTSP这时候StreamSource那层抽象就派上用场了——解码器代码一行没改只新增了一个RtspStreamSource。// core/stream/rtsp_stream_source.cpp bool RtspStreamSource::open(const std::string url) { AVDictionary* opts nullptr; // 默认走 UDP丢包后花屏能持续好几秒局域网也建议强制 TCP av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 3000000, 0); // 3 秒单位微秒新版本改叫 timeout av_dict_set(opts, max_delay, 500000, 0); fmt_ avformat_alloc_context(); // 中断回调拔网线时靠它把 av_read_frame 从阻塞里拽出来 fmt_-interrupt_callback.callback RtspStreamSource::interruptCb; fmt_-interrupt_callback.opaque this; if (avformat_open_input(fmt_, url.c_str(), nullptr, opts) 0) { av_dict_free(opts); return false; } av_dict_free(opts); // 用完必须释放哪怕内容为空 if (avformat_find_stream_info(fmt_, nullptr) 0) return false; videoIdx_ av_find_best_stream(fmt_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); return videoIdx_ 0; } // 返回 1 表示中断FFmpeg 会立刻让当前阻塞调用返回 AVERROR_EXIT int RtspStreamSource::interruptCb(void* opaque) { auto* self static_castRtspStreamSource*(opaque); return self-stopFlag_.load() ? 1 : 0; }stimeout这个键名是历史遗留老版本 FFmpeg 用它较新版本改成了timeout两个都设一遍最省事多余的键会被忽略。中断回调里只能用原子变量不能加锁、不能调av_*函数因为它可能在 FFmpeg 内部的任意位置被调用。RTSP 分支和 SDK 分支最后都往同一个FrameQueue里塞数据只是 RTSP 侧需要先av_read_frame拿到AVPacket再取pkt-dataPTS 换成pkt-pts * av_q2d(stream-time_base) * 1e6转成微秒。上层解码器对此一无所知这正是分层带来的好处。4. FFmpeg 解码与渲染从裸流到屏幕上的那张 QImage拿到 H.264 裸流之后剩下的活是把它变成QImage。这里踩的坑比取流还多因为 FFmpeg 的 API 在 3.x 到 6.x 之间改过好几轮网上搜到的示例一半跑不通。整套链路拆开是四步拆包、送解码器、取帧、转 RGB。4.1 解码器初始化裸流必须过 parser关键区别在于数据来源。RTSP 拿到的AVPacket已经带了 SPS/PPS解码器自己能搞定而 SDK 回调给的是纯裸流SPS/PPS 可能夹在第一帧里也可能根本没给。这时候必须用av_parser先把字节流切成完整的 NAL 单元它顺手会把 SPS/PPS 拼到第一个 packet 前面。// core/decode/h264_decoder.cpp bool H264Decoder::open() { codec_ avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec_) return false; ctx_ avcodec_alloc_context3(codec_); ctx_-flags | AV_CODEC_FLAG_LOW_DELAY; // 预览场景不要帧重排缓冲 ctx_-thread_count 4; // 多路时按核数分摊别每路都 0(自动) ctx_-thread_type FF_THREAD_SLICE; if (avcodec_open2(ctx_, codec_, nullptr) 0) return false; // 裸流专用没有容器提供 extradata靠 parser 拼出完整 NAL parser_ av_parser_init(AV_CODEC_ID_H264); return parser_ ! nullptr; }如果厂商 SDK 会单独给出 SPS/PPS有些接口会在流开始前回调一帧FRAME_TYPE_SPS_PPS那就把这几十个字节手动塞进ctx_-extradata长度写extradata_size再加AV_CODEC_FLAG_GLOBAL_HEADER。这两种做法二选一不要都做重复塞会导致解码器报 “SPS unavailable” 之类的错误。thread_count设 4 而不是 0是因为 8 路预览时如果每路都让 FFmpeg 自动决定它会按机器核数开满几十个线程互相抢 CPU帧率反而掉得厉害。按“核数 / 路数”手工分配实测更稳。4.2 send / receive 循环与时间戳处理新版 FFmpeg 只有一套正确用法avcodec_send_packet送入avcodec_receive_frame取帧两个函数各要循环到返回AVERROR(EAGAIN)为止。裸流的时间戳只能自己造用采集时刻的微秒值灌进pkt-pts和pkt-dts不要试图从字节流里解析。void H264Decoder::feed(const uint8_t* data, int size, int64_t ptsUs) { while (size 0) { uint8_t* out nullptr; int outSize 0; // parse2 返回消耗掉的字节数out/outSize 是切出来的完整 NAL int used av_parser_parse2(parser_, ctx_, out, outSize, data, size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); if (used 0) break; // 防止脏数据导致死循环 data used; size - used; if (outSize 0) decodePacket(out, outSize, ptsUs); } } void H264Decoder::decodePacket(const uint8_t* data, int size, int64_t ptsUs) { AVPacket* pkt av_packet_alloc(); av_new_packet(pkt, size); std::memcpy(pkt-data, data, size); pkt-pts pkt-dts ptsUs; // 用采集时间别信裸流里的值 if (avcodec_send_packet(ctx_, pkt) 0) { AVFrame* f av_frame_alloc(); while (avcodec_receive_frame(ctx_, f) 0) { if (sink_) sink_(toImage(f)); // 转 RGB交给 UI 线程 av_frame_unref(f); // 复用结构体别每次都 free/alloc } av_frame_free(f); // 循环结束才真正释放 } av_packet_free(pkt); // pkt 和 pkt-data 一起回收 }av_frame_unref和av_frame_free的区别是新手最常搞混的前者只清空内容、保留结构体可以反复用后者彻底释放。一轮receive_frame里可能吐出多帧B 帧重排也可能一帧都没有所以unref必须放在循环内。时间戳统一用微秒是刻意的选择。SDK 给微秒RTSP 的AVStream::time_base换算过来也是微秒录像索引和回放跳转时全部用同一个单位省掉一堆整数除法带来的取整误差。4.3 YUV 转 RGBsws_scale 与 QImage 的生命周期解码出来的AVFrame是AV_PIX_FMT_YUV420PQt 画不了得转成Format_RGB32。SwsContext的创建相当贵不能每帧都建要按分辨率缓存——摄像机切主辅码流时分辨率会变这时候才重建。QImage H264Decoder::toImage(AVFrame* f) { if (!sws_ || lastW_ ! f-width || lastH_ ! f-height) { sws_freeContext(sws_); sws_ sws_getContext(f-width, f-height, (AVPixelFormat)f-format, f-width, f-height, AV_PIX_FMT_RGB32, SWS_BILINEAR, nullptr, nullptr, nullptr); lastW_ f-width; lastH_ f-height; } QImage img(f-width, f-height, QImage::Format_RGB32); uint8_t* dst[4] { img.bits(), nullptr, nullptr, nullptr }; int dstStride[4] { static_castint(img.bytesPerLine()), 0, 0, 0 }; // dstStride 必须传 bytesPerLine宽度*4 在非对齐宽度下会错行 sws_scale(sws_, f-data, f-linesize, 0, f-height, dst, dstStride); return img; }bytesPerLine()这行坑过很多人。QImage为了内存对齐每行字节数不一定等于width * 4直接传width * 4会让画面整体斜着错位看起来像“图像撕裂”其实是 stride 对不上。SWS_BILINEAR是画质和速度的平衡点如果要更省 CPU 可以换SWS_FAST_BILINEAR在 1080p 缩到 400x300 的小窗口里肉眼几乎看不出差别。四路 1080p 全用SWS_BICUBIC的话CPU 能吃掉两个核。4.4 渲染与丢帧UI 线程只认最新一帧解码再快也没意义屏幕只有 60Hz。UI 侧用一个 30ms 的QTimer驱动重绘加上“只保留最新帧”的策略解码线程吐多少帧都无所谓画的时候永远拿的是最新那张。// ui/preview_widget.cpp void PreviewWidget::onFrameReady(const QImage img) { QMutexLocker lk(m_); pending_ img; // 覆盖旧值中间帧自动丢弃 hasNew_ true; } void PreviewWidget::paintEvent(QPaintEvent*) { QImage img; { QMutexLocker lk(m_); if (!hasNew_) return; // 没有新帧就保持上一帧别清屏 img pending_; hasNew_ false; } QPainter p(this); p.setRenderHint(QPainter::SmoothPixmapTransform, false); // 关掉缩放开销不小 p.drawImage(rect(), img.scaled(size(), Qt::KeepAspectRatio, Qt::FastTransformation)); }onFrameReady必须通过QMetaObject::invokeMethod(..., Qt::QueuedConnection)或者信号槽跨到 UI 线程绝不能直接在解码线程里调update()。paintEvent的锁范围要尽量小只保护pending_和hasNew_两个字段图像缩放放到锁外面做否则 4 路同时刷新时会互相等锁界面肉眼可见地发涩。SmoothPixmapTransform关掉是有意的这个开关打开后 Qt 会走平滑采样1080p 缩到小窗口时单帧多花 2~3ms四路就是十几毫秒帧率直接掉一档。预览窗口本来就不追求印刷级画质。5. 避坑与排查这套组合在 Linux 上最容易翻车的五个点下面五条都是现场真出过的按“现象 → 原因 → 解决”记下来遇到时能少走弯路。5.1 画面绿屏或上半部分花屏现象程序刚起来第一帧是绿的或者画面下半截是花的过几秒自己好了有时候一直不好。原因解码器拿到的第一个 packet 里没有 SPS/PPS。裸流模式下如果 SDK 的回调从 P 帧开始给中途订阅经常这样avcodec根本不知道分辨率和帧参数只能吐垃圾。另一种情况是 GDRgradual decoder refresh编码关键帧信息分几帧才拼完整。解决一是确认av_parser_init那条路径生效且第一帧确实是 IDR二是订阅后主动丢弃所有非关键帧直到收到frameType IPC_FRAME_I为止再开始解析。这个“等首帧”的逻辑一定要加否则每次切码流都会绿一下。// 订阅后的前处理等到第一个 I 帧再开闸 if (!gotKeyFrame_) { if (!frame.key) return; // 非关键帧直接丢别送进 parser gotKeyFrame_ true; }5.2 跑十分钟内存涨到 2G现象top里 RES 从 200M 一路涨成了阶梯状到最后被 OOM Killer 干掉画面本身看不出来异常。原因三个地方最常见。AVFrame只用av_frame_free释放了最外层没有unrefAVPacket用av_packet_alloc之后忘了freeSwsContext每次toImage都新建一个。还有一种隐蔽的QImage包装了外部缓冲区但没copy()QImage的隐式共享让那块外部内存被反复引用谁都没释放。解决把unref和free的位置固定下来一轮 receive 结束av_frame_free一个 packet 处理完av_packet_freeSwsContext按分辨率缓存QImage一定返回拥有自己 buffer 的实例。验证方式很土但有效用valgrind --leak-checkfull跑十分钟看definitely lost是不是零。5.3 回调线程直接刷 UI偶发段错误现象平时正常切通道或者关窗口时程序直接崩gdb的栈一半在 Qt 的绘制代码里一半在厂商 SDK 的线程里。原因在 SDK 回调里调了update()、setPixmap()或者用Qt::DirectConnection连了跨线程的信号槽。Qt 的绘图必须在主线程跨线程调用属于未定义行为崩不崩全看运气。解决所有跨线程通信统一走Qt::QueuedConnection信号参数用值类型QImage是可隐式共享的值类型可以直接传。窗口关闭时先stop()队列、join解码线程、再IPC_Logout顺序反了就会出现“对象已析构但回调还在跑”。5.4 拔网线后程序卡死不退出现象网络断掉后界面卡住点关闭按钮没反应CtrlC也退不掉只能kill -9。原因av_read_frame在等 socket 数据没有超时会一直阻塞SDK 的IPC_StopRealPlay如果内部在等回调返回也会挂住。这两种阻塞都不在 Qt 的控制范围内。解决RTSP 侧靠interrupt_callback置位中断SDK 侧在退出前先注销回调再停流。另外给整个退出流程加一个 5 秒的兜底定时器超时就_exit()虽然粗暴但比现场打电话让你远程处理要好。5.5 编译通过运行时报找不到 so 或符号未定义现象./camera_client直接报error while loading shared libraries: libxxx.so: cannot open shared object file或者链接阶段一堆undefined reference to ...。原因前者是LD_LIBRARY_PATH没设或者是 32/64 位不匹配厂商只给了 32 位库你编了 64 位程序后者常见于厂商库用老版 GCC 编译std::string的 ABI 和你这边不一致。解决先file libipc_sdk.so看架构再ldd看依赖有没有缺ABI 问题在编译时加-D_GLIBCXX_USE_CXX11_ABI0试试注意这个宏必须对整个工程统一不能只加在一个文件上否则链接期会出现更诡异的符号错位。厂商只给 32 位库的话编译时加-m32并装齐gcc-multilib别想着混编。6. 二次开发把单路 Demo 改成四路预览需要动哪些地方单路跑通之后几乎所有人下一步都是“我想同时看四路”。这件事在架构上不难CameraSession这个类就是为这个准备的把IpcSdk、FrameQueue、H264Decoder、PreviewWidget四件东西打包成一个自洽的单元构造时传一份CameraInfo析构时自己清理干净。四路就是new四个CameraSession主窗口用QGridLayout摆四个PreviewWidget。真正要注意的是资源分配。四路 1080p 主码流在 i5 上解码占用约 60% CPU如果每路thread_count都设 4FFmpeg 会开 16 个解码线程上下文切换的损耗比并行收益还大。我的做法是按路数动态算thread_count std::max(1, cores / sessionCount)四路时每路 2八路时每路 1实测总吞吐比每路 4 高出 15% 左右。改造点改动位置大致工作量单路 → 多路主窗口布局 CameraSession列表半天主辅码流热切换IpcSdk::switchStream 队列重建一天需处理首帧等待本地录像新增Recorder在入队前落盘一天注意加文件头时间轴回放按ptsUs建索引顺序读文件喂解码器两到三天画面叠加 OSDpaintEvent里drawText两小时录像这块有个容易忽略的点裸流落盘的文件没有时间戳索引回放时无法定位。惯常做法是写一个自建的.idx伴生文件每收到一个 I 帧就记一行ptsUs 文件偏移回放拖动进度条时二分查最近的关键帧偏移再seek过去。这套索引格式自己定就行不用兼容任何标准。验证改造成果有个很省事的办法用ffmpeg -re -i test.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/live起一路本地测试流需要先起一个 RTSP 服务把客户端指向它这样可以稳定复现各种分辨率和码率不用每次跑去机房插网线。压测时把码率调到 8Mbps 以上能很快暴露队列积压和内存泄漏问题。最后说个自己的教训。早期做这类项目我总想着“先把功能跑通参数以后再说”结果每次到了现场才发现超时值、队列容量、线程数全都是拍脑袋定的一出问题就得靠猜。从那以后我每次接新的摄像机 SDK都强制先把三件事写死并记在配置里回调线程里的最大拷贝耗时、队列上限对应的秒数、断线重连的退避间隔。这三组数字定下来后面 80% 的“偶发卡顿”都不会发生。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?