控制台版播放器一共四步:建实例、建媒体、建播放器、等结束。 代码只有二十行,但它把 LibVLC 三层对象的用法演示完整了。
上一节把 SDK 装好了,这一节写第一个能跑的程序。 控制台版是最小可运行版本:没有界面,画面开在 VLC 自己的窗口里。 先把这条链路跑通,再谈嵌入到别的界面里(后面三节)。
一、工程配置四处
在 VS 里新建一个控制台工程后,要改四个地方:
| 位置 | 填什么 |
|---|---|
| C/C++ → 常规 → 附加包含目录 | VLC 目录\include |
| 链接器 → 常规 → 附加库目录 | VLC 目录\lib |
| 链接器 → 输入 → 附加依赖项 | libvlc.lib |
| 调试 → 环境 | PATH=VLC 目录 |
第四行是最容易被漏的:不加这一句,调试时程序起不来(找不到 libvlc.dll), 但直接双击 exe 又可能能跑(因为系统 PATH 里有)。 这种「F5 不行、双击可以」的怪现象,八成就是这一行没配。
还有一个替代做法:把 libvlc.dll 与 libvlccore.dll 直接拷到工程输出目录,
这样不用改环境变量,但每次清理重建都要重拷一遍。
开发阶段建议用「调试环境变量」,交付时用「拷文件」。
二、最小可运行的代码
四步,一步不多:
#include <vlc/vlc.h>
#include <windows.h>
int main(int argc, char* argv[])
{
libvlc_instance_t* inst;
libvlc_media_t* m;
libvlc_media_player_t* mp;
inst = libvlc_new(0, NULL); /* 1 建实例 */
m = libvlc_media_new_path(inst, "test.mp4");
mp = libvlc_media_player_new_from_media(m);
libvlc_media_player_play(mp); /* 2 开始播 */
while (libvlc_media_player_is_playing(mp))
Sleep(100); /* 3 等结束 */
libvlc_media_player_release(mp); /* 4 释放 */
libvlc_media_release(m);
libvlc_release(inst);
return 0;
}
注意 new_from_media 之后没有释放 m 的语句——因为那个函数内部已经加了一次引用,
外面这份 m 你仍然要释放(示例里放在最后)。
很多示例代码省了这一句,那是泄漏,只是量小看不出来。
还有一个细节:控制台版没有窗口,所以画面会开在 VLC 自己创建的窗口里。
这也是为什么它「能跑」但不能「嵌进你的界面」——要嵌进去得用 set_hwnd,那是下一节的事。
三、为什么要 Sleep 轮询
play 是异步的,所以主线程不能立刻退出——退了程序就结束,播放器跟着死。
所以要有一个「等」的循环。
两种等法:
轮询 is_playing(示例里的写法):简单,但每 100 毫秒醒一次。
挂 EndReached 事件:精确,但要写回调与事件循环。
示例用轮询是因为它短,工程里应该用事件。
因为轮询有个明显的缺陷:「暂停」时 is_playing 也是 false,循环会提前退出。
所以正确的退出条件是「状态变成 Ended 或 Error」,而不是「不再 playing」。
libvlc_media_player_get_state 返回的枚举里有这几个值:NothingSpecial、Opening、Buffering、
Playing、Paused、Stopped、Ended、Error。
四、Python 等价物
沙箱里没有 VS,但有 Python 与 PyAV。 用 PyAV 写一遍同样的四步,可以在沙箱里真跑起来。
import av
c = av.open("test.mp4") # 1 打开(相当于建实例+媒体)
for fr in c.decode(video=0): # 2 解码(相当于播放)
rgb = fr.reformat(format="rgb24")
# 这里本该把 rgb 贴到窗口(相当于 VLC 的 vout)
c.close() # 3 释放
这个对照能看清一件事:LibVLC 省掉的,正是「贴到窗口」那一步之后的所有事——它自己建窗口、 自己按时间显示、自己同步声音。PyAV 只做到「拿到帧」,剩下全靠你。
所以「用 LibVLC 还是用 FFmpeg」这个问题,答案取决于「你愿意自己写多少」: 愿意写同步与渲染,就用 FFmpeg(体积小、可控);不愿意,就用 LibVLC(体积大、省事)。
五、踩坑清单
五个坑,按出现频率排:
| 现象 | 原因 |
|---|---|
| 程序一闪就退 | 没加等待循环,主线程直接结束 |
| 没有声音 | 音频输出模块没加载,或系统默认设备被改 |
| 画面是黑的 | 文件路径用了相对路径,工作目录不对 |
| 播到一半停了 | 文件播完,正常结束,不是 bug |
| 退出时崩 | 释放顺序错,或事件没 detach |
第三行值得单独说:VS 调试时的工作目录是「工程目录」, 而直接双击 exe 时的工作目录是「exe 所在目录」。所以同一个相对路径,F5 能找到、双击找不到。 这类问题一律用绝对路径,或者显式设工作目录。
第一行也很常见:很多人写完四步就 return 0,
结果窗口闪一下就没了——因为主线程退出后,整个进程被销毁,播放器跟着死。
六、动手 Demo:最小播放器的循环,其实是一个时钟
这段用真实视频读出每帧的时间戳,算「每帧该等多久」。 它对应控制台版里那个「等」的循环——只不过这里等得更精确。
for fr in c.decode(video=0):
if fr.pts is None:
continue
sec = float(fr.pts * tb) # 换算成秒
ms = sec * 1000
gap = 0.0 if last is None else ms - last
last = ms
实跑输出:
--------------------------------------------------------------------
最小播放器的循环,其实是一个时钟
--------------------------------------------------------------------
**所有播放器的主循环都能写成一句话:算「这一帧该在什么时候出现」,然后等或不等。**
**LibVLC 把这句话包好了,但它内部做的还是这件事。**
视频流:**320×240、30 fps、时间基 1/15360**。
**按时间戳算出来的「每帧该等多久」**:
帧号 时间戳 应显示时刻 毫秒 距上一帧 毫秒
---------------------------------------------------------
0 0 0.0 0.0
1 512 33.3 33.3
2 1024 66.7 33.3
3 1536 100.0 33.3
4 2048 133.3 33.3
5 2560 166.7 33.3
6 3072 200.0 33.3
7 3584 233.3 33.3
**注意这一列全是 33.3,一毫秒都不差。**
**因为这个片子的时间基是 1/15360,而 30 fps 一帧正好 512 个刻度**,能整除。
**换成 29.97 或 23.976 就没这么巧了**——
帧率 每帧刻度 只能取整到 一千帧累计错
-------------------------------------------------------
30.000 512.00 512 0.0 毫秒
29.970 512.51 513 31.7 毫秒
25.000 614.40 614 26.0 毫秒
23.976 640.64 641 23.4 毫秒
**29.97 fps 每帧要 512.51 个刻度,而时间戳只能是整数**——**一千帧(约 33 秒)就累计错三十多毫秒。**
**这就是为什么播放器不能「每帧固定睡 33 毫秒」**——**误差会累积,几分钟后音画就错开一大截。**
**正确做法是「按绝对时间对齐」**:每一帧都拿「它的时间戳」减「当前时钟」,
**得到的差值决定等多久或丢多少,而不是累加每帧的固定时长。**
这条规则在第九章会再出现一次,那时候它叫「主时钟」。
**LibVLC 把这个时钟藏起来了**,所以写二次开发代码时几乎不用管它。
**但一旦要做「多路画面同步」「外部时钟驱动」,就必须知道它在哪儿。**
表格里每帧间隔都是 33.3 毫秒,一毫秒不差——因为这个片子的时间基 1/15360 与 30 fps 正好整除。
换成 29.97 fps 就没这么巧了:每帧要 512.51 个刻度,而时间戳只能是整数, 一千帧(约 33 秒)就累计错三十多毫秒。 这就是「每帧固定睡 33 毫秒」这种做法必然出问题的原因。
正确做法是「按绝对时间对齐」:每帧都拿「它的时间戳」减「当前时钟」, 差值决定等多久或丢多少,而不是累加固定时长。 这条规则在第九章会再出现一次,那时候它叫「主时钟」。
30 秒实验
- 按上面四步写一遍控制台程序,用 F5 与双击各跑一次,比较工作目录的差别。
- 把
is_playing换成get_state,看暂停时循环还会不会退出。 - 把
new_from_media换成new+set_media,比较两种写法的释放语句。
常见误区
- 写完四步就 return。 主线程退出,进程销毁,播放器跟着死。必须有等待循环。
- 用
is_playing当退出条件。 暂停时它也是 false,循环会提前退出。应该判 Ended 与 Error。 - 用相对路径。 F5 与双击的工作目录不同,同一句代码在两种运行方式下结果不同。
自测三问(点开对答案)
1. 控制台版为什么要有一个 Sleep 循环? —— 因为 play 是异步的,主线程一退出进程就销毁,播放器跟着死。循环让进程活到播放结束。 2. 为什么用 `is_playing` 当退出条件不严谨? —— 暂停时它也是 false。正确的判据是状态变成 Ended 或 Error。 3. PyAV 版与 LibVLC 版差在哪一步之后? —— 拿到帧之后。LibVLC 自己建窗口、自己同步、自己显示;PyAV 只到「拿到帧」,后面全靠你写。控制台版能跑了,但它开的是 VLC 自己的窗口。下一节把它嵌进 MFC 的对话框里——靠的是一个窗口句柄。
- ☐ (本节完,下一节 02.03.06 使用MFC开发基于LibVLC的播放器)