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

几十个函数,按名字前缀就能分成三类:造对象、控状态、问状态。 真正难的不是记名字,是记住「哪些是异步的、哪些会加引用」。

📝 本节和前面几节的关系

上一节讲了三层对象,这一节看这些对象身上的函数。 LibVLC 的命名很规矩:libvlc_<对象>_<动作>,看懂前缀就知道它动的是哪一层、干什么。

一、命名规律

所有函数都长这样:libvlc_<对象>_<动作>。

前缀 动的是哪层 例子
libvlc_ 实例 libvlc_new/libvlc_release
libvlc_media_ 媒体 libvlc_media_new_path
libvlc_media_player_ 播放器 libvlc_media_player_play
libvlc_video_ 画面相关 libvlc_video_set_scale
libvlc_audio_ 声音相关 libvlc_audio_set_volume
libvlc_event_ 事件相关 libvlc_event_attach

这个规律让「查文档」变得容易:你想「设置音量」,就知道去找 libvlc_audio_*; 想「设窗口」,就去找 libvlc_media_player_set_hwnd。

注意没有 libvlc_player_ 这个前缀,播放器一律是 media_player。 这个命名有点绕,但记住就不会找错地方。

二、三类函数

按动作分,只有三类:

一、造与毁(生命周期) *_new 系列创建,*_release 系列释放。这一类的坑是「引用计数」,上一节说过。

二、控状态(设置) play/pause/stop、set_media/set_hwnd、set_volume/set_time。 这一类多数是异步的——调完不会立即生效。

三、问状态(查询) is_playing/get_time/get_length/get_state。 这一类通常很快返回,但结果可能是「上一次的」。

这三类的区别在工程上很重要: 你要是写完 play 立刻 get_time,拿到的多半是 0——因为播放还没真正开始。 所以「设置之后不要立刻查」是一条实用的经验。

三、阻塞与非阻塞

LibVLC 里有一对很容易混的函数:

函数 行为 什么时候用
media_player_play 立刻返回 界面里播放
media_player_stop 可能等一小会儿 需要确认停下
media_player_set_time 立刻返回 拖进度条
media_player_will_play 等待并返回是否可播 播前检查

stop 为什么要等:它要等解码线程退出、要等输出设备释放。 在界面线程里频繁调 stop,会让界面卡顿。

will_play 是那个「等待并返回」的异类:它会阻塞到「知道能不能播」为止, 所以绝对不能在界面线程里调它——网络慢的时候它会卡住好几秒。

这条经验很值钱: 任何「可能等网络」的函数,都不能放在界面线程上。看文档时要专门找「blocking」这个词。

四、事件系统

事件是 LibVLC 唯一的「回调方向」——它不会主动通知你,只能你先把回调挂上去。

挂事件的固定四步: 准备一个回调函数(签名固定,第一个参数是事件指针)。 把回调包成 libvlc_callback_t。 libvlc_event_attach 挂到指定对象与指定事件上。 退出前必须 detach,否则回调可能在对象释放后还被调用。

最后一步是最容易漏的:程序退出时先 release 了播放器、却忘了 detach 事件, VLC 内部还留着一个指向你代码的函数指针, 下一帧触发就跳到已释放的内存里——崩溃现场看起来毫无关联。

还有一个细节:事件的回调里拿到的数据是「当时那一刻的快照」,不要在回调里长时间持有它, 更不要跨线程传出去。要传就拷贝一份。

五、错误处理

LibVLC 的错误处理很「C 风格」:创建失败返回 NULL,动作失败返回 -1 或 false。 没有异常,也没有错误码对象。

所以每一句都要判: libvlc_new 返回 NULL → 插件目录找不到、或者内存不足。 media_new_path 返回 NULL → 路径不合法。 media_player_new 返回 NULL → 实例不合法。

「出错了但返回成功」的情况也存在:play() 返回 0 只表示「请求被接受」, 真正播不了会通过 EncounteredError 事件告诉你。 所以「检查返回值」与「监听错误事件」两件事都要做。

