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

LibVLC 是 VLC 的核心被剥成一个库,对外只留一套 C 接口。 它的对象只有三层:实例、播放器、媒体——每层的寿命不一样,这是它最容易用错的地方。

📝 本节和前面几节的关系

上一节看完模块体系,这一节看接口层。 LibVLC 的定位是「把整个 VLC 塞进你的程序」:你不需要懂解码、不需要懂容器,只要「给个地址、 给个窗口」它就播。这是它比 FFmpeg 好用的地方,也是它比 FFmpeg 笨重的地方。 要对照「用 FFmpeg 自己搭」的那条路, 看 🎬 SDK二次开发。

一、它到底是什么

VLC 安装目录里有两个库:libvlc.dll 与 libvlccore.dll。前者是壳,后者是核心。 你的程序只跟前者打交道。

所以「用 LibVLC」这句话的准确意思是: 「在你的进程里加载 VLC 的全部模块,用它的能力播一个流」。 它不是一个轻量解码库,它是整个 VLC。

这个定位带来两个直接后果: 你的安装包必须带上几百 MB 的插件目录,否则运行时什么都打不开。 它的能力上限就是 VLC 的能力上限——VLC 能放的它都能放,VLC 放不了的它也放不了。

对比一下另一条路:直接调 FFmpeg 的库,安装包只有几十 MB, 但你要自己写解封装循环、自己做同步、自己找地方显示。 LibVLC 省的是这几百行代码,付的是体积。

二、三层对象

LibVLC 的 API 看着几十个函数,其实只有三种对象在流转:

对象 创建 释放 寿命
instance libvlc_new libvlc_release 整个程序
media_player media_player_new media_player_release 一个窗口
media media_new_path/location media_release 一次播放

三层的关系是「实例带播放器,播放器带媒体」: 实例是引擎,一个进程里有几个都行,但通常只要一个。 播放器是一路输出,几路画面就要几个播放器。 媒体是内容描述,可以重复给不同播放器用。

最容易搞错的是「媒体」这一层:很多人以为「设了媒体就占住了文件」, 其实它可以同时被多个播放器引用——这就是「同一路流开两个窗看」的做法。

三、实例为什么重

libvlc_new 做的事比看上去多:读配置、扫插件目录、给每个模块打分、建日志系统、起事件循环。

所以它是「一次性的重活」:建一个实例按几 MB 内存与几十到几百毫秒启动时间来算。

demo 里算过一笔账:每播放器一实例,十六路就要七十 MB;共享一个实例,只要十 MB。 省下六十 MB。

但共享实例有三条必须知道的代价: 一个实例崩了,挂在上面的全部一起崩。 实例级配置(日志级别、音量策略)是全局的。 事件回调都在这一个实例的线程上飞,回调里做重活会拖慢所有播放器。

所以工程上的判断标准是: 同一个界面里的多路(监控九宫格、画中画)共享实例;不同业务、要互相隔离的,各自建实例。

四、引用计数与释放顺序

LibVLC 用引用计数管对象,所以「谁创建谁释放」不是唯一规则——加了引用就要减引用。

两个最常见的引用场景: media_player_new_from_media(m) 会给 m 加一次引用,调完这句你自己的 m 就可以释放了。 取当前正在播的媒体(media_player_get_media)也会加引用,用完必须释放,否则泄漏。

释放顺序是「后建先释放」:先释放播放器,再释放实例。 反过来会崩,因为播放器还挂在实例的链表上。

这一点在 C 里特别容易错:没有析构函数兜底,漏一次 release 就是内存泄漏,顺序错一次就是崩溃。 所以实际项目里都会用 RAII 包一层(C++ 里写个小的智能指针类), Python 的 PyAV 那种 with 语句就是同一件事。

五、事件是异步的

LibVLC 的播放是异步的:play() 立刻返回,真正的播放发生在它自己的线程里。 所以你不能「play 完就取画面」,只能等事件。

常用的事件有这几个:

事件 什么时候触发 典型用途
MediaPlayerPlaying 真正开始播 隐藏加载提示
MediaPlayerEndReached 播完了 播下一个
MediaPlayerEncounteredError 出错了 报错、换源
MediaPlayerTimeChanged 时间变了 更新进度条
MediaPlayerPositionChanged 位置变了 更新进度条

