首页 / 资讯中心 / 文章详情

Qt+ffmpeg+IPC SDK:Linux 多路视频预览链路搭建与避坑

Qt+ffmpeg+IPC SDK:Linux 多路视频预览链路搭建与避坑 ★ FEATURED ARTICLE
简介面向Linux平台多媒体应用开发的Qt与大华摄像机SDK、ffmpeg解码整合工程包适合具备C与Qt基础、希望打通GUI、设备取流和视频解码链路的中级开发者参考。压缩包共2000个文件约156.52MB包含大量C/H源码、汇编与头文件、o目标文件、so/a库、pc配置、mak与makefile构建脚本以及texi文档和多格式测试样本覆盖Qt5.9.1工程构建、SDK依赖与ffmpeg编译产物等模块。已有1036人学习。资源以DH_Camera项目为线索呈现Qt界面工程、大华摄像机SDK接入及ffmpeg解码显示相关代码与依赖组织便于读者理解设备控制、实时视频流处理、YUV/RGB数据呈现和工程依赖管理中的关键环节也可作为类似摄像机与多媒体项目的整合参考。1. 凌晨两点工控机上的预览窗口全是黑的摄像机在线网线绿灯在闪ping通可 Linux 桌面上那个 Qt 窗口就是一片黑。这种现场问题我遇到过太多次最后定位下来八成不是摄像机坏了而是「取流 → 解封装 → 解码 → 上屏」这条链路上某一环悄悄断了。这套工程干的就是把链路接通Qt负责界面、线程调度和图像呈现摄像机厂商的IPC SDK负责登录设备、拉实时码流ffmpeg负责把 SDK 吐出来的私有封装数据解复用、解码成图像。整套东西跑在 Linux 上目标机器往往是工控机或者国产化桌面不是开发机。适合谁看做安防上位机、工业视觉预览、多路视频墙的开发者手上已经有一份能跑的 demo但一到多路、一到长时间运行就开始黑屏、花屏、内存涨、退出崩溃的人。下面按「工程怎么搭 → 流怎么进来 → 帧怎么出来 → 坑在哪 → 怎么改」的顺序拆。2. 工程骨架sdk、ffmpeg、业务代码三层怎么切先把目录定下来后面所有问题都好查。这类项目最常见的失败姿势是SDK 的头文件和系统里的 ffmpeg 头文件混在一个 include 路径里编译期没事运行期找不到.so现场装机直接翻车。2.1 目录分层三块东西物理隔开我的习惯是彻底分开厂商 SDK 一个目录、业务源码一个目录、构建产物一个目录谁也别碰谁ipc_preview/ ├── CMakeLists.txt ├── sdk/ # 厂商 SDK整包丢进来不改一行 │ ├── include/ # 头文件 │ └── lib/ │ ├── libipc_sdk.so │ └── libipc_sdk.so.1 - libipc_sdk.so.1.2.3 ├── src/ │ ├── main.cpp │ ├── MainWindow.h/.cpp # 界面、分屏布局、按钮 │ ├── StreamWorker.h/.cpp # SDK 登录/起流/回调跑在子线程 │ ├── FrameQueue.h/.cpp # 回调线程 → 解码线程的字节队列 │ └── FfmpegDecoder.h/.cpp # ffmpeg 解复用 解码 转 RGB └── resources/ └── style.qssStreamWorker和FfmpegDecoder一定分开。我见过把 SDK 回调和avcodec_send_packet写进同一个函数的代码结果解码耗时把 SDK 的回调线程堵死SDK 内部缓冲区溢出表现就是「预览三秒后画面卡住日志里没有任何报错」。分层之后SDK 回调只管把字节塞进队列解码线程自己按节奏消费两边互不拖累。FrameQueue是这个设计的核心。它是个带互斥锁和条件变量的环形字节队列写端是 SDK 回调线程读端是 ffmpeg 的 IO 回调。容量要设上限我一般给 48 MB满了就丢最老的数据而不是阻塞——实时预览丢几帧没人看得出来阻塞住整条链路就是灾难。2.2 CMake 链接与运行时找库$ORIGIN必配编译能过、运行报error while loading shared libraries: libipc_sdk.so: cannot open shared object file这是现场部署第一大坑。系统里没有这个库你也不能保证目标机器上有 root 权限去改/etc/ld.so.conf。cmake_minimum_required(VERSION 3.16) project(ipc_preview LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 有 Q_OBJECT 就必须开 # 关键让程序优先从自己所在目录的 lib/ 找 .so装到哪都能跑 set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) set(CMAKE_INSTALL_RPATH $ORIGIN/lib;$ORIGIN) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) find_package(PkgConfig REQUIRED) pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libavutil libswscale) add_executable(ipc_preview src/main.cpp src/MainWindow.cpp src/StreamWorker.cpp src/FrameQueue.cpp src/FfmpegDecoder.cpp ) target_include_directories(ipc_preview PRIVATE ${CMAKE_SOURCE_DIR}/sdk/include ${FFMPEG_INCLUDE_DIRS} ) target_link_directories(ipc_preview PRIVATE ${CMAKE_SOURCE_DIR}/sdk/lib) target_link_libraries(ipc_preview PRIVATE Qt5::Widgets ${FFMPEG_LIBRARIES} ipc_sdk # 对应 libipc_sdk.so )$ORIGIN是 ELF 的动态链接器占位符表示「可执行文件自身所在目录」。写成$ORIGIN/lib之后只要把libipc_sdk.so拷到可执行文件旁边的lib/目录任何路径启动都能找到它。CMAKE_BUILD_WITH_INSTALL_RPATH TRUE这行容易被忽略默认情况下构建产物的 rpath 是构建目录路径只有make install之后才写成$ORIGIN。你要是不加这行直接从build/目录拿二进制去现场跑一样找不到库。参数说明pkg_check_modules(FFMPEG REQUIRED ...)会从系统 pkg-config 里取 ffmpeg 的头文件和链接参数如果目标机的 ffmpeg 是自编译装在/opt下需要额外给PKG_CONFIG_PATH/opt/ffmpeg/lib/pkgconfig这一点在交叉编译或者国产化发行版上尤其常见因为系统源自带的 ffmpeg 版本往往偏老。2.3 依赖体检上线前跑一遍 ldd程序在开发机上跑得好好的拷到工控机上就起不来九成是动态库缺失或者版本对不上。上线前把这三条命令跑一遍比在现场猜快得多# 1) 看所有依赖是否都能解析缺的会显示 not found ldd ./ipc_preview | grep -E not found| # 2) 确认 ffmpeg 的实际版本4.x 和 5.x 在解码接口上有差异 pkg-config --modversion libavcodec # 3) 确认 SDK 的 ABI 架构跟主程序一致x86_64 / aarch64 别搞混 file ./ipc_preview sdk/lib/libipc_sdk.so第一条里出现not found就别急着上现场了。第二条用来判断代码该用哪套解码接口avcodec_send_packet/avcodec_receive_frame这套 send/receive 模型在新版本里是唯一推荐路径老版本里还能见到avcodec_decode_video2两套写法不能混用。第三条是交叉编译时的血泪经验——给 ARM 工控机编译结果 SDK 是 x86 的编译能过运行直接Exec format error。提示如果目标机器上的 SDK 只有.so没有.so.1这类版本软链target_link_libraries里不要写绝对路径写库名ipc_sdk靠target_link_directories找否则换一台机器路径一变就要重新改 CMake。3. 取流链路从设备登录到回调拿到裸码流链路的前半段全在厂商 SDK 里代码不多但顺序极重要。句柄的生命周期管不好退出崩溃、重连失败、句柄泄漏全从这里来。3.1 登录与起流句柄必须成对出现SDK 的调用顺序基本固定初始化 → 登录 → 起流 → 停流 → 登出 → 反初始化。每一步都可能失败失败后要把已经成功的步骤按逆序回退。// 函数名按你手上 SDK 的头文件替换逻辑一致即可 bool StreamWorker::start(const StreamConfig cfg) { // 1) 全局初始化进程内只需一次有些 SDK 内部按引用计数重复调用无害 if (!IPC_Init(nullptr)) { emit errorOccurred(QStringLiteral(SDK 初始化失败)); return false; } // 2) 登录设备拿长连接句柄 IPC_DEVICE_INFO dev{}; dev.nPort static_castIPC_WORD(cfg.port); strncpy(dev.szIP, cfg.ip.toUtf8().constData(), sizeof(dev.szIP) - 1); strncpy(dev.szUser, cfg.user.toUtf8().constData(), sizeof(dev.szUser) - 1); strncpy(dev.szPwd, cfg.pwd.toUtf8().constData(), sizeof(dev.szPwd) - 1); m_loginId IPC_Login(dev, /*timeoutMs*/5000, /*retry*/3); if (m_loginId 0) { emit errorOccurred(QStringLiteral(登录失败错误码 %1).arg(IPC_GetLastError())); IPC_Cleanup(); return false; } // 3) 起实时预览channel 通道号0 表示主码流 m_playId IPC_StartRealPlay(m_loginId, cfg.channel, /*streamType*/0, StreamWorker::onRealData, this); if (m_playId 0) { emit errorOccurred(QStringLiteral(起流失败错误码 %1).arg(IPC_GetLastError())); IPC_Logout(m_loginId); // 起流失败也要回退登录 m_loginId 0; IPC_Cleanup(); return false; } return true; }参数说明timeoutMs是 TCP 连接超时内网给 30005000 就够跨网段或者无线链路给到 8000 更稳妥retry是重试次数别设太大否则界面会长时间卡在「连接中」而没有任何反馈。cfg.channel是通道号单设备从 0 开始接 NVR 时按 IPC 挂载顺序递增这里一定要让用户可配写死是二次开发的死穴。this作为 user 参数传进回调是整个设计里最需要小心的地方SDK 会把指针原样回传如果对象先析构了、SDK 还在回调就是标准的野指针访问。所以停流必须在对象析构函数的第一行执行而且要等回调真正停下来后面避坑章节细讲。3.2 回调线程只做两件事拷贝和入队SDK 的数据回调跑在自己的线程里这个线程和 Qt 主线程没有任何关系。在里面做解码、做 UI 更新、做日志刷盘轻则卡顿重则死锁。// 这个函数运行在 SDK 内部线程不是 Qt 主线程 void StreamWorker::onRealData(IPC_LONG playId, IPC_DWORD dataType, unsigned char *buf, IPC_DWORD size, void *user) { auto *self static_castStreamWorker *(user); if (!self || !buf || size 0) { return; } // dataType 常见取值 // 0 私有封装数据PS 流或带 SPS/PPS 的裸流—— 工程里只接这个 // 1 帧数据裸 H.264/H.265 NAL缺 SPS/PPS见 3.3 // 2 YUV 原始数据 3 PCM 音频 if (dataType ! 0) { return; } // push 内部加锁 条件变量唤醒解码线程队列满时丢最老的帧而不是阻塞 self-m_queue.push(buf, static_castsize_t(size)); }这里的两个参数值得展开。buf是 SDK 内部缓冲区的指针回调返回后随时可能被复用所以push里必须做深拷贝不能只存指针。size是一包数据的字节数单包通常几 KB 到几十 KBPS 流的 I 帧可能超过 100 KB队列单槽容量别按 4 KB 估。playId在多路场景下用来区分是哪一路的流。如果你们是多通道共用一个回调有些 SDK 支持一定要在回调开头按playId分发到对应的队列否则多路数据混进同一个队列解码器会疯掉——表现就是画面剧烈撕裂、时间戳乱跳。3.3 码流类型判断为什么必须接私有流这是新手最容易踩的坑也是「回调里有数据但画面全黑」的头号原因。摄像机 SDK 一般能给你三种数据私有封装流PS/TS 之类、裸 NAL 帧数据、解码后的 YUV。三种数据的处理路径完全不同dataType数据形态后续处理适用场景0 私有封装PS 流含 SPS/PPS 和完整封装头直接喂 ffmpeg 的avformat解复用本工程采用1 帧数据裸 NAL 单元通常不含 SPS/PPS需要自己拼 extradata 或周期性缓存 SPS/PPS低延迟场景2 YUV已解码的原始图像直接 sws_scale 转 RGB不需要 ffmpeg 解码极低路数、CPU 富余接 dataType0 的好处是省事ffmpeg 自己会从 PS 流里解析出 H.264 的extradata也就是 SPS/PPS解码器初始化时能拿到完整参数集第一帧就能正常解出来。接 dataType1 就麻烦了。裸 NAL 里通常不带 SPS/PPS解码器没有参数集avcodec_send_packet会一直返回错误画面永远黑着。要用这条路你得自己在回调里识别 NAL 类型H.264 是nal_unit_type 7/8把 SPS/PPS 缓存下来塞进AVCodecContext::extradata并且处理「中途设备重连、参数集变化」的情况。为了省那点解复用开销不值得。注意不同厂商对 dataType 的编号定义可能不一样有的把私有流标成 0有的标成 4。拿到 SDK 后第一件事是写个 demo把回调里的dataType和size全打出来跑十分钟看清楚到底给了什么再决定接哪个分支。4. ffmpeg 解码模块内存喂流到 QImage 上屏后半段是纯 ffmpeg 的活。难点不在 API 本身而在「数据源不是文件也不是网络 URL而是一个随时可能断流的回调队列」。这块用自定义 AVIO 解决。4.1 内存 AVIO把回调队列伪装成一个文件avformat_open_input默认只认文件名或 URL。数据在我们自己的队列里就得用avio_alloc_context造一个自定义 IO 上下文让 ffmpeg 以为它在读一个文件。bool FfmpegDecoder::open() { // 1) AVIO 的内部缓冲区由 ffmpeg 读写但内存由我们分配 m_avioBuf static_castuint8_t *(av_malloc(kIoBufferSize)); // 建议 32KB if (!m_avioBuf) return false; // 2) readPacket 是我们的静态回调opaque 原样回传这里传队列指针 m_avioCtx avio_alloc_context(m_avioBuf, kIoBufferSize, /*write_flag*/0, m_queue, FfmpegDecoder::readPacket, nullptr, // 不写 nullptr); // 不支持 seek if (!m_avioCtx) return false; // 3) 把 AVIO 挂到 format context 上必须打 CUSTOM_IO 标记 m_fmtCtx avformat_alloc_context(); m_fmtCtx-pb m_avioCtx; m_fmtCtx-flags | AVFMT_FLAG_CUSTOM_IO; // 4) PS 流头部信息少探测窗口给小了会认不出格式 m_fmtCtx-probesize 512 * 1024; if (avformat_open_input(m_fmtCtx, nullptr, nullptr, nullptr) 0) { return false; } if (avformat_find_stream_info(m_fmtCtx, nullptr) 0) { return false; } m_videoIdx av_find_best_stream(m_fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); return m_videoIdx 0; }AVFMT_FLAG_CUSTOM_IO这个标记不能漏。不打的话avformat_close_input时会尝试去关闭你并没有打开的文件行为未定义运气不好就是 double free。probesize调到 512 KB 是为了对抗「探测失败」PS 流的封装头很紧凑默认探测窗口偏小时ffmpeg 可能判断不出这是 MPEG-PS直接返回Invalid data found when processing input。这条错误在现场日志里出现先加 probesize别急着怀疑码流。读回调本身很短关键是阻塞语义要对int FfmpegDecoder::readPacket(void *opaque, uint8_t *buf, int bufSize) { auto *q static_castFrameQueue *(opaque); // 阻塞直到凑到数据或者解码器被要求停止 const int n q-readBlocking(buf, static_castsize_t(bufSize)); if (n 0) { return AVERROR_EOF; // 只有主动 stop() 时才会走到这里 } return n; // 返回实际读取字节数 }返回值语义正数是读到的字节数0 会被 ffmpeg 当成文件末尾av_read_frame直接返回 EOF解码循环退出负数是错误。所以绝对不能在没有数据时返回 0否则流刚起就被判定结束。正确做法是阻塞在条件变量上等只有用户点「停止」时才让readBlocking返回 -1转成AVERROR_EOF优雅收尾。bufSize是 ffmpeg 传进来的目标缓冲区大小readBlocking里要按这个上限做截断绝不能写得比它多。4.2 解码循环send 和 receive 的节奏不能乱拿到AVFormatContext之后就是标准解码流程但 send/receive 的配对关系一定要理清一个 packet 送进去可能吐出 0 帧、1 帧或多帧。void FfmpegDecoder::decodeLoop() { AVPacket *pkt av_packet_alloc(); AVFrame *frm av_frame_alloc(); m_sws sws_getContext(m_codecCtx-width, m_codecCtx-height, m_codecCtx-pix_fmt, m_codecCtx-width, m_codecCtx-height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); if (!m_sws) { /* 清理后返回 */ } const int w m_codecCtx-width; const int h m_codecCtx-height; std::vectoruint8_t rgb(static_castsize_t(w) * h * 3); while (!m_stop.load(std::memory_order_relaxed)) { // 1) 从自定义 IO 里读一个 packet返回负值说明流结束或已停止 if (av_read_frame(m_fmtCtx, pkt) 0) { break; } if (pkt-stream_index ! m_videoIdx) { av_packet_unref(pkt); // 音频/数据流直接丢弃但必须 unref continue; } // 2) 送包失败通常是非关键帧缺参考帧丢掉继续不要 break if (avcodec_send_packet(m_codecCtx, pkt) 0) { av_packet_unref(pkt); continue; } av_packet_unref(pkt); // 送完立刻释放pkt 可复用 // 3) 一个包可能吐出多帧用 while 全部取干净 while (avcodec_receive_frame(m_codecCtx, frm) 0) { uint8_t *dst[4] { rgb.data(), nullptr, nullptr, nullptr }; int dstLs[4] { w * 3, 0, 0, 0 }; sws_scale(m_sws, frm-data, frm-linesize, 0, h, dst, dstLs); // QImage 这里是浅包装rgb 缓冲下一帧就被覆盖必须 copy() QImage img(rgb.data(), w, h, w * 3, QImage::Format_RGB888); emit frameReady(img.copy()); av_frame_unref(frm); } } av_frame_free(frm); av_packet_free(pkt); }几个参数和返回值的含义必须说清楚。avcodec_receive_frame返回AVERROR(EAGAIN)表示「当前没有帧可出继续送包」返回AVERROR_EOF表示解码器彻底结束返回 0 才是拿到一帧。上面用 0做循环条件把 EAGAIN 当退出条件是安全写法如果你在循环里对 EAGAIN 也做错误处理就会在每次正常送包时打一堆假错误日志。avcodec_send_packet失败不 break 是有意为之网络抖动丢一个 P 帧后面的 P 帧都会因为缺参考帧报错但如果直接跳出循环整路流就断了。丢包继续等下一个 I 帧回来自动恢复。m_stop用std::atomicbool不用volatile。有些老代码用bool加 mutex在回调线程里频繁加锁解锁CPU 会莫名其妙飙高。4.3 像素转换与上屏QImage 的生命周期陷阱sws_scale那三行里藏着一个非常经典的坑QImage用外部缓冲区构造时不接管内存所有权它只是引用。rgb是循环外复用的 vector第二帧一来第一帧的像素数据就被覆盖了。如果你在信号槽里跨线程传这个 QImage接收端拿到的极可能是撕裂的画面。所以必须img.copy()它做一次深拷贝把像素数据搬进 QImage 自己的内存。跨线程传递还有一步QImage要注册成元类型不然队列连接会失败槽函数根本不执行但没有任何报错。// 放在 main() 里 QApplication 构造之后 qRegisterMetaTypeQImage(QImage);接收端用Qt::QueuedConnection跨线程默认就是队列连接槽里只做m_label-setPixmap(QPixmap::fromImage(img))或者update()触发paintEvent。如果分辨率是 1920×1080QPixmap::fromImage每帧都要做一次格式转换四路以上就开始吃 CPU。优化思路是在sws_scale里直接输出AV_PIX_FMT_BGRA配合QImage::Format_RGB32让 QImage 和 pixmap 走同一条快路径省掉一次转换。参数改两处sws_getContext的目标格式换成AV_PIX_FMT_BGRA缓冲区大小从w*h*3变成w*h*4dstLs从w*3变成w*4。5. 避坑与排查黑屏、花屏、内存涨、退出崩这一章是拿事故换来的每条都按「现象 → 原因 → 解决」写遇到对应症状直接对号入座。5.1 黑屏花屏类回调有数据画面就是不出来现象日志里 SDK 回调一直在打印size也不为 0但窗口始终黑的或者闪过一两帧马赛克就没了。原因接了 dataType1 的裸 NAL 数据。裸流里通常没有 SPS/PPSavformat_find_stream_info找不到参数集avcodec_send_packet一直返回AVERROR_INVALIDDATA解码器吐不出帧。解决改接 dataType0 的私有封装流让 ffmpeg 从 PS 头里自己解析参数集。如果非要用裸流必须在AVCodecContext里手动填extradata// SPS/PPS 需要从码流里识别 NAL type 7/8 后缓存下来按 AV_INPUT_BUFFER_PADDING_SIZE 对齐 m_codecCtx-extradata static_castuint8_t *(av_malloc(spsPps.size() AV_INPUT_BUFFER_PADDING_SIZE)); memcpy(m_codecCtx-extradata, spsPps.data(), spsPps.size()); memset(m_codecCtx-extradata spsPps.size(), 0, AV_INPUT_BUFFER_PADDING_SIZE); m_codecCtx-extradata_size static_castint(spsPps.size());现象画面能出但底部一条绿边或者画面被拉成斜的。原因sws_scale的目标linesize写成了w*3但实际缓冲区按 4 字节对齐分配越界写或者读错行。另外sws_getContext的源宽高用了codecCtx的尺寸而某些流有裁剪信息应该用frame-width/height。解决linesize严格等于目标格式的实际每行字节数RGB24 是w*3BGRA 是w*4缓冲区按linesize * h申请不要图省事混用。5.2 卡顿与内存类跑十分钟就涨到几个 G现象单路预览没问题开四路跑半小时内存从 300 MB 涨到 3 GB最后 OOM 被杀。原因av_read_frame成功之后忘记av_packet_unref。这个函数内部会引用计数不 unref 的话每包都泄漏avcodec_receive_frame拿到帧后不av_frame_unref同理。更隐蔽的是avformat_open_input失败时没释放m_avioCtx和m_avioBuf。解决每条路径都保证 unref失败分支也一样。open 失败时的清理要写全void FfmpegDecoder::close() { if (m_codecCtx) avcodec_free_context(m_codecCtx); // 内部会释放 extradata if (m_fmtCtx) avformat_close_input(m_fmtCtx); if (m_avioCtx) { // 必须在 close_input 之后 av_freep(m_avioCtx-buffer); avio_context_free(m_avioCtx); } }顺序很重要avformat_close_input内部会碰m_fmtCtx-pb所以 AVIO 必须在它之后释放。反过来的话就是 use-after-free崩溃位置还会飘在完全无关的代码里查起来非常费劲。现象界面一卡一卡的鼠标拖动窗口时预览直接冻住。原因在 SDK 回调里直接调了emit到主线程的槽而那个槽里做了QPixmap::fromImage加update()。回调线程被主线程的重绘拖着走SDK 缓冲区堆满。解决回调只入队解码在独立线程上屏用队列连接并且做丢帧——主线程忙不过来时直接丢弃旧帧而不是排队。5.3 退出与部署类点关闭按钮就段错误现象关窗口时进程直接Segmentation faultgdb 栈里一半是 SDK 内部函数。原因析构顺序错了。窗口或 worker 对象先被销毁SDK 的取流回调还在跑回调里的user指针已经是野指针。解决停流必须放在析构第一行并且要等回调真正停止。很多 SDK 的StopRealPlay是同步返回的但内部回调可能还在队列里稳妥做法是停流之后加一个短延时100200 ms再登出StreamWorker::~StreamWorker() { m_stop.store(true); // 先让解码线程准备退出 m_queue.wakeAll(); // 唤醒阻塞在 readBlocking 的线程 if (m_playId) { IPC_StopRealPlay(m_playId); // 先停流 m_playId 0; } if (m_loginId) { IPC_Logout(m_loginId); // 再登出 m_loginId 0; } IPC_Cleanup(); // 最后反初始化 m_decodeThread.join(); // 线程 join 必须在句柄释放之后、成员析构之前 }IPC_StopRealPlay之后如果立刻IPC_CleanupSDK 内部可能正处在回调函数中会崩在第三方库里栈上什么都看不出来。延时 100 ms 是我试过代价最小、最稳的做法。现象开发机好好的拷到现场机器启动就黑屏日志里avformat_open_input返回Invalid data。原因现场那台机器上装着系统源里的老版本 ffmpegprobesize默认值更小同样的 PS 流探测不出来。解决把probesize显式设成 512 KB 甚至 1 MB别依赖默认值。同时用ldd确认实际加载的libavformat.so是从哪来的避免出现「以为用的是自己编的实际加载的是系统旧版」。提示现场排查黑屏第一步永远是打日志确认「回调有没有数据 → 队列里有没有堆积 →av_read_frame返回什么 →avcodec_receive_frame返回什么」。这四个点定位下来问题基本跑不掉。6. 二次开发从单路 demo 改成能交付的工具这套骨架直接当产品用是不够的但改造成本很低。最常见的两个方向是多路分屏和抓图录像。6.1 多路分屏从单实例改成 N 实例单路代码改成四路不要试图在一个StreamWorker里塞四个句柄。正确做法是每个通道一个StreamWorker加一个FfmpegDecoder各自跑在自己的QThread上主线程只负责布局和渲染。四路 1080p 在 i5 上 CPU 大约 60%80%线程数上去之后解码器的内部线程数要压下来否则 4 路 × 8 线程 32 个解码线程上下文切换的开销比解码本身还大// 每路解码器线程数按总核数 / 路数分配别用默认值 m_codecCtx-thread_count std::max(1, QThread::idealThreadCount() / channelCount); m_codecCtx-thread_type FF_THREAD_SLICE; // 切片并行延迟比帧级并行低FF_THREAD_SLICE对实时预览更友好帧级并行FF_THREAD_FRAME虽然吞吐高但会引入额外缓冲延迟。6.2 加抓图和直存录像抓图最简单在解码循环里拿到img之后如果抓图标志位被置上img.save(path)就完事注意别在 UI 线程做。录像有个省事的做法不要重新编码直接把 SDK 的原始码流写文件。SDK 大多提供「原始码流存盘」接口或者你自己用 ffmpeg 的avformat_alloc_output_context2建一个 mp4 muxer把AVPacket原样av_interleaved_write_frame写进去。这样 CPU 几乎零开销缺点是文件只能被标准播放器播做不了二次转码。// 只写视频流的最小 muxer 骨架 AVFormatContext *ofmt nullptr; avformat_alloc_output_context2(ofmt, nullptr, mp4, path); AVStream *st avformat_new_stream(ofmt, nullptr); avcodec_parameters_from_context(st-codecpar, m_codecCtx); // 直接复用解码器参数集 avio_open(ofmt-pb, path, AVIO_FLAG_WRITE); avformat_write_header(ofmt, nullptr); // 之后每拿到一个视频 packet 就 av_interleaved_write_frame(ofmt, pkt)avcodec_parameters_from_context这行把 SPS/PPS 一并写进 mp4 的 avcC box播放器才认。漏了这步录出来的文件在 VLC 里能播在别的播放器里黑屏。6.3 验证清单与一个习惯改完之后别急着交付按这几条过一遍连续跑 8 小时看内存曲线是否平稳拔网线 30 秒再插回去看能否自动恢复不能恢复就在av_read_frame返回负值时重建整个解码链路别指望原地复活四路同时开关窗口二十次看句柄数是否回到初始值把 CPU 压到 90% 看会不会丢帧到卡死。从那以后我每次改完取流或解码相关代码都会强制走一遍「拔网线 → 关窗口 → 重开」这三步因为真正让项目翻车的从来不是正常流程而是这些边角状态。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站