多媒体容器格式(一):MP4

697 阅读14分钟

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 媒体数据逻辑结构

逻辑结构.png 我们先首先介绍一下MP4标准中的逻辑结构,了解这些逻辑概念有利于我们后续对于box结构的具体字段组织的理解。

  • 多媒体文件:也就是整个MP4的容器结构,可以复合音频、视频、字幕等多种数据的容器。
  • track:轨道,表示的是一个视频、音频、字幕等的数据集合,它是sample按照内容分类进行划分的结果,对于一个多媒体文件,我们可以看作是由多条的track组成。
  • chunk:块,所属一个track的多个连续的sample组成的单元称为一个chunk,chunk的概念包含了存储空间上的意义,所属一个chunk的samples应该是在存储空间上连续的。
  • sample:采样(点),与时间相关联的数据,一般对应视频中的一帧,或者音频中的一段压缩的音频,一般而言,同一个track中不可能有两个及以上的sample具有同样的时间戳。

2.2 Box结构

MP4文件的物理结构是由许多个box与fullbox结构嵌套组成。 image.png

  • 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字段用法的特殊场景:

  1. 当size为0时,表示该box是文件的最后一个box。
  2. 当size为1时,表示需要使用到扩展box结构,后续会使用8个字节的largesize来记录长度。
  3. 当type为"uuid"时,说明box中存放的是用户自定义的扩展类型。

2.3 嵌套字段结构

2.3.1 字段组织结构

上面我们介绍了MP4数据的组织结构Box,而具体的MP4文件则是由不同的Box结构嵌套组成。下面我们将介绍对于一个本地的MP4文件,具体是由哪些不可或缺的字段构成,以及他们的组织逻辑。 image.png

  • 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在文件中的存放位置和形式。

image.png

上面那个图每次都不好放大,每次回顾都很麻烦,整理了以下的树状字符图,更好看一点。

┌─ ftyp 文件类型,记录当前MP4的规范版
├─ moov 定义了整个多媒体文件的元数据信息。
│  ├──── mvhd 全局的头部,定义了整个多媒体文件的属性。最大的作用是定义了MP4全局的时间计量单位timescale和持续时间duration。
│  └──── trak 定义量多媒体文件中的一个轨道(track)的信息。
│        ├──── tkhd 定义该条轨道的头部信息,如每个轨道的trackid,持续时间duration,视频宽高,音频音量等。
│        └──── mdia 媒体容器box,保存该条trak对于媒体数据(mdat)的引用和描述,根据该字段把track信息和具体的data相关联
│              ├──── mdhd 描述媒体数据的头部,timescale、duration、语言等
│              ├──── hdlr 描述媒体流的具体类型和处理方式,可以根据该字段确认track的具体类型,如VideoAudio、字幕或其他数据类型
│              └──── 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包含:

  1. 初始化段:【ftyp】【moov【mvex】】,仅包括整个视频维度的数据。
  2. 分片*N:【styp】【moof】【mdat】。 (styp字段类似于ftyp字段,但仅针对切片)
  3. 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格式。

需要注意的是,两种方式的主要区别在于:

  1. 满足的标准不同。

AnnexB格式是源于H264/H265标准,所以它满足H264/H265裸流使用情况下的需要。 AVCC格式是MP4格式的标准,它描述的是“使用MP4承载H264/H265”的场景。

  1. 确认编码后的NALU长度的方式不同。
  2. AVCC格式在MP4中记录了额外的H264/H265编解码器信息
3.1.1.1 AnnexB格式

为了区分每个NALU,一般使用一种预定义的前缀码加在每个NALU前面进行区分,例如,在H264中,加入前缀码0x000001来区分每个NALU。
在加入前缀码时,还需要注意另外一个问题,即每个NALU单元内的有效数据,可能是存在“0x000001”的情况,为了避免这种数据被理解成“新NALU开头”,那么需要将NALU内的0x000001序列进行转移,即加入“防竞争码”0x03,让这部分数据变为0x00000301,在解码时会对数据进行还原。

所以,AnnexB格式具体做了如下的操作:

  1. 在每个NALU头部插入0x000001的前缀码,区分每一个NALU
  2. 在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字段的内容格式:

偏移字节数字段名说明
01configurationVersion固定值0x01
11AVCProfileIndicationH.264 Profile(如0x64表示High Profile)
21profile_compatibility兼容性标志
31AVCLevelIndicationH.264 Level(如0x1F表示Level 3.1)
41lengthSizeMinusOneNALU长度字段字节数-1(通常0x03表示4字节)
51numOfSequenceParameterSetsSPS数量(通常1个)
62sequenceParameterSetLength第一个SPS的长度
8NsequenceParameterSetNALUnitSPS数据
...1numOfPictureParameterSetsPPS数量(通常1个)
...2pictureParameterSetLength第一个PPS的长度
...NpictureParameterSetNALUnitPPS数据

综上,我们可以总结AVCC格式,主要做了以下的处理:

  1. 在NALU前面直接加入长度
  2. 在NALU内还是使用了防竞争码0x03
  3. 引入了额外的全局头部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的解析数据。 image.png

5. 参考资料

  1. 《深入理解FFmpeg》 :刘歧等
  2. Bento4 官网