🎬 第二章 VLC播放器及二次开发应用
📄 一句话概括

Qt 版的 set_hwnd 与 MFC 版一模一样,只是句柄从 winId() 来。 真正的差别在「跨线程回界面」:Qt 用信号槽排队,不能直接碰控件。

📝 本节和前面几节的关系

前两节是 Windows 原生路线,这一节换跨平台的 Qt。 这一节也是本章的收尾。三种宿主(MFC、Duilib、Qt)里,Qt 的写法最接近「现代客户端」, 也最能说明「跨线程」这件事。 Qt 自己的多媒体模块对比在第四章,那时会看到「不用 LibVLC 也能播」。

一、winId 与 hwnd 是同一件事

Qt 的每个 QWidget 在 Windows 上都有一个原生窗口句柄,用 winId() 取。 所以 set_hwnd 那一句与 MFC 版完全一样。

对比项 MFC Qt
取句柄 GetSafeHwnd() widget->winId()
交给谁 set_hwnd(mp, hwnd) set_hwnd(mp, (void*)winId)
消息泵 CWnd 的消息循环 QApplication 的事件循环
跨线程回界面 PostMessage 信号槽排队投递

winId() 是 WId 类型,在 Windows 上就是 HWND,但在别的平台上是别的东西(X11 的 Window、 macOS 的 NSView 指针)。 所以这里要显式转成 void*——LibVLC 的接口签名就是这么定的:它只认一个「平台相关的句柄」。

二、嵌入的代码

注意 winId() 会在第一次调用时「真的创建原生窗口」, 所以它必须在窗口已经 show() 之后调,否则可能拿到 0。

void Player::play(const QString& path)
{
    m_inst = libvlc_new(0, NULL);
    m_mp   = libvlc_media_player_new(m_inst);

    /* 关键:把 QWidget 的原生句柄交给 VLC */
    libvlc_media_player_set_hwnd(m_mp,
                                (void*)ui->videoWidget->winId());

    libvlc_media_t* m =
        libvlc_media_new_path(m_inst, path.toUtf8().constData());
    libvlc_media_player_set_media(m_mp, m);
    libvlc_media_player_play(m_mp);
    libvlc_media_release(m);
}

path.toUtf8().constData() 这一句容易出问题:toUtf8() 返回一个临时对象, constData() 拿到的是它的内部指针,如果写成两行、临时对象先析构,指针就悬空了。 一行写完才安全。 这一点在 Qt 里是通用规则:临时对象的 constData() 不能存下来。

三、跨线程:Qt 的强项

LibVLC 的事件回调在它自己的线程里跑,而 Qt 的控件只能在界面线程里碰。 所以必须跨线程传递。

Qt 的解法是信号槽的「排队连接」:信号发出时把参数拷贝一份、放进事件队列, 等界面线程回到事件循环时再执行槽函数。

这带来一个重要结论:跨线程的信号参数必须「可拷贝」。 传指针不行(对面可能已经释放)、传引用不行(跨线程无意义), 只能传值或 Qt 的隐式共享类(QString、QImage 这些)。

demo 里算过延迟:60 Hz 下一轮事件循环是 16.7 毫秒, 所以一个投递过去的通知最快也要等这么久才被执行。 如果每帧都投递一次、而槽函数里又做了重活,队列会越积越长。

四、只传「有新数据了」

这是 Qt 播放器里最重要的一条工程经验:

错误做法:解码线程每解出一帧,就发一个信号把整帧图像传过去。 问题:界面处理不过来,队列积压,延迟越来越大。

正确做法:解码线程把最新一帧放进一个「只保留最新」的变量, 发一个「有新帧了」的信号(不带数据),界面收到后自己去取最新那帧。 这样队列永远不积压——旧帧直接被覆盖。

这个模式有个名字:单槽缓冲(latest-only buffer)。 它在「数据产生快、消费慢」的所有场景里都适用:摄像头预览、传感器读数、实时曲线。

这条规则与 LibVLC 的事件回调、MFC 的 PostMessage 完全一致: 跨线程只传「有新数据了」,不传数据本身。 三种框架,同一句话。

五、要不要用 Qt Multimedia

Qt 自带 QMediaPlayer,那为什么还要嵌 LibVLC?

对比项 QMediaPlayer LibVLC
格式支持 依赖系统后端 自带几百种
部署体积 几乎为零 两三百 MB
低延迟流 较弱 强
自定义渲染 受限 能拿帧自己画
跨平台一致性 各平台后端不同 一套行为

判断标准很简单:放普通 MP4/MP3,用 QMediaPlayer——体积小、跟系统集成好; 要放监控流、要低延迟、要格式全覆盖,用 LibVLC。

