🎬 第五章 FFmpeg命令行实现音视频转码
📄 一句话概括

TS 整个文件就是一堆 188 字节的定长包,每包开头必须是 0x47。 百分之二的开销,换来的是「任意位置都能接入」。

📝 本节和前面几节的关系

前面几个容器都是「盒子套盒子」,TS 不一样,它是定长包。 这个设计是为直播与广播准备的,理解了它就能理解为什么电视信号不怕丢包。

一、188 是这个格式的全部

把输出文件按 188 字节切开,检查每个包的开头:

项 值


文件字节 17484 包数 93 同步字节正确 93 余数字节 0

第三行与第二行相同、第四行是 0,说明整个文件能被 188 整除,且每个包开头都是 0x47。 这就是「同步字节」的作用:从任意位置开始读,找到 0x47 就能重新对齐。

为什么是 188 这个奇怪的数字:4 字节包头加 184 字节载荷, 刚好能塞进当年 ATM 信元的整数倍——这是历史留下的尺寸,不是算出来的最优值。

二、两条流各有自己的编号

TS 里画面与声音是交织的,靠 PID 区分:

流 编号 PID 编码


video 256 h264 audio 257 aac

解复用器就是靠这个编号,把交织在一起的包分回各自的流。 再配合 PAT 与 PMT 两张表,播放器才知道「哪个 PID 是画面、哪个是声音」。

这也解释了为什么 TS 天生支持多路:加一条流就是加一个 PID, 互相不干扰,插一条新流也不用重排整个文件。

三、开销花在哪

每个包 4 字节包头,剩下 184 字节装数据:

项 值


包头字节 372(93×4) 占整文件 2.1% 每包有效载荷 184 字节

百分之二左右的开销,换来「任意点接入」的能力,直播场景这笔账很划算。 换成 MP4 就没这个余量:盒子长度可变,必须从头解析索引。

代价是丢包处理:TS 包之间互相独立,丢一个只影响那一小段, 所以电视信号在弱网下是「花一下」而不是「整个卡住」。

四、动手 Demo:转成 TS,再按 188 字节拆开验证

把素材重编码成 TS,然后读整个文件、按 188 字节切包,数同步字节。 最后打印两条流的 PID 与编码名。

import av, os

c1 = av.open(u"o.ts", u"w", format=u"mpegts")
v1 = c1.add_stream(u"libx264", rate=25)
v1.width, v1.height, v1.pix_fmt = 160, 90, u"yuv420p"
v1.options = {u"crf": u"26"}
a1 = c1.add_stream(u"aac", rate=44100)
a1.layout, a1.format = u"stereo", u"fltp"
c0 = av.open(u"s5.mp4")
for f in c0.decode(video=0):
    for o in v1.encode(f.reformat(width=160, height=90, format=u"yuv420p")):
        c1.mux(o)
for o in v1.encode(None):
    c1.mux(o)
for f in c0.decode(audio=0):
    for o in a1.encode(f):
        c1.mux(o)
for o in a1.encode(None):
    c1.mux(o)
c1.close()
c0.close()

data = open(u"o.ts", u"rb").read()
n = len(data) // 188
good = sum(1 for i in range(n) if data[i * 188] == 0x47)
print(u"包数 %d  同步正确 %d  余数 %d" % (n, good, len(data) % 188))
c = av.open(u"o.ts")
for s in c.streams:
    print(u"%s PID %s %s" % (s.type, s.id, s.codec_context.name))
c.close()

实跑输出:

--------------------------------------------------------------------
5.4.4 MP4 格式转码为 TS
--------------------------------------------------------------------

**① 转成 TS,先看它能被 188 整除**

  项                     值
  -----------------------------
  文件字节               17484
  包数                   93
  同步字节 0x47 正确     93
  余数字节               0

**第三行与第二行相同、第四行是 0,说明整个文件就是一堆 188 字节的定长包。**
**TS 的全部设计都建立在这个 188 上:从任意位置切入,找到 0x47 就能重新同步。**

**② 两条流各有自己的编号**

  流        编号 PID     编码
  ----------------------------
  video     256          h264
  audio     257          aac

**这个编号就是 PID。解复用器靠它把交织在一起的包分回各自的流,**
**再配合 PAT 与 PMT 两张表告诉播放器「哪个 PID 是画面、哪个是声音」。**

**③ TS 的体积开销花在哪**

  项               值
  -----------------------------
  包头字节         372(93×4)
  占整文件         2.1%
  每包有效载荷     184 字节

**百分之二左右的开销换来的是「任意点接入」的能力,直播场景这笔账很划算。**
**换成 MP4 就没有这个余量:它的盒子长度可变,必须从头解析索引。**

读整个文件再切包,是最直观的验证方式:几行代码就能证明「它真的是定长的」。 文件大时不要这么干,改成一次读 188 字节循环即可。

s.id 就是 PID,codec_context.name 是这条流的编码名。 PID 由封装器自动分配,命令行里可以用 -streamid 手工指定。

注意音视频是分开两个循环喂的,写进 TS 时会自动交织。 真实场景应该边解边写同一个循环,否则内存里会堆一大批包。

30 秒实验

  1. 命令行对照:ffmpeg -i 片子.mp4 -c:v libx264 -c:a aac -f mpegts 出.ts。
  2. 切一段:ffmpeg -i 出.ts -t 1 -c copy 切片.ts,TS 天然支持按时间切。
  3. 看包结构:用十六进制工具看每 188 字节的第一位,应当都是 47。

常见误区

  1. 以为 TS 是「另一种 MP4」。一个是定长包流,一个是可变长盒子,设计目标完全不同。
  2. 用 TS 做本地存档。开销比 MP4 高,而且索引能力弱。
  3. 手工改 PID。改错了整路流就解不出来,让封装器分配就行。
自测三问(点开对答案) 1. TS 一个包多少字节?同步字节是什么? —— 188 字节,第一字节固定是 0x47。找到它就能重新对齐。 2. PID 是干什么的? —— 区分同一条 TS 里的不同流。解复用器靠它把交织的包分回画面与声音。 3. 为什么 TS 适合直播? —— 包定长、可随时接入、丢包只影响局部,不需要拿到整份文件才能开始播。

相关笔记