ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MP4、TS 流媒体容器结构、特点与应用场景详解

MP4、TS 流媒体容器结构、特点与应用场景详解

目录

背景

一、编码、容器、分发是三层

1.1 编码、容器、分发三层介绍

1.2、三层各留各的信息

二、MP4:索引在 moov,样本在 mdat

2.1 盒子

2.2 一个点播文件里的三块

2.3 用这些表定位一帧

2.4 moov 在头,还是在尾

2.5 分片 MP4

2.6 参数集和 NAL 的两种形态

三、MPEG-TS:188 字节的传送流

3.1 包头只有 4 字节

3.2 PAT、PMT 和 PES

3.3 PCR 管时钟,PTS 管显示

3.4 广播和直播里它为什么稳

四、FLV、PS、MKV,以及切片协议

4.1 FLV:按 Tag 往下写

4.2 MPEG-PS:国标监控里很常见

4.3 MKV 与 WebM

4.4 MOV 与 AVI

4.5 HLS、DASH、CMAF 是清单加切片

五、放在一张表里看

六、按场景选

七、转封装时要守住的几件事

八、自己把结构看出来

总结


背景

录像、转码和播放里,H.264 与 AAC 会反复出现在 MP4、TS、FLV 中。画面可以一样。能不能拖动、能不能边录边播、丢几个包之后还能不能重新对齐,差别在容器。容器把已经压好的基本流放上时间轴:轨道怎么排、时间戳用什么刻度、关键帧记在哪里、参数集放在带内还是带外。

下面按结构往下拆。先分清编码、容器和分发,再看 MP4 的盒子和 TS 的定长包,然后把 FLV、PS、MKV 以及 HLS、DASH 放回各自的场景。

一、编码、容器、分发是三层

1.1 编码、容器、分发三层介绍

图 1:同一路压缩数据,可以换打包方式,再换传送方式

1.2、三层各留各的信息

换容器时,压缩数据可以原样拷贝。会变的是时间戳刻度、参数集的位置,以及 NAL 用起始码还是长度前缀。FFmpeg 里这叫 stream copy:-c copy。它跳过解码和编码,速度接近文件拷贝。

分发再在容器外面套一层。HLS 的m3u8是播放列表,DASH 的 MPD 是清单,RTMP 是会话。它们描述“去哪拿下一段”,本身不替代 MP4 或 TS。

二、MP4:索引在 moov,样本在 mdat

MP4 属于 ISO 基本媒体文件格式(ISO/IEC 14496-12)。文件由盒子组成。读取器先认品牌,再靠moov里的表把每一帧从mdat里捞出来。

2.1 盒子

每个盒子开头是长度和类型:

size 为 0 表示这个盒子一直延伸到文件末尾,常见于还在增长的mdat。类型决定后面是子盒子,还是一张表。moov、trak、mdia、minf、stbl、moof、traf里面继续嵌盒子;stts、stsz、mdat的载荷是表或样本,不能再按盒子往下切。

2.2 一个点播文件里的三块

图 2:ftyp 表明品牌,moov 放索引,mdat 里音视频按时间交错

ftyp写出主要品牌和兼容品牌,例如isom、mp42、avc1、iso6。ffprobe对 MP4 和 MOV 经常给出同一个format_name:mov,mp4,m4a,3gp,3g2,mj2,因为它们共用一套解复用器。要区分文件代际,看ftyp的品牌。

moov是整部片子的索引。一条轨道一个trak。和定位直接相关的表都在stbl里:

mdat 里是样本本体。点播文件会把音视频按时间交错写进去。整段视频在前、整段音频在后时,即使moov已经在文件头,前面一段时间也只有画面。

MOV 和 MP4 是同一套盒子。QuickTime 文件的品牌常为 qt。很多"mov 转 mp4"只改品牌和样本描述,码流可以原样拷贝。

2.3 用这些表定位一帧

设视频轨 timescale 为 90000,stts 里每帧时长 3000,也就是 30 帧每秒。要播第 10 秒:

(1). 媒体时间 = 10 × 90000 = 900000。

(2). 样本序号 = 900000 / 3000 = 300。

(3). 在 stss 里取不大于 300 的最后一个同步样本。GOP 为 30 帧且第 0 帧是同步样本时,300 本身就能独立解开

(4). 用 stsc 查出它落在哪个 chunk、是这个 chunk 里的第几帧。

(5). stco 给出该 chunk 的文件偏移,stsz 把同 chunk 里前面几帧的字节数加起来。

