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 秒实验
- 命令行对照:
ffmpeg -i 片子.mp4 -c:v libx264 -c:a aac -f mpegts 出.ts。 - 切一段:
ffmpeg -i 出.ts -t 1 -c copy 切片.ts,TS 天然支持按时间切。 - 看包结构:用十六进制工具看每 188 字节的第一位,应当都是 47。
常见误区
- 以为 TS 是「另一种 MP4」。一个是定长包流,一个是可变长盒子,设计目标完全不同。
- 用 TS 做本地存档。开销比 MP4 高,而且索引能力弱。
- 手工改 PID。改错了整路流就解不出来,让封装器分配就行。
自测三问(点开对答案)
1. TS 一个包多少字节?同步字节是什么? —— 188 字节,第一字节固定是 0x47。找到它就能重新对齐。 2. PID 是干什么的? —— 区分同一条 TS 里的不同流。解复用器靠它把交织的包分回画面与声音。 3. 为什么 TS 适合直播? —— 包定长、可随时接入、丢包只影响局部,不需要拿到整份文件才能开始播。- ☐ (本节完,下一节 5.4.5 其他格式之间互转)