这条在界面开发里特别重要:用户拖进来一个坏文件,play 返回成功,界面显示「正在播放」, 两秒后弹出「无法播放」——这中间的空白体验就是没监听事件造成的。

六、动手 Demo:三层对象,各管一段生命周期

这段用 PyAV 把「容器 → 流 → 解码器」三层摆出来,对应 LibVLC 的「实例 → 播放器 → 媒体」。 沙箱里没有 VLC,所以只能看「同构的那一层」,看不到 LibVLC 本身。

c = av.open(p)
for st in c.streams:
    cc = st.codec_context
    T([u"流 %d" % st.index, u"%s" % st.type,
       u"解码器 %s" % cc.name], [0, 0, 0])
c.close()

实跑输出:

--------------------------------------------------------------------
三层对象,各管一段生命周期
--------------------------------------------------------------------
**LibVLC 的 API 看起来散,其实只有三层对象:实例、播放器、媒体。**
**每层的生命周期长度不一样,搞错了就是崩溃或泄漏。**

  对象             活多久           建与毁                  PyAV 里的对应物
  -----------------------------------------------------------------------------
  instance         整个程序         libvlc_new/release     —
  media_player     一个窗口         new/release            CodecContext
  media            一次播放         new/release            AVStream
  event            跟播放器同寿     attach/detach          回调函数

**用 PyAV 看一遍同样的三层关系**:

  层       取到的对象         状态
  -------------------------------------------
  容器     InputContainer     1 条流
  流 0     video              解码器 h264

**三层的关系是「外层的死掉,内层的必须已经放开」**:**先关播放器,再关实例。反了就崩。**
  这一点在 C 接口里尤其容易错,因为没有析构函数帮你兜底。
  **PyAV 用 with 语句或 close() 把这件事管住了,C 里全靠自觉。**

**还有一个常被忽略的坑**:**事件的回调是在 VLC 自己的线程里触发的,不是主线程。**
**所以回调里绝对不能直接碰界面控件**——这条规则跟 Qt 的信号槽、MFC 的 PostMessage 是同一个道理。

为什么要用 PyAV 类比: LibVLC 与 FFmpeg 解决的是同一个问题,只是包得粗细不同。LibVLC 把「解封装 → 解码 → 显示」全包了;FFmpeg 只包前两段,显示留给你。所以「三层对象」这个结构在两边都能看到。

差别在于「谁负责释放」:PyAV 用 with 或 close(),Python 还有 GC 兜底; C 里全靠你记得 release。 这就是为什么 LibVLC 的示例代码里,释放语句比功能语句还多。

最后一个必须记住的点:事件回调在 VLC 自己的线程里跑。 所以「回调里不能碰控件」这条规则,在 LibVLC、Qt、MFC 三处是同一句话。

30 秒实验

  1. 打开 LibVLC 的头文件(装 SDK 后),数一下 libvlc_media_player_ 开头的函数有几个。
  2. 把它们按「造与毁/控状态/问状态」分一下类。
  3. 想一想:为什么 set_time 立刻返回,而实际跳转要等一会儿?

常见误区

  1. play 之后立刻 get_time,拿到 0 就以为失败了。播放是异步的,真正的开始要靠事件通知。
  2. 在界面线程里调 will_play。 它会阻塞到知道结果为止,网络慢时界面会假死。
  3. 退出时只 release 不 detach 事件。 VLC 还留着你的函数指针,下一次触发就跳进已释放的内存。
自测三问(点开对答案) 1. `libvlc_audio_*` 这一组函数管什么? —— 声音相关:音量、静音、声道、输出设备。 2. 为什么「设置之后立刻查询」经常拿到旧值? —— 因为设置类函数多数是异步的,真正生效在 VLC 的线程里。 3. `play()` 返回 0 就代表能播吗? —— 不。它只代表「请求被接受」。播不了会通过 EncounteredError 事件通知。

API 看完了,下一步是把 SDK 装起来。下一节讲那三样东西,以及为什么「装了 VLC 还是编不过」。

相关笔记