文件位置 = chunk 偏移 + 帧内偏移。播放器由此只读这一帧,不必扫描整个 mdat。

stts 推进的是解码时间。有 B 帧时,显示顺序和解码顺序不一致,ctts 把显示时刻从解码时刻上挪开。忽略 ctts,B 帧会画在错误的时间上。ctts 的 version 1 允许负偏移;只认 version 0 的旧实现,遇到负值会把显示时间算错。

拖动还要落在真正的同步样本上。H.264 里这通常是 IDR。开放 GOP 中如果把普通 I 帧写进 stss,拖过去要等下一帧真正的随机访问点,中间会花。

2.4 moov 在头,还是在尾

录制过程中,总时长和每帧大小要等写完才知道,所以很多文件先写 mdat,把 moov 留在末尾。这样的文件做渐进下载时,播放器要先拿到文件尾才能开播。

点播成品把 moov 放在 ftyp 后面:

ffmpeg -i input.ts -c copy -movflags +faststart out.mp4

+faststart需要可以回头写的输出文件。它在写完后把moov挪到前面,并修正stco/co64,因为mdat的位置变了。管道、边传边写这类不能回头改的输出,用下一节的分片 MP4。

时长也有版本问题。mvhdversion 0 的时长是 32 位。timescale 取 90000 时,大约 13.3 小时就会溢出。更长的录像要用 version 1 的 64 位时长。单文件超过 4GB 时,偏移改走co64。

2.5 分片 MP4

图 3:点播把一个 moov 放在文件头;直播和切片用初始化段加重复的 moof/mdat

分片文件分成两段职责:

  • 初始化段:ftyp+moov+mvex。这里有编码参数和轨道默认值,没有每帧索引。
  • 媒体分片:moof+mdat。moof里的tfdt给出这个分片在时间轴上的起点,trun给出分片内每帧的时长、大小和是否关键帧。

因此后一个分片不依赖前一个分片的样本表。直播可以一直追加;播放器从任意一个以关键帧开头的分片切入。sidx是可选的段索引,用来按字节范围请求一个大文件里的某一段。default_base_moof让trun的数据偏移相对moof本身计算,输出端还不知道最终文件偏移时也能写。

DASH 的主流封装、新一代 HLS,以及 CMAF(ISO/IEC 23000-19),都使用这套结构。HLS 用#EXT-X-MAP指向初始化段。CMAF 把约束收紧,让 DASH 和 HLS 共用同一套分片。

生成可追加的分片文件:

ffmpeg -i input.ts -c copy -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof out.mp4

frag_keyframe在关键帧处切开。转封装造不出新的关键帧,分片能不能独立解码,取决于原来的 GOP。希望 2 秒一切并且每段都能单独起播,编码时就要让关键帧间隔落在 2 秒以内。

本地点播仍优先用带头moov的整文件。分片文件适合边写边播、CDN 切片和中途加入。

2.6 参数集和 NAL 的两种形态

H.264 在 TS 里常见形态是 Annex-B:NAL 前面有00 00 01起始码,SPS/PPS 周期性地出现在 IDR 之前,方便中途加入。

在 MP4 的mdat里,以及 FLV 的 AVC NAL Tag 里,常见形态是长度前缀。每一条 NAL 前面是长度字段。MP4 把长度占几个字节写在avcC的lengthSizeMinusOne里,通常是 4 字节;FLV 的序列头用同一份AVCDecoderConfigurationRecord。SPS/PPS 放在参数集里,不在每帧里重复。只把mdat抠出来当裸流播,解码器没有参数集。

H.265 同样有两套习惯:hvc1把 VPS/SPS/PPS 放在hvcC;hev1允许参数集同时出现在带内。分片直播如果中途会换参数集,要确认初始化段和分片里的约定一致。

三、MPEG-TS:188 字节的传送流

MPEG-TS(ISO/IEC 13818-1)没有文件级样本表。它用固定长度的包往前送,节目结构靠周期性重复的表来宣布。接收端丢了几个包,还能从下一个同步字节重新对齐。

3.1 包头只有 4 字节

图 4:同步字节 0x47 对齐包,PAT 指出 PMT,PMT 指出各路 PES

标准包长 188 字节。包头字段:

只含适配字段、没有载荷的包不增加连续性计数。同一 PID 上计数保持不变,多半是重复包;计数跳变,说明中间丢了包。