「时间变了」这个事件每秒会来好几次, 所以回调里只该「记一个值」,不该「重绘整个界面」——否则界面会被事件压垮。

还有一条铁律:回调是在 VLC 的线程里执行的,不是你的界面线程。 所以回调里绝对不能直接碰控件。 这条规则和 Qt 的信号槽、MFC 的 PostMessage 完全一样。

六、动手 Demo:一个实例带多个播放器,别一个播放器建一个实例

这段把「每播放器一实例」与「共享一个实例」的内存各算一遍。 按每实例 4 MB、每播放器 0.4 MB 估,这两个数在真实项目里是同一量级。

inst, player = 4.0, 0.4        # MB
for n in (1, 4, 16, 64):
    a = n * inst + n * player    # 每播放器一实例
    b = inst + n * player        # 共享一个实例
    T([u"%d 路" % n, u"%.1f MB" % a, u"%.1f MB" % b,
       u"%.1f MB" % (a - b)], [0, 0, 0, 0])

实跑输出:

--------------------------------------------------------------------
一个实例带多个播放器,别一个播放器建一个实例
--------------------------------------------------------------------
**LibVLC 里有两个层级:实例(instance)和播放器(media player)。**
**一个实例可以挂多个播放器,实例本身是重的,播放器是轻的。**

**实例重在哪**:它要加载插件表、建日志系统、起事件循环。**一块儿按几 MB 估,播放器只占几百 KB。**

  同时播放     每播放器一实例     共享一个实例     省下
  -------------------------------------------------------------
  1 路         4.4 MB             4.4 MB           0.0 MB
  4 路         17.6 MB            5.6 MB           12.0 MB
  16 路        70.4 MB            10.4 MB          60.0 MB
  64 路        281.6 MB           29.6 MB          252.0 MB

**十六路同时播,省下六十多 MB——这还不算启动时间。**
  每建一个实例都要重新枚举一遍插件,**启动开销是叠加的**。
  而共享实例时,插件表只建一次。

**但共享实例也有代价,必须知道**:
  **一个实例挂了,挂在上面的所有播放器一起挂。**
  实例级的配置(日志级别、音量策略)是全局的,改一处影响全部。
  **事件回调也都在一个实例上飞,回调里做重活会拖慢所有播放器。**

**所以工程上的结论是**:**同一个窗口里的多路(画中画、九宫格监控)共享一个实例。**
  不同业务、要求互相隔离的,才各自建实例。

十六路省下六十 MB,六十四路省下两百五十 MB。 这个差距在监控客户端里是会被用户直接感知到的——因为内存占用是实打实的。

但真正该记住的不是内存,是启动时间:每建一个实例都要重新扫一遍插件目录, 十六路就是扫十六遍。这就是为什么「一开九宫格就卡三秒」的客户端,问题往往在这里。

最后提醒一句:共享实例意味着共享故障域。如果其中有路流的数据能把解码器搞崩, 整个实例连带所有画面一起黑。 所以「隔离」与「省资源」永远是一对矛盾,得按业务挑。

30 秒实验

  1. 想一想:一个「九宫格监控客户端」,该共享实例还是各自建?
  2. 如果九路里有一路是网络极差的高清流,共享实例会有什么风险?
  3. 如果要做「主画面 + 四个小窗」的播放器,媒体对象该怎么复用?

常见误区

  1. 以为 LibVLC 是个轻量解码库。 它是整个 VLC,你的安装包必须带上几百 MB 的插件。
  2. 在每个窗口的初始化里都 libvlc_new。 实例是重的,多路共享一个实例能省内存与启动时间。
  3. 在事件回调里直接刷新界面。 回调在 VLC 的线程里,碰控件就是跨线程访问。
自测三问(点开对答案) 1. LibVLC 的三层对象分别是什么、各活多久? —— instance(整个程序)、media_player(一个窗口)、media(一次播放)。 2. `media_player_new_from_media` 之后,自己的 media 还要释放吗? —— 可以立即释放。这个函数内部已经加了一次引用,你手里的那份留着也是负担。 3. 为什么说「时间变了」事件不能在回调里重绘界面? —— 它每秒触发好几次,而且是在 VLC 的线程里。重绘会拖垮界面且跨线程。

下一节把 API 按用途分一遍:几十个函数其实只有三种角色,记住角色比记住名字有用。

相关笔记