几十个函数,按名字前缀就能分成三类:造对象、控状态、问状态。 真正难的不是记名字,是记住「哪些是异步的、哪些会加引用」。
上一节讲了三层对象,这一节看这些对象身上的函数。
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 秒实验
- 打开 LibVLC 的头文件(装 SDK 后),数一下
libvlc_media_player_开头的函数有几个。 - 把它们按「造与毁/控状态/问状态」分一下类。
- 想一想:为什么
set_time立刻返回,而实际跳转要等一会儿?
常见误区
- play 之后立刻 get_time,拿到 0 就以为失败了。播放是异步的,真正的开始要靠事件通知。
- 在界面线程里调
will_play。 它会阻塞到知道结果为止,网络慢时界面会假死。 - 退出时只 release 不 detach 事件。 VLC 还留着你的函数指针,下一次触发就跳进已释放的内存。
自测三问(点开对答案)
1. `libvlc_audio_*` 这一组函数管什么? —— 声音相关:音量、静音、声道、输出设备。 2. 为什么「设置之后立刻查询」经常拿到旧值? —— 因为设置类函数多数是异步的,真正生效在 VLC 的线程里。 3. `play()` 返回 0 就代表能播吗? —— 不。它只代表「请求被接受」。播不了会通过 EncounteredError 事件通知。API 看完了,下一步是把 SDK 装起来。下一节讲那三样东西,以及为什么「装了 VLC 还是编不过」。
- ☐ (本节完,下一节 02.03.04 安装VLC的SDK)