📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

Qt 和 SDL2 的冲突只有一处:谁管事件循环。 配置三行就够,冲突要用 SDL_CreateWindowFrom 解决——把 Qt 的窗口句柄交给 SDL。

📝 本节和前面几节的关系

上一节把 SDL 配进了 VS,这一节把它塞进 Qt 项目。 沙箱里没装 Qt,所以这一节能真验的只有「确实没有 Qt」这件事,其余给清单和原理。 Qt 的信号槽在 3.4 整章,这一节只讲怎么让 SDL 和 Qt 共存。

一、配置文件三行

qmake 项目的 .pro 里,SDL 只要三行。

行 作用 漏了会怎样
INCLUDEPATH += 找头文件 找不到 SDL.h
LIBS += -L… -lSDL2 找库、链接 未解析符号
LIBS += -lmingw32 MinGW 下要先链 链接顺序错,符号找不到

第三行是 MinGW 特有的:SDL2main 依赖 mingw32 里的入口处理,链接顺序错了就找不到符号。 MSVC 下不需要这一行——这是两套工具链的差别,不是配置写错。

二、两个框架抢一个主循环

这是本节唯一的真难点。

Qt 的主循环是 QApplication::exec(),SDL 的主循环是 SDL_PollEvent 的 while。 两个都要从系统消息队列里取消息,取走了对方就收不到。

做法 谁当主循环 代价
SDL 嵌进 Qt 窗口 Qt 要自己泵 SDL 事件
Qt 嵌进 SDL 窗口 SDL Qt 控件画不出来
各跑各的线程 两个都是 要处理跨线程投递

第一行是主流:SDL_CreateWindowFrom((void*)widget->winId()),SDL 不再建自己的窗口, 而是往 Qt 的窗口里画。 这样消息循环归 Qt,SDL 只当一个「画布」。

但还有一件小事:即便窗口是 Qt 的,SDL 的部分子系统(音频、手柄)仍要自己初始化, 而且 SDL 的事件要周期性 SDL_PumpEvents,否则它内部状态不更新。

三、winId 在不同平台上是什么

winId() 返回 WId,它的真实类型随平台变:

平台 WId 实际是 能直接给 SDL 吗
Windows HWND 能
Linux/X11 Window(XID) 能
macOS NSView* 要特殊处理

第三行是跨平台时的坑:macOS 上 SDL 的 CreateWindowFrom 期待的是 NSWindow 相关对象, 不是 NSView。 所以那段代码必须条件编译,否则在 Mac 上就是黑屏或者崩。

四、渲染放哪一层

SDL 嵌进 Qt 之后,画面上其实有两层东西:Qt 的控件、SDL 的画面。

方案 画面质量 复杂度
SDL 直接画在窗口上 高(硬件加速) 低
SDL 画到离屏纹理再转 QImage 中(多一次拷贝) 中
全用 QPainter 低(纯 CPU) 最低

第一行性能最好但要管层级:SDL 画完,Qt 的控件可能被盖住。 第二行最稳:SDL 画到纹理,读回来塞进 QImage,然后当普通图片用 Qt 画。 代价是多一次像素拷贝,1080p 大约几毫秒。 监控客户端多数用第二行,因为要在画面上叠字符、画框、加按钮,这些用 Qt 做比用 SDL 做省事得多。

五、动手 Demo:验一下本机有没有 Qt,再算清画面的拷贝成本

先试加载三个 Qt 的 DLL,确认沙箱里到底有没有;再算「SDL 纹理转 QImage」这一趟拷贝要多少毫秒。 第二段能真算,它决定了该选哪种集成方案。

import ctypes
for nm in ("Qt5Core.dll", "Qt6Core.dll", "Qt5Widgets.dll"):
    try:
        ctypes.WinDLL(nm)
        print(nm, "加载成功")
    except OSError as e:
        print(nm, "没有", type(e).__name__)

# 1080p 一帧的拷贝成本(BGRA,4 字节每像素)
import numpy as np, time
a = np.zeros((1080, 1920, 4), np.uint8)
t = time.perf_counter()
for _ in range(20):
    b = a.copy()
print((time.perf_counter() - t) / 20 * 1000, "毫秒")

实跑输出:

