3. 核心概念:封装格式与编码格式的区别、容器与流的关系、音视频同步基础、时间基(time_base)理解
说实话,我见过不少刚入行的朋友,一上来就对着 FFmpeg 命令行猛敲,结果经常遇到「编码器不支持这个格式」或者「音画不同步」的诡异问题。其实这些坑,根源都在于没搞清楚几个最基础的概念。
今天这一节,咱们就把这些核心概念掰开揉碎。你搞懂了它们,后面写命令行就跟玩一样。
3.1 封装格式 vs 编码格式:别再把它们混为一谈
很多人以为 .mp4 就是一种「视频格式」。其实不对。.mp4 是封装格式,而里面的视频数据可能是 H.264 编码,也可能是 H.265 编码。这是两码事。
一句话总结:
- 编码格式:决定「怎么压缩」视频和音频数据。比如 H.264、AAC。
- 封装格式:决定「怎么打包」压缩后的数据。比如 MP4、MKV、FLV。
我举个例子你就明白了。编码格式就像你写文章用的语言——中文还是英文。封装格式就像你装订文章的方式——是订书机装订,还是活页夹。语言变了,内容就变了;装订方式变了,内容还是那个内容。
我在项目中遇到过有人把 .ts 文件直接改成 .mp4,结果播放器死活不认。嗯,改后缀名只是改了「包装盒」,里面的数据还是 MPEG-TS 的流结构,播放器当然不买账。
| 维度 | 编码格式 | 封装格式 |
|---|---|---|
| 本质 | 压缩算法 | 容器规范 |
| 常见例子 | H.264, H.265, VP9, AAC, MP3 | MP4, MKV, AVI, MOV, FLV |
| 文件扩展名 | 通常不直接体现 | 直接对应 |
| FFmpeg 参数 | -c:v (视频编码器), -c:a (音频编码器) |
-f (强制格式) 或由扩展名自动推断 |
我的小习惯: 拿到一个陌生视频文件,先用 ffprobe 看一眼。它能告诉你封装格式是什么,里面流的编码格式又是什么。别靠猜。
3.2 容器与流的关系:一个盒子里的多条轨道
封装格式也叫「容器」。为什么叫容器?因为它真的就像一个盒子,里面可以装好几条「流」。
流(Stream)是什么?你可以把它想象成一条独立的数据轨道。一个视频文件里,通常至少有一条视频流和一条音频流。有时候还有字幕流、章节信息流。
你想想看,一个 MP4 文件里,视频流和音频流是各自独立压缩、独立存储的。播放器在播放时,再把它们同步拉出来。这就是「容器」的功劳——它负责管理这些流,告诉播放器「哪一段视频对应哪一段音频」。
# 查看一个文件里有多少条流
ffprobe -v error -show_entries stream=index,codec_type,codec_name input.mp4
# 输出示例:
# [stream]
# index=0
# codec_type=video
# codec_name=h264
# [/stream]
# [stream]
# index=1
# codec_type=audio
# codec_name=aac
# [/stream]
# [stream]
# index=2
# codec_type=subtitle
# codec_name=mov_text
# [/stream]
看到没?索引 0 是视频,索引 1 是音频,索引 2 是字幕。这三条流共享一个容器。
我曾经踩过的坑: 有一次我需要把一个 MKV 文件转成 MP4,直接用了 ffmpeg -i input.mkv output.mp4。结果报错说字幕流不支持。后来才想起来,MP4 容器对字幕格式有限制,不是所有字幕流都能往里装。所以转封装时,一定要考虑容器的「兼容性」。
3.3 音视频同步基础:为什么画面和声音会「各说各话」
音视频不同步,是转码时最让人头疼的问题之一。说白了,就是视频帧和音频采样点没对上号。
为什么会这样?因为视频和音频是两条独立的流,它们各自有自己的时间线。播放器需要根据某种「时钟」来协调它们。这个时钟,就是时间基(time_base)。
我习惯把时间基理解成「刻度尺上的最小单位」。视频流可能以 1/90000 秒为刻度,音频流可能以 1/48000 秒为刻度。播放器拿到一个视频帧,看到它的时间戳是 3600,就知道它应该在 3600 * (1/90000) = 0.04 秒时显示。音频包同理。
如果转码时,时间基计算错了,或者时间戳被错误地截断了,画面和声音就会错位。你想想看,一个人说话,嘴型对不上声音,多别扭。
音视频同步的核心:
- 每条流都有自己的 PTS(显示时间戳) 和 DTS(解码时间戳)。
- 播放器根据 PTS 来决定什么时候显示这一帧或播放这一段音频。
- 时间基(time_base)决定了 PTS 值的「真实时间」是多少。
3.4 深入理解时间基(time_base)
时间基这个概念,说实话,刚接触 FFmpeg 时我也绕了很久。它其实就是一个分数,表示「一个时间单位等于多少秒」。
比如 time_base = 1/90000,意味着每个时间单位是 1/90000 秒。如果一个视频帧的 PTS 是 180000,那它的显示时间就是 180000 * (1/90000) = 2 秒。
FFmpeg 内部用 AVRational 结构体来表示时间基,包含分子和分母:
typedef struct AVRational {
int num; // 分子
int den; // 分母
} AVRational;
所以 1/90000 就是 {num=1, den=90000}。
不同的封装格式和编码标准,默认的时间基不一样:
| 场景 | 常见 time_base | 说明 |
|---|---|---|
| H.264 视频流 | 1/90000 | MPEG 标准时间基 |
| AAC 音频流 | 1/48000 或 1/44100 | 与采样率相关 |
| MP4 容器 | 1/1000 (毫秒) | 容器层时间基 |
| MKV 容器 | 1/1000 (毫秒) | 同上 |
| FLV 容器 | 1/1000 (毫秒) | 同上 |
转码时,FFmpeg 会自动做时间基的转换。但如果你手动操作不当,比如用 -vsync 或 -async 参数时,就可能引入同步问题。
我个人的调试技巧: 遇到音画不同步,先用 ffprobe -show_streams input.mp4 查看每条流的 time_base 和 start_pts。如果发现两条流的 start_pts 差距很大(比如视频从 0 开始,音频从 1000 开始),那多半是源文件就有问题。
3.5 一张图理清所有关系
下面这张 SVG 图,把封装格式、编码格式、流、时间基之间的关系串起来了。你盯着看一分钟,应该就能明白整个架构。
从这张图可以看得很清楚:容器在最外层,里面包裹着视频流、音频流,有时还有字幕流。每条流都有自己的编码格式和时间基。播放器读取容器,解析出各条流,然后根据时间戳把它们同步呈现出来。
3.6 实战中怎么用这些概念
搞懂了这些,再回头看 FFmpeg 命令行,很多参数就一目了然了。
# 转码时指定视频编码器和音频编码器
ffmpeg -i input.mkv -c:v libx264 -c:a aac output.mp4
# 只改变封装格式,不重新编码(流拷贝)
ffmpeg -i input.mkv -c copy output.mp4
# 查看每条流的详细信息
ffprobe -v error -show_streams -of json input.mp4
你看,-c:v 指定视频编码器,-c:a 指定音频编码器,-c copy 就是直接复制流数据,不做编解码。这些操作,都是基于「容器里有独立流」这个前提。
注意: 流拷贝(-c copy)虽然快,但不是万能的。如果源文件的编码格式目标容器不支持,就会报错。比如把 H.265 视频直接拷贝到老旧的 AVI 容器里,大概率会失败。所以,先搞清楚容器的「能力边界」。
好了,这一节的内容就到这里。封装格式、编码格式、容器与流、时间基,这几个概念是 FFmpeg 的基石。你以后写任何转码命令,都离不开它们。多花点时间理解透,后面会省很多事。