第四章会专门讲 QMediaPlayer,那时候你会看到它有多简单(三行代码就能播), 也会看到它的边界在哪儿。

六、动手 Demo:Qt 的 winId 与 MFC 的 hwnd 是同一件事

这段把两种框架的五项差异列出来,再算「排队投递」的延迟。 最后那张表的「最多等几轮」全是 1——这就是排队的下限。

TH([u"对比项", u"MFC", u"Qt"], [0, 0, 0])
T([u"句柄从哪来", u"`GetSafeHwnd()`", u"`widget->winId()`"], [0, 0, 0])
T([u"交给谁", u"`set_hwnd(hwnd)`", u"`set_hwnd((void*)winId)`"],
  [0, 0, 0])
T([u"跨线程怎么回界面", u"`PostMessage`", u"信号槽排队投递"],
  [0, 0, 0])

实跑输出:

--------------------------------------------------------------------
Qt 的 winId 与 MFC 的 hwnd 是同一件事
--------------------------------------------------------------------
**两个框架看起来完全不同,交出去的东西却一模一样:一个窗口句柄。**
**差别在「谁负责消息循环」和「跨线程怎么传」。**

  对比项               MFC                  Qt
  -----------------------------------------------------------------------
  句柄从哪来           `GetSafeHwnd()`      `widget->winId()`
  交给谁               `set_hwnd(hwnd)`     `set_hwnd((void*)winId)`
  消息谁泵             `CWnd` 的消息泵      `QApplication` 的事件循环
  跨线程怎么回界面     `PostMessage`        信号槽排队投递
  重绘触发             `Invalidate`         `update()`

**「跨线程回界面」这一行是两者最像的地方**:
**都不能直接碰控件,都得「投递一个消息,让主线程去执行」。**

**排队投递的延迟可以算**:事件循环每轮处理完所有待办,所以一个投递的事件最快在「下一轮」执行。

  屏幕刷新     每帧毫秒     最多等几轮
  ----------------------------------------
  30 Hz        33.3         1
  60 Hz        16.7         1
  144 Hz       6.9          1

**60 Hz 下一轮是 16.7 毫秒**,听起来很短。
  **但每帧都投递一次、而槽函数里又做了重活,队列就会越积越长。**
  表现是「画面延迟越来越大,最后像卡住」。

**所以 Qt 版的播放器有一个固定套路**:
**解码线程只负责「把帧放进一个只保留最新一帧的变量」,再发一个信号通知界面来取。**
**这样队列永远不会积压——旧帧直接被丢弃。**
  这条规则与 VLC 的事件回调、MFC 的 PostMessage 完全一致:
  **跨线程只传「有新数据了」,不传数据本身。**

「句柄从哪来」这一行是形式差异,「跨线程怎么回界面」是本质相同。 两者都是「投递一个消息,让主线程去执行」。

延迟那张表里,30/60/144 Hz 的「最多等几轮」都是 1,意思是「最快下一轮就执行」。 但这是「最快」,不是「平均」—— 如果队列里已经有十个待办,就要等十轮。

所以真正的瓶颈不是「一轮多长」,是「队列有多长」。 这就是为什么「只传有新数据了」这个模式这么重要:它让队列长度恒为 1。

30 秒实验

  1. 想一想:winId() 为什么必须在 show() 之后调用?
  2. 如果把一帧 1080p 的图像(约 3 MB)每帧都通过信号传一次,60 fps 下带宽是多少?
  3. 判断一下:你的项目该用 QMediaPlayer 还是 LibVLC?

常见误区

  1. 在构造函数里调 winId()。 那时原生窗口可能还没创建,拿到的可能是 0。要等 show 之后。
  2. 把 toUtf8().constData() 存下来。 临时对象析构后指针悬空。
  3. 跨线程信号传整帧图像。 队列会积压,延迟越来越大。应该传「有新帧了」。
自测三问(点开对答案) 1. Qt 里 `set_hwnd` 的第二个参数怎么写? —— `(void*)ui->videoWidget->winId()`——把 WId 转成 void 指针。 2. 跨线程的信号参数为什么必须可拷贝? —— 因为排队连接会把参数拷贝一份放进事件队列,等界面线程有空再执行。 3. 什么情况下该用 QMediaPlayer 而不是 LibVLC? —— 只放普通格式、不想带几百 MB 依赖、要跟系统媒体栈集成时。

第二章到此为止。 VLC 这条路的特点是「全包了」——从解封装到显示都不用你管。 第三章开始换一条路:Qt 自己的多媒体栈,代码少得多,边界也明显得多。

相关笔记