🎬 第四章 FFmpeg命令行实现音视频转封装
📄 一句话概括

容器不是一种视频格式,它是个盒子:装若干条流,再加一张目录。 「MP4 文件」的准确说法是「用 MP4 盒子装了 H.264 加 AAC」。

📝 本节和前面几节的关系

第三章讲的是「查选项」,从这一章开始动手改文件。 第一件事就是搞清楚:一个 mp4 里到底有几样东西。

一、一个文件的四层账

从外到里,每一层都有自己的账:

层 叫什么 管什么


盒子 容器/封装 把几条流装在一起,写一张目录 流 track 一条视频或一条音频 包 packet 封装的最小单位,带时间戳 帧 frame 解出来的画面或声音

「封装」就是把包按某种规矩排进盒子,「解封装」就是反过来把包取出来。 这两个动作都不碰画面本身——包里的字节一个都不改。

而「解码」才是把包变回帧,那一步才真的动数据。 分清「封装」与「解码」,是这一整章的地基。

二、包与帧不是一一对应的

最容易想错的一处:以为一帧就是一个包。 实际上大多数时候接近,但不严格相等:

情况 包与帧 原因


视频无 B 帧 基本相等 一帧压成一个包 编码器收尾 包多 1 flush 时吐一个尾包 音频 各自计数 一包固定 1024 采样

第二行那个尾包很容易被忽略,但它是「解完必须 flush」这个规矩的由来。 不 flush,最后那一小段数据就丢了——表现是「时长少了一点点」。

第三行的音频更要小心:一包 1024 采样,跟「一帧画面」完全不是一个概念。 所以「这个文件有多少帧」这种问题,问音频是没有意义的。

三、转封装为什么不花钱

因为包可以原样搬走,只有目录要重写:

动作 要做什么 代价


转封装 取包、改目录、写包 几乎不花 CPU 重编码 解帧、算、编包 CPU 与时间都贵 分离 取包、写两个盒子 同转封装

第一行的代价「几乎不花 CPU」是有前提的:盒子接不接这种编码。 盒子不接,就只能走第二行——这是第四章与第五章的分界线。

所以判断「能不能只转封装」,问的永远是「目标盒子认不认这种码流」, 而不是「这两个格式能不能互转」——后者是个伪问题。

四、动手 Demo:一个文件的四层账:盒子、流、包、帧

造一份 2 秒的素材:1 路视频(50 帧)加 1 路音频(2 秒)。 然后分别数:文件多大、有几条流、解出多少包、解出多少帧。 四个数字摆在一起,四层结构就具体了。

import av
import numpy as np

# 造素材:1 路视频 + 1 路音频(完整代码见 Demo 素材函数 _mk)
import av

c = av.open(u"m4.mp4", u"w")
v = c.add_stream(u"libx264", rate=25)
v.width, v.height, v.pix_fmt = 160, 90, u"yuv420p"
a = c.add_stream(u"aac", rate=44100)
a.layout, a.format = u"stereo", u"fltp"
# 50 帧画面 + 2 秒声音,写入 m4.mp4
...
c.close()

c = av.open(u"m4.mp4")
print(u"容器名 %s" % c.format.name)
print(u"流数   %d" % len(c.streams))
c.close()

# 数包
c = av.open(u"m4.mp4")
nv = na = 0
for pk in c.demux():
    if not pk.size:
        continue
    if pk.stream.type == u"video":
        nv += 1
    else:
        na += 1
c.close()
print(u"视频包 %d  音频包 %d" % (nv, na))

# 数帧
c = av.open(u"m4.mp4")
fv = sum(1 for _ in c.decode(video=0))
c.close()
print(u"视频帧 %d" % fv)

实跑输出:

--------------------------------------------------------------------
4.1 容器与封装/解封装——一个文件的四层账
--------------------------------------------------------------------

**① 容器不是「一种视频格式」,它是个盒子**

**盒子里装着若干条流,每条流由一种编码格式压出来,再加一张目录(索引)。**
**所以「MP4 文件」这个说法本身就不准确——准确说法是「用 MP4 这个盒子装了 H.264 加 AAC」。**

  项             值
  ------------------------------------------
  容器格式名     mov,mp4,m4a,3gp,3g2,mj2
  流数           2
  流类型         video、audio
  时长           2.00 秒
  文件字节       37982

**② 解封装出包,解码出帧**

  统计项         视频     音频
  ---------------------------------
  包数           50       88
  包字节合计     2552     32331

**③ 帧数才是「解出来」的东西**

  统计项         值
  ---------------------------------------
  视频帧数       50
  音频帧数       87(每帧 1024 采样)
  包字节占比     92% 的文件是包

**视频 50 帧,2 秒 × 25 帧/秒,对得上;音频 87 帧 × 1024 采样约 2.02 秒,也够 2 秒。**
**包数与帧数并不一一对应:视频这一路相等(50 对 50),音频那一路包数多 1(88 对 87),**
**多出来的是编码器收尾时吐的那个尾包——这也是「解码完必须 flush」的由来。**
**第三行说明剩下的那部分是容器自己花的钱——头、索引、每个包的描述信息。**
**这就是「转封装只改盒子、不改内容」的物质基础:包可以原样搬走,只有目录要重写。**

四层账在这份素材上的比例是:文件 37982 字节,其中 92% 是包,剩下 8% 是盒子的开销。 这个 8% 就是「转封装要重写的那部分」——只有目录,没有数据。

视频 50 包对 50 帧,音频 88 包对 87 帧——差的那一个就是 flush 时吐的尾包。 所以「包数等于帧数」这句话只在视频这一侧、且没有 B 帧延迟时才好用。

注意代码里「数包」和「数帧」是两次独立打开文件:一次只做解封装,一次做了完整的解码。 这两件事的开销差着一个数量级——这也解释了为什么 -c copy 那么快。

30 秒实验

  1. 看容器与本流:ffprobe -show_format -show_streams 片子.mp4,先认盒子再认流。
  2. 只转封装:ffmpeg -i 片子.mp4 -c copy 出.mp4,观察耗时几乎是零。
  3. 强制重编码:ffmpeg -i 片子.mp4 -c:v libx264 -crf 28 出.mp4,对比耗时与体积。

常见误区

  1. 把「MP4」当成一种视频编码。MP4 是盒子,里面装的可能是 H.264,也可能是 H.265。
  2. 以为一帧就是一个包。编码器收尾会多吐一个尾包,音频更是「一包 1024 采样」。
  3. 以为格式之间「能不能互转」是个格式问题。真正的问题是「目标盒子认不认这种码流」。
自测三问(点开对答案) 1. 封装与解码的区别是什么? —— 封装只是把包按规矩排进盒子,不碰画面字节;解码才是把包还原成画面或声音。 2. 为什么 `-c copy` 那么快? —— 因为它只做解封装加封装,跳过了解码、滤镜、编码三步,CPU 几乎不干活。 3. 「这个文件有多少帧」这句话对音频成立吗? —— 不成立。音频的包是固定 1024 采样一块,跟「帧」不是同一个概念。

相关笔记