适配字段里可以放 PCR。PCR 是 27 MHz 时钟:33 位基值按 90 kHz 走,9 位扩展把精度补到 27 MHz。规范要求相邻 PCR 的间隔不超过 100 ms,广播里常见大约 40 ms。

实际文件不一定是 188 一步:

  • 标准 TS:188 字节。
  • 一部分 DVB 流在每个包后面带 16 字节校验,步长 204。
  • 蓝光m2ts在每个包前面有 4 字节到达时间,步长 192。

自己写解析时,从0x47向后看下一次0x47的间距。连续几个包都落在 188、192 或 204 上,步长就确定了。

3.2 PAT、PMT 和 PES

PID 0 上是 PAT。它把节目号映射到该节目 PMT 的 PID。PMT 再宣布两件事:PCR 在哪一路,以及每路基本流的stream_type和 PID。这些表会周期重发,所以中途加入的播放器不用从文件头读起。

常见stream_type:

PES 才装基本流。一个 PES 跨多个 TS 包,只有起点那个包把 PUSI 置 1。PES 头里带 33 位的 PTS,需要时再带 DTS,单位都是 90 kHz。视频在 TS 里多为 Annex-B。AAC 若是0x0F,帧外还有 ADTS 头。

直播推流大多是单节目 TS:PAT 里一套节目。广播复用器把多套节目打进一路,PAT 里有多条节目号,这种叫多节目 TS。统计码率时要单独看空包,空包的 PID 是0x1FFF,用来把变码率填成恒定码率。把空包算进"视频码率",会和编码器输出对不上。

3.3 PCR 管时钟,PTS 管显示

PCR 告诉解码器"现在几点"。PTS 告诉这一帧"该在几点显示"。解码器把本地时钟锁到 PCR 上,等到时钟追上 PTS,再把这帧送去显示。DTS 告诉解码器何时解码。有 B 帧时 PTS 和 DTS 不同,解码顺序和显示顺序就靠这两个值拉开。

两个时间都要有。只有 PTS、PCR 长期不来,时钟会漂,音画逐渐分开。把 PCR 直接当成显示时间,也会和 PTS 对不齐。

PTS 是 33 位。90 kHz 下大约 26.5 小时回绕一次。长时间录像要把回绕接回连续时间轴,不能把它当成时间倒流。HLS 在切段处用#EXT-X-DISCONTINUITY明确告诉播放器:下一段的时间轴另起。

3.4 广播和直播里它为什么稳

定长包对复用器友好:丢包后按同步字节重新找到边界,PAT/PMT 过一会儿又会来,不需要一份越写越大的全局索引。代价是随机拖动要扫描,或者靠外面的索引。FFmpeg 打开大 TS 时会扫建索引,所以同样时长的文件,TS 往往比 MP4 开得慢。

传统 HLS 的媒体段就是短 TS。播放列表负责"第几秒在哪个文件",TS 负责这一小段里的时钟和基本流。

ffmpeg -i input.mp4 -c copy -f mpegts out.ts

进 TS 时,H.264 会从长度前缀改回起始码。新版本 FFmpeg 在-c copy时通常自动插入h264_mp4toannexb。

四、FLV、PS、MKV,以及切片协议

4.1 FLV:按 Tag 往下写

FLV 文件以FLV三个字节开头,接着是版本、音视频标志和 9 字节头长,然后是一个值为 0 的 PreviousTagSize。后面是重复的 Tag:

视频 CodecID 为 7 表示 AVC。AVC 包再分序列头和 NAL 包:序列头里是AVCDecoderConfigurationRecord,和 MP4 的avcC是同一份参数;NAL 包里还可以带 CompositionTime,用来放 B 帧的显示偏移。音频格式 10 表示 AAC,同样先来序列头(AudioSpecificConfig),再来裸 AAC 帧。

脚本 Tag 里的onMetaData可以带关键帧时间表。点播 FLV 靠这张表拖动。直播可以没有它,因为本来就不往回 seek。

RTMP 的音视频消息和 FLV Tag 同构,消息类型也是 8、9、18。所以 RTMP 推上来的流,落地时可以直接存成 FLV。HTTP-FLV 则是一条不结束的 HTTP 响应,响应体是一个不断增长的 FLV:开头几个 Tag 交出 AAC/AVC 序列头,之后按 Tag 往下播。常见延迟在 1 到 3 秒,适合要接现有 RTMP 推流、又希望浏览器用 HTTP 来拉的直播。