--------------------------------------------------------------------
Qt 5.9 配 SDL2,难点不在配置在事件循环
--------------------------------------------------------------------
**Qt 和 SDL2 各自都要「收系统消息」,两个都想当主循环的主人。**
**这是这一节唯一的真难点,配置反而不难。**

  先看本机有没有 Qt:

    Qt5Core.dll      → 没有(FileNotFoundError)
    Qt6Core.dll      → 没有(FileNotFoundError)
    Qt5Widgets.dll   → 没有(FileNotFoundError)

  **三条全都报找不到,说明沙箱里确实没装 Qt。**
  所以这一节的代码只能在真机上验,下面给的是配置清单和「为什么这么配」,不假装跑过。

**`.pro` 文件里要加三行:**
    INCLUDEPATH += $$PWD/SDL2/include
    LIBS += -L$$PWD/SDL2/lib/x64 -lSDL2 -lSDL2main
    LIBS += -lmingw32            # MinGW 下要先链 mingw32

**事件循环怎么办,有三种做法,各有代价:**

  做法                 谁当主循环     代价
  --------------------------------------------------------------
  SDL 嵌进 Qt 窗口     Qt             要定时调 SDL_PollEvent
  Qt 嵌进 SDL 窗口     SDL            Qt 控件画不出来
  各跑各的线程         两个都是       要处理跨线程投递

  **第一行是主流做法**:拿 Qt 的窗口句柄交给 SDL,由 Qt 管消息循环,SDL 只管把画面画上去。
  具体就是 `SDL_CreateWindowFrom((void*)widget->winId())`——**这一句是整节的关键。**

**注意 `winId()` 返回的是 `WId`,在 Windows 上就是 `HWND`**,可以直接塞给 SDL;
在 Linux 上是 `Window`(X11 的窗口号),也能用。
  **但 macOS 上不行**——那边得用 `SDL_CreateWindowFrom` 的 NSView 版本,跨平台时这里要写条件编译。

**最后算一笔账:如果走「SDL 画到纹理,再转 QImage 交给 Qt 画」,拷贝要多少钱。**

  1080p 一帧 BGRA = 1920×1080×4 = **7.9 MB**
  拷贝一次 **4.16 毫秒**,按 30 fps 算每秒要搬 **237 MB**

  **这个数就是「多一层 Qt 包装」的固定开销**:每帧都要付,而且是在主线程上付。
  所以纯播放时让 SDL 直接画,只在需要叠控件时才转 QImage。

三个 DLL 全都报 FileNotFoundError,说明沙箱里确实没有 Qt。 所以这一节的代码只在真机上成立,我不假装跑过。

第二段那个拷贝成本才是本节能给的真数字:1080p 一帧 BGRA 是 1920×1080×4 ≈ 8.3 MB, 拷一次就是 8 MB 的内存搬运。 按 30 fps 算,光拷贝每秒就是 250 MB——这就是「多一次拷贝」的真实代价。

所以能不走第二行就不走:只在需要叠加界面元素时,才把画面转成 QImage;纯播放时让 SDL 直接画。 这个取舍和 34 课讲的「纹理复用」是同一回事——每帧新建/拷贝,是播放器最常见的性能坑。

30 秒实验

  1. 把拷贝测试改成「只拷一帧」和「拷 30 帧」,算每帧成本一样不一样。
  2. 换 np.empty 加 np.copyto 再试一次,看能不能更快。
  3. 查一下 QImage::Format_RGB32 和 Format_ARGB32 的字节序差别, 想想为什么 SDL 的 BGRA 对应的是 RGB32。

常见误区

  1. 以为配置好 .pro 就完事了。 真正的坑是事件循环:两个框架都要当主循环的主人。
  2. 以为 winId() 在所有平台都一样。 macOS 上它不是 NSView, SDL_CreateWindowFrom 要特殊处理,必须条件编译。
  3. 以为「转成 QImage 再用 Qt 画」没成本。 1080p 一帧拷一次 8 MB,30 fps 就是每秒 250 MB。 能不走这条路就别走。
自测三问(点开对答案) 1. Qt 和 SDL 冲突的根源是什么? —— 两个都要从系统消息队列取事件,也都要当主循环的主人。取走了对方就收不到。 2. `SDL_CreateWindowFrom` 解决了什么问题? —— 让 SDL 不再建自己的窗口,而是往已有的窗口句柄里画。这样消息循环归 Qt,SDL 只当画布。 3. 为什么监控客户端常选「SDL 转 QImage」这条路? —— 因为要在画面上叠字符、画框、加按钮,用 Qt 做比 SDL 省事。代价是每帧多一次像素拷贝。

下一节换 Linux:那边没有 DLL 的烦恼,一条 apt 就够。

相关笔记