1. MP4介绍
MP4算是最常见的一种容器格式,其对应的是MPEG-4标准,是由国际标准化组织(ISO)和国际电工委员会(IEC)下属的“动态图像专家组”(Moving Picture Experts Group,即MPEG)制定的,用于音频、视频信息的压缩编码标准。MPEG-4标准目前分为27个部分,统称为ISO/IEC14496国际标准。
MP4文件格式的基础是ISO BMFF(ISO Base Meida File Format),最初起源于苹果的QuickTime文件格式,后由MPEG进行了开发和标准化定制,主要在MPEG-4标准的第12、14部分。MP4文件格式的内容组织是一种树形结构,其中包含多个子容器,这些子容器被称为"box"或“atom”(在ISO标准中使用“box”称呼,而在苹果的QuickTime文档中使用"atom")。
2. 主要结构
2.1 媒体数据逻辑结构
我们先首先介绍一下MP4标准中的逻辑结构,了解这些逻辑概念有利于我们后续对于box结构的具体字段组织的理解。
- 多媒体文件:也就是整个MP4的容器结构,可以复合音频、视频、字幕等多种数据的容器。
- track:轨道,表示的是一个视频、音频、字幕等的数据集合,它是sample按照内容分类进行划分的结果,对于一个多媒体文件,我们可以看作是由多条的track组成。
- chunk:块,所属一个track的多个连续的sample组成的单元称为一个chunk,chunk的概念包含了存储空间上的意义,所属一个chunk的samples应该是在存储空间上连续的。
- sample:采样(点),与时间相关联的数据,一般对应视频中的一帧,或者音频中的一段压缩的音频,一般而言,同一个track中不可能有两个及以上的sample具有同样的时间戳。
2.2 Box结构
MP4文件的物理结构是由许多个box与fullbox结构嵌套组成。
- Box结构是MP4的基本组成单元。它包括两个部分,header部分和data部分。
其中header部分由size字段和type字段组成,分别各占4个字节。size字段表示的是整个box的长度(以字节为单位),而type字段则是表明box存放内容的标识,一般是4字符的标签,被称为"FourCC"。 - 扩展长度的Box结构,主要是为了描述更大的容器文件,它在Box的基础上使用了8字节的largesize来描述整个box的长度。当Box的size字段值为1时(正常而言不可能出现,因为size字段的长度至少都为4个字节),则其使用largesize的扩展长度字段。
- FullBox结构,主要是在原有的Box结构基础上,增加了1个字节的版本号,以及3个字节的flags标识,而flags标识的具体的意义则由具体的type确定。
- Box嵌套,Box是一种支持嵌套的树型容器结构,如果需要嵌套的子节点,也是一样以Box结构加入到父节点的Data字段中。
几个box字段用法的特殊场景:
- 当size为0时,表示该box是文件的最后一个box。
- 当size为1时,表示需要使用到扩展box结构,后续会使用8个字节的largesize来记录长度。
- 当type为"uuid"时,说明box中存放的是用户自定义的扩展类型。
2.3 嵌套字段结构
2.3.1 字段组织结构
上面我们介绍了MP4数据的组织结构Box,而具体的MP4文件则是由不同的Box结构嵌套组成。下面我们将介绍对于一个本地的MP4文件,具体是由哪些不可或缺的字段构成,以及他们的组织逻辑。
- ftyp:文件类型,表示是当前MP4容器所遵从标准规范的版本。如isom表示该文件遵循ISO/IEC 14496-12标准(MPEG-4 Part 12,即ISO基础媒体文件格式标准)。后续章节会提及到的AVCC格式使用到的“avc1”,“avc3”。
- moov:定义了整个多媒体文件的元数据信息。
- mvhd:全局的头部,定义了整个多媒体文件的属性。最大的作用是定义了MP4全局的时间计量单位timescale。
- trak:定义了多媒体文件中一个轨道(track)的属性。
- tkhd: track header,该条轨道的头部信息,每条track唯一。我们可以获取到每条track的id,持续时间等信息。
- mdia:media,该条track里面的对于媒体数据(mdat)的引用和描述,根据该字段把track信息和具体的data相关联
- mdat:存储了实际的媒体样本(media sample)数据,包括视频帧、音频帧、字幕等原始内容。
更具体一些的字段描述,可以参考下方的脑图,我们可以重点理解一下mdia字段内的stbl,其中的字段描述了sample在文件中的存放位置和形式。
上面那个图每次都不好放大,每次回顾都很麻烦,整理了以下的树状字符图,更好看一点。
┌─ ftyp 文件类型,记录当前MP4的规范版
├─ moov 定义了整个多媒体文件的元数据信息。
│ ├──── mvhd 全局的头部,定义了整个多媒体文件的属性。最大的作用是定义了MP4全局的时间计量单位timescale和持续时间duration。
│ └──── trak 定义量多媒体文件中的一个轨道(track)的信息。
│ ├──── tkhd 定义该条轨道的头部信息,如每个轨道的trackid,持续时间duration,视频宽高,音频音量等。
│ └──── mdia 媒体容器box,保存该条trak对于媒体数据(mdat)的引用和描述,根据该字段把track信息和具体的data相关联
│ ├──── mdhd 描述媒体数据的头部,timescale、duration、语言等
│ ├──── hdlr 描述媒体流的具体类型和处理方式,可以根据该字段确认track的具体类型,如Video、Audio、字幕或其他数据类型
│ └──── minf (media infomation),描述音视频采样数据Sample信息的容器
│ ├──── vmhd 视频信息头部
│ ├──── smhd 音频信息头部
│ ├──── dinf 描述数据信息的容器,承载dref
│ │ └──── dref 数据引用box,定义采样数据Sample的引用方式,如何定位和访问到具体的数据流(如文件中的mdat字段,或外部的url)
│ └──── stbl (Sample Table Box),描述了track中media sample的时间和数据索引。
│ │ 利用stbl可以定位到sample到媒体时间、文件位置的映射关系,确定其类型、大小,以及如何找到紧邻的Sample
│ ├──── stsd 采样描述容器(Sample Description Box),存储了Sample的编解码器参数信息和配置,可以将编码的参数集数据放到该字段下。
│ │ 如编码格式H264/AAC等,编解码器参数信息(SPS/PPS、采样率、声道数),数据的表示方式(色彩空间等)
│ ├──── stco (chunk offset box), 记录了track中的chunk数量,以及每个chunk到文件开头的offset
│ ├──── stsc (sample-to-chunk box),记录了track中每个chunk中的sample的数量
│ ├──── stsz (sample size),记录了track中每个sample的大小,可以通过chunk offset和累积的sample size,具体定位sample在文件中的位置。
│ ├──── stts (sample timestamp),记录量每个sample的持续时间,可以累积映射确认每个sample的dts,以mdhd中定义的timescale为单位
│ └──── ctts (composition time to sample box),记录了smaple的pts和dts之间的偏移量,用于B帧视频的情况
│
├─ mdat (Media Data box),是具体的数据载体,存储了实际的sample数据,包括视频帧、音频帧、字幕帧等数据。保存的数据是raw_data,解析需要根据moov中信息来处理。
└─ meta (MetaData box), 存储视频级别的元数据
2.3.2 stbl字段的应用
下面以Seek操作的原理,来说明stbl字段的应用:
1.根据输入时间,通过stts表,确认seek到的一张dts小于输入时间的目标sample的index。
(一般而言,seek操作是基于pts的,所以有B帧的情况这里的计算时间还需要结合ctts字段来计算出pts,但不论如何,这一步都可以确认具体的Sample index)
2.根据sample index,在stsc中确认得到所属chunk的index。
3.通过stco,确认帧所属chunk的offset。
4.通过stsc,可以确认chunk起始的sample,可以计算得到目标sample之前的sample数量。
5.根据stsz,可以得到chunk中sample之前的每一帧大小,可以得到目标sample到chunk起始的offset。
6.根据chunk_offset和sample_offset,得到目标sample在文件中的偏移量。
7.通过偏移量和smaple size,得到具体的一张sample,这个数据一定是存放在mdat字段中的。
2.3.3 fMP4
上面提及的MP4结构适用于存储和点播场景,但是这种结构由于数据解析单一地依赖moov box,所以在流式传输场景会有一些限制: 1.在数据完全写入前不能完全生成moov,这使得实时录制是不可行的。 2.moov的结构可能太大,在播放前需要下载大量的数据才能开始播放,启播效率低。
所以fMP4的理念被提出,其思路是将一段完整的视频或直播流划分成较小的片段来封装,使用分片来作为播放的单元。
fMP4由一段初始化段和一连串的媒体段组成,初始化段类似于一个未分片文件的开始,由一个ftyp box和一个moov box组成,其中moov box包含额外的信息,表明流是切片的,主要包括一个mvex box结构。fMP4的moov box中只存储量文件级别的媒体数据,比传统的MP4文件的moov box小很多。
而在fMP4的每个分片(fragment)中,都会有一个moof box,其内容类似于moov,但仅包含分片段的信息,以及一个moof对应的mdat段。
fMP4包含:
- 初始化段:【ftyp】【moov【mvex】】,仅包括整个视频维度的数据。
- 分片*N:【styp】【moof】【mdat】。 (styp字段类似于ftyp字段,但仅针对切片)
- mfra box:可选,主要用于提升随机访问(如快进、快退)的性能。需要配置-movflags +mfra。(Dash视频中可以通过mpd来经包含每个分片的开始时间、时长、字节范围等信息,所以也不需要这个字段)
3. 其他
3.1 MP4ToAnnexB
下面提及一下我们在使用ffmpeg时经常会用到的一个bitstream过滤器mp4toAnnexB,它经常和MP4的概念关联起来,实际上是发生H.264视频在MP4容器中的承载实现时。
旧版本的FFmpeg中,将视频文件转换至MPEG-TS或H264、H265格式时,需要手动执行hecv_mp4toannexb/h264_mp4toannexb的bitstream过滤器操作。在新版本中,FFmpeg将这些过滤器操作直接集成到视频数据写入文件过程中,自动处理了。
3.1.1 H264/H265的两种编码存储方式
在封装,传输H264/H265编码的数据时,通常有两种编码存储格式,一是按照H264/H265的参考标准的AnnexB(附录B)的存储格式,一种是使用MP4容器承载时的AVCC格式。
需要注意的是,两种方式的主要区别在于:
- 满足的标准不同。
AnnexB格式是源于H264/H265标准,所以它满足H264/H265裸流使用情况下的需要。 AVCC格式是MP4格式的标准,它描述的是“使用MP4承载H264/H265”的场景。
- 确认编码后的NALU长度的方式不同。
- AVCC格式在MP4中记录了额外的H264/H265编解码器信息
3.1.1.1 AnnexB格式
为了区分每个NALU,一般使用一种预定义的前缀码加在每个NALU前面进行区分,例如,在H264中,加入前缀码0x000001来区分每个NALU。
在加入前缀码时,还需要注意另外一个问题,即每个NALU单元内的有效数据,可能是存在“0x000001”的情况,为了避免这种数据被理解成“新NALU开头”,那么需要将NALU内的0x000001序列进行转移,即加入“防竞争码”0x03,让这部分数据变为0x00000301,在解码时会对数据进行还原。
所以,AnnexB格式具体做了如下的操作:
- 在每个NALU头部插入0x000001的前缀码,区分每一个NALU
- 在NALU内将数据0x000001转义成0x00000301,在解码时还原
因此,AnnexB格式下要计算每个NALU长度,需要从前一个起始码开始,直到下一个起始码开头,计算中间的字节数。
3.1.1.2 AVCC格式
AVCC格式的思路不同,它不需要添加任何的起始码,而是在每个NALU开头加上其长度。
既然AVCC格式不再需要前缀码,自然也没有在NALU内加入“防竞争码”的必要,但实际上为了简化编解码器的工作,使之不需要区分这两种场景,AVCC格式下还是在NALU内使用了0x03的防竞争码对0x000001数据进行转义。
此外,AVCC格式下,还在MP4的avcC box字段(在stsd下)包含解码参数,内置SPS/PPS等关键信息。avcC字段在上节图中描述的stsd字段的avc1/avc3 box下(avc1:每个关键帧前需要重复SPS/PPS,avc3:SPS/PPS只需在avcC中存储一次)。
以下是avcC box字段的内容格式:
| 偏移 | 字节数 | 字段名 | 说明 |
|---|---|---|---|
| 0 | 1 | configurationVersion | 固定值0x01 |
| 1 | 1 | AVCProfileIndication | H.264 Profile(如0x64表示High Profile) |
| 2 | 1 | profile_compatibility | 兼容性标志 |
| 3 | 1 | AVCLevelIndication | H.264 Level(如0x1F表示Level 3.1) |
| 4 | 1 | lengthSizeMinusOne | NALU长度字段字节数-1(通常0x03表示4字节) |
| 5 | 1 | numOfSequenceParameterSets | SPS数量(通常1个) |
| 6 | 2 | sequenceParameterSetLength | 第一个SPS的长度 |
| 8 | N | sequenceParameterSetNALUnit | SPS数据 |
| ... | 1 | numOfPictureParameterSets | PPS数量(通常1个) |
| ... | 2 | pictureParameterSetLength | 第一个PPS的长度 |
| ... | N | pictureParameterSetNALUnit | PPS数据 |
综上,我们可以总结AVCC格式,主要做了以下的处理:
- 在NALU前面直接加入长度
- 在NALU内还是使用了防竞争码0x03
- 引入了额外的全局头部avcC,其中包括SPS,PPS等编解码参数。
3.2 faststart
这里faststart具体指FFmpeg中对于MP4封装格式的控制参数,在命令行中可以通过如下方式进行设置:
ffmpeg -i input.mp4 -movflags faststart -c copy -y output.mp4
一般情况下,FFmpeg生成的MP4文件中的moov box会默认存储在文件的尾部,mdat之后。
当作为本地文件播放时,这样处理没有问题,但当作为流媒体在线播放时,由于播放器需要获取视频的元数据后才能正常播放,而音视频数据以流的形式加载(无法直接加载视频结尾的数据,只能从前向后加载),所以必须等待视频完整加载完后才能播放,而不能边加载边播。
所以,faststart的功能就是将moov box的位置移动到头部,以方便流媒体在线播放等场景使用。
3.3 查询ffmpeg可以配置的参数
ffmpeg对于mp4支持很多具体的参数配置,具体根据需要去查询和配置,记录一下查看的方式。
ffmpeg -h demuxer=mp4 # 解封装
ffmpeg -h muxer=mp4 # 封装
4. 例子
使用工具来查看MP4的结构。在windows上可以使用mp4info,在mac上使用使用"Bento4"工具。
如图使用的是Bento4中的“mp4dump”来查看所有的box的解析数据。