32 位毫秒时间戳大约 49.7 天回绕一次。超长推流要处理这个回绕。Tag 的 StreamID 固定为 0,格式里只有一路视频和一路音频的位置。多音轨和软字幕要换 MP4 或 MKV。

4.2 MPEG-PS:国标监控里很常见

节目流(PS)也来自 ISO/IEC 13818-1,包长可变。包头起始码是00 00 01 BA,节目映射在 PSM(00 00 01 BC),视频 PES 的 stream_id 常为0xE0,音频常为0xC0。DVD 的 VOB 就是 PS。

国标监控的实时预览,RTP 负载里经常是 PS:设备用 PSM 宣布视频和音频。平台级联和录像里也能见到 TS。收流程序要按起始码认壳。拿 TS 的0x47、按 188 去切 PS,会对不齐。

PS 适合可靠链路上的存储和单路传输。包长不固定,丢一个包之后,边界不如 TS 好找。

4.3 MKV 与 WebM

Matroska 用 EBML 编码。媒体放在 Cluster 里,每个 Cluster 有自己的时间码;Cues 把时间映射到 Cluster 的文件位置,粒度通常是一块。章节、多音轨、软字幕和附件都可以放进同一个文件,适合归档和后期工程。

WebM 是收紧后的子集:视频 VP8、VP9 或 AV1,音频 Vorbis 或 Opus。浏览器里的 Media Source Extensions 既能接分片 MP4,也能接 WebM。直播时 Cluster 可以往外推,Cues 往往要等收尾才完整,所以更常用来保存录制结果。广播复用仍走 TS。

4.4 MOV 与 AVI

MOV 已在第二节说过,和 MP4 同源。AVI 是 RIFF,索引用idx1。它能装老的编码,现代的 H.265 和可变帧率都很别扭。新录像不必再选 AVI。

4.5 HLS、DASH、CMAF 是清单加切片

这三者和容器的关系是:

  • HLS:m3u8列出每一段的时长和地址。传统媒体段是 TS;使用分片 MP4 时,用#EXT-X-MAP给出初始化段。
  • DASH:MPD 描述 Period、AdaptationSet 和 Representation。媒体段主流是分片 MP4,初始化段单独缓存。
  • CMAF:约束分片 MP4 的写法,使同一组初始化段和媒体段同时给 HLS 和 DASH 用。

传统 HLS 的延迟常见在十几秒到半分钟,主要花在「等满一个切片再公布」。低延迟 HLS 和 CMAF 的分块可以把这个等待压到数秒。再往下的超低延迟,WebRTC 的媒体直接走 RTP,会话结束再录成 WebM 或分片 MP4。

加密也沿这条线分开。MP4 的 CENC 在样本层加密,pssh、tenc、senc仍留在盒子结构里,清单和索引可读。HLS 的 AES-128 则把整个切片文件加密,播放器先按密钥解开,再按 TS 或 fMP4 去解析。广播 TS 使用条件接收表,和文件层的 CENC、AES-128 各走各的。

五、放在一张表里看

六、按场景选

图 5:拖动、收尾、丢包,这三件事决定用哪一种壳

可以收成六条:

(1). 点播成品用带头 moov 的 MP4。手机、浏览器、桌面播放器都认,拖动是一次查表。

(2). 边录边播、多端切片用分片 MP4。DASH 和新一代 HLS 直接吃它;CMAF 让两边共用。

(3). 广播、UDP、传统 HLS 切片用 TS。定长包、周期表、PCR,都是为「中途加入」和「丢了还能接」准备的。

(4). 低延迟 HTTP 直播,上游已经是 RTMP 时,用 FLV / HTTP-FLV。Tag 到了就能播,不必等一个完整切片。

(5). 国标监控先按 PS 解 RTP 负载,用 PSM 认轨道;平台侧若约定的是 TS,再走 PAT/PMT。

(6). 多音轨、软字幕、章节放进 MKV。网页点播仍以 H.264/H.265 + AAC 的 MP4 最稳,Opus 更常见于 WebM 和 WebRTC。

选壳之前先看编码是否被目标普遍接受。H.264 + AAC 在 MP4、TS、FLV 之间拷贝,播放器支持最齐。Opus 进 TS、AC-3 进 MP4,封装可以成功,播放器却不一定解得开。

七、转封装时要守住的几件事

(1). 点播文件把 `moov` 放在头上。 +faststart 会重写偏移。不能回头写的输出改用分片。

(2). 分片从关键帧开始。 -c copy 不会新增 IDR。GOP 是 5 秒,就不要指望切出 2 秒且每段都能独立起播的切片。

(3). 比特流形态会变,压缩数据可以不变。 TS 进 MP4:视频改成长度前缀,AAC 去掉 ADTS,对应的过滤器是 aac_adtstoasc。MP4 进 TS:视频改回起始码,对应 h264_mp4toannexb。新版本在 -c copy 时通常会自动插入。

(4). 有 B 帧就保留显示时间。 MP4 靠 ctts,FLV 靠 CompositionTime,TS 靠 PES 里同时存在的 PTS 和 DTS。只拷解码时间,显示顺序会乱。

(5). 大文件用 `co64`,长时长用 64 位 `mvhd`。 32 位偏移止于 4GB;90000 的 timescale 配 32 位时长,大约 13.3 小时封顶。

(6). 时间戳回绕要接上。 TS 的 PTS 约 26.5 小时回绕,FLV 的毫秒时间戳约 49.7 天回绕。HLS 切段处的不连续要用标签标明。

(7). PCR 和 PTS 各干各的。 PCR 恢复时钟,间隔不超过 100 ms。PTS 决定显示。音画分开时,先看这两路是不是还在同一条时间轴上。

(8). 参数集要能被中途加入的人拿到。 MP4 把它们放在 avcC / hvcC,初始化段必须先到。TS 里则要在 IDR 前周期性重发。

(9). 音轨的编辑列表会挪几十毫秒。 AAC 编码器延迟常见约 2112 个采样,48 kHz 下大约 44 ms。elst 用来把这段延迟从时间轴上补偿掉。忽略它,音画会有一个稳定的小错位。

(10). 先认壳,再切包。 TS 找 0x47 并确认步长是 188、192 还是 204。PS 找 00 00 01 BA。两种起始码不能混用。

八、自己把结构看出来

先看容器名、时长和每路编码:

ffprobe -v error -show_entries format=format_name,duration,bit_rate -show_streams -of json input.mp4

看 TS 前几包的时间戳和关键帧标志。flags里的K表示这个包落在关键帧上:

ffprobe -v error -show_packets -select_streams v:0 -show_entries packet=pts_time,dts_time,flags,pos -read_intervals "%+#5" input.ts

MP4 的盒子层级可以用下面的脚本打印。它按长度跳过mdat,不会把媒体数据读进内存。stsd的载荷以版本和条目数开头,后面才是样本条目。把它放进继续下钻的名单,会把版本号当成盒子。

import struct import sys from pathlib import Path CONTAINERS = { "moov", "trak", "mdia", "minf", "stbl", "dinf", "edts", "mvex", "moof", "traf", } def walk(f, start: int, end: int, depth: int = 0) -> None: pos = start while pos + 8 <= end: f.seek(pos) hdr = f.read(8) if len(hdr) < 8: return size, typ = struct.unpack(">I4s", hdr) try: name = typ.decode("ascii") except UnicodeDecodeError: return if not name.isprintable(): return header = 8 if size == 1: ext = f.read(8) if len(ext) < 8: return size = struct.unpack(">Q", ext)[0] header = 16 elif size == 0: size = end - pos if size < header or pos + size > end: return print(f"{' ' * depth}{name} offset={pos} size={size}") if name in CONTAINERS: walk(f, pos + header, pos + size, depth + 1) pos += size def main() -> None: path = Path(sys.argv[1]) with path.open("rb") as f: f.seek(0, 2) walk(f, 0, f.tell()) if __name__ == "__main__": main()

点播 MP4 上,输出大致是ftyp、moov(里面有trak/stbl/stco)、然后mdat。若moov出现在mdat之后,这个文件还没做 faststart。分片文件则会在初始化用的moov之后,反复出现moof和mdat。

总结

编码负责压缩,容器负责把压缩结果放上时间轴。MP4 用moov换来随机访问,分片之后又能边写边播。TS 用 188 字节的定长包和周期发送的 PAT/PMT 换来丢包后的重新同步,所以广播、UDP 和传统 HLS 都用它。FLV 按 Tag 顺序写,接得住 RTMP 和低延迟 HTTP 直播。国标实时流先认 PS,多轨归档用 MKV。

换壳时优先-c copy,同时改好 NAL 形态、参数集位置和时间戳。关键帧间隔在编码时已经定了,切片和拖动都得顺着它来。

返回列表