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

VLC 是「什么都能放」的代名词,它的本事来自一个很朴素的设计:按能力挂模块。 代价是启动慢、体积大——它把兼容性放在了第一位。

📝 本节和前面几节的关系

第一章把播放器的五段流水讲完了,这一章开始看一个把五段全做好的现成产品。 先看 VLC 本体怎么用、为什么兼容性这么强;再看怎么用它提供的接口写自己的播放器(LibVLC 二次开发)。 这一章是「先会用,再会改」。

一、它是怎么来的

VLC 起于 1996 年巴黎中央理工的学生项目,本来只是给校园网做电视流的路由工具。名字的展开是 VideoLAN Client——它最早是个客户端,服务端另有其物。

它的两个关键决定,一直影响到今天: 一是不用系统的解码器,自己带一套(后来大量借助 FFmpeg 的库)。 二是跨平台:Windows、macOS、Linux、Android、iOS 全套自己做。

「自己带解码器」是兼容性无敌的根本原因:系统自带的解码器受专利与授权限制,格式支持有限; VLC 自己带,能解多少取决于它编译了什么,不取决于你装了什么。这就是为什么一台刚装好的 Windows 放不了 mkv,VLC 一装就能放。

二、模块化的组织方式

VLC 内部不是一个大程序,是几百个小模块拼起来的。 每个模块只声明「我能干什么」,不声明「我叫什么」。

这个「按能力挂」的思路,是整个架构的核心:播放一个文件时,VLC 依次问——谁能打开它(access)、 谁能拆它(demux)、谁能解它(decode)、谁能显示它(vout)、谁能发声(aout)。 每一问都有多个候选。

好处是「加一种格式」变成「丢一个 DLL 进去」,不用改主程序、不用重新发行。 坏处是「每次都要从一堆候选里挑」——这就是启动慢的第一个来源。

demo 里量了一下「挑」的成本:三百个候选里顺序找一个,每次 1.66 微秒;查表只要 0.063 微秒。 差别二十六倍。 但这个量级还解释不了启动那两三秒——真正的时间花在加载 DLL 与初始化上。

三、它的体积为什么这么大

VLC 的安装包一百多 MB,解压后两三百 MB,插件目录里上千个文件。 这些体积换来的是「不用装解码包」。

拆开看,体积主要在三处: 解码器:FFmpeg 的一整套库(avcodec 就有几十 MB)。 硬件加速后端:D3D11、DXVA、VA-API、VDPAU、Metal 各一份。 界面与扩展:Qt 界面、皮肤、Lua 脚本引擎、流媒体服务器模块。

这个取舍值得记住: 要「装完就能放一切」,就得把所有可能用到的解码器都带上;要「体积小」,就只能按需装。 这是同一个问题的两个答案,没有对错,只有取舍。

四、和 FFplay 的关系

VLC 与 ffplay 都是播放器,但定位完全不同:

对比项 VLC ffplay
用户 普通观众 开发者
界面 完整图形界面 一个窗口加几个快捷键
能调的东西 菜单里的那些 命令行参数
代码量 几十万行 一个 C 文件
定位 成品 FFmpeg 的 API 示例

ffplay 的价值不是「用来放片」,是「用来验证 API」:它把 FFmpeg 每个接口都用了一遍, 代码又只有几千行,是学 FFmpeg 最好的参考实现。第九章整章都在拆它。

VLC 也用 FFmpeg 的库,但它是「用」,不是「照着写」:它有自己的解封装、自己的渲染、 自己的模块体系,只在需要解码时才调 libavcodec。所以「VLC 是基于 FFmpeg 的」这句话只对了一半。

五、什么时候该用它

三句话能说清:

要「在客户机器上放任何格式」→ 用 VLC。它是唯一能保证「不装解码包也能放」的成品播放器。

要「把播放能力嵌进自己的程序」→ 用 LibVLC。 这是本章后半部分的主题:它把 VLC 的全部能力包成一套 C 接口,你只要给一个窗口句柄、给一个地址, 剩下的它全包。

要「学播放器怎么写」→ 读 ffplay。VLC 的代码量太大,读起来看不到全局;ffplay 一个文件就能读完。

还有一条实用建议: 如果你只是要「在程序里播个视频」,先查系统自带的媒体框架(Windows 的 Media Foundation、 macOS 的 AVFoundation),它们体积为零、不需要额外部署。 只有当格式支持不够时,才轮到 LibVLC。

六、动手 Demo:几百个模块,查找方式决定启动快慢

这段先在三百个候选里比「顺序找」与「查表」的成本,再说明这个差距能解释什么、不能解释什么。 重点是后半段那句「别拿它解释整段启动时间」。

names = [u"mod%03d" % i for i in range(299)] + [u"mp4"]
rng.shuffle(names)

# 顺序找
for n in names:
    if n == u"mp4":
        break

# 查表
dmap = dict((n, i) for i, n in enumerate(names))
dmap.get(u"mp4")

实跑输出:

--------------------------------------------------------------------
几百个模块,查找方式决定启动快慢
--------------------------------------------------------------------
**VLC 号称「什么都能放」,代价是它内部注册了几百个模块,每次要按用途挑一个。**
**挑法有两种:按顺序一个一个试,或者查表直接命中。**
**这两种在几百个候选下的差距是可以量出来的。**

  本机 FFmpeg 注册的封装格式名一共 **414 个**,VLC 的模块数是同一个量级。

**同一个「找 mp4」的动作,各跑两万次**:

  找法       每次微秒     相对         说明
  ---------------------------------------------------------------
  顺序试     1.56         1.0 倍       要试到第 72 个才命中
  查表       0.052        29.89 倍     一步到位,与候选数无关

**单次差 1.51 微秒,看着不多;但 VLC 启动要枚举上千次能力。**
  **所以真实 VLC 启动那两三秒,主要花在「加载 DLL 并初始化」,不是花在「比对名字」。**
  这段量出来的只是「查找」这一层的量级,别拿它解释整段启动时间。

**但它说明了一个设计取舍**:
**模块化让「加一个格式」只需丢一个 DLL 进去,代价是每次都要从一堆候选里挑。**
**这就是「可扩展」与「启动快」之间的那笔账。**

二十六倍的差距听着很大,但它解释不了 VLC 启动那两三秒。因为「查找」是纳秒到微秒级的事, 而启动是秒级的事。

真正花时间的是三件事:读上千个小文件(插件目录)、把每个 DLL 映射进内存、 执行每个模块的初始化代码。这三件事都是 IO 与系统调用,比字符串比较贵三四个数量级。

那为什么还要量这段:因为它说明「模块化本身有成本」。 这个成本在候选少的时候可以忽略,在候选上千的时候就必须靠「缓存结果」来摊平——VLC 确实会把模块清单缓存起来,第二次启动明显更快。

30 秒实验

  1. 装一个 VLC(如果没有),数一下它的 plugins 目录里有多少个文件。
  2. 第一次启动与第二次启动各掐一次表,看差多少。
  3. 想一想:为什么它宁可启动慢,也不做成「按需下载解码器」?

常见误区

  1. 以为 VLC 只是「FFmpeg 的一个界面」。 它自己实现了解封装、渲染与模块体系, 只在解码环节大量借助 FFmpeg 的库。
  2. 以为「什么都能放」是免费的。 代价是几百 MB 的体积与更长的启动时间。
  3. 以为 VLC 适合嵌进产品。 嵌进产品应该用 LibVLC 或系统媒体框架, 直接带整个 VLC 会让安装包膨胀好几倍。
自测三问(点开对答案) 1. 为什么刚装好的 Windows 放不了 mkv,装了 VLC 就能放? —— 因为系统自带的解码器受授权限制,格式支持有限; VLC 自己带一整套解码器,能解什么取决于它编译了什么。 2. VLC 的模块「按能力挂」是什么意思? —— 每个模块声明的是「我能做 demux/decode/vout 中的哪一种」,而不是「我叫什么」。 播放时按能力逐段挑选,而不是按名字调用。 3. 要学播放器怎么写,为什么读 ffplay 而不是读 VLC? —— ffplay 只有几千行、一个文件,能看到全局;VLC 几十万行,读进去容易迷路。

下一节把它的能力列成一张表:播放之外它还能转码、推流、录制,这些能力其实共用同一条管道。

相关笔记