6.2 视频封装格式:MP4_格式详解


本文由 简悦 SimpRead 转码, 原文地址 zhuanlan.zhihu.com

1.MP4 格式概述

1.1 简介

MP4 或称 MPEG-4 第 14 部分(MPEG-4 Part 14)是一种标准的数字多媒体容器格式。扩展名为. mp4。虽然被官方标准定义的唯一扩展名是. mp4,但第三方通常会使用各种扩展名来指示文件的内容:

  • 同时拥有音频视频的 MPEG-4 文件通常使用标准扩展名. mp4;
  • 仅有音频的 MPEG-4 文件会使用. m4a 扩展名。

大部分数据可以通过专用数据流嵌入到 MP4 文件中,因此 MP4 文件中包含了一个单独的用于存储流信息的轨道。目前得到广泛支持的编解码器或数据流格式有:

  • 视频格式:H.264/AVC、H.265/HEVC、VP8/9 等
  • 音频格式:AAC、MP3、Opus 等

十年编程老舅:C/C++ 音视频开发学习路线 + 资料

1.2 术语

为了后面能比较规范的了解这种文件格式,这里需要了解下面几个概念和术语,这些概念和术语是理解好 MP4 媒体封装格式和其操作算法的关键。

(1)Box
这个概念起源于 QuickTime 中的 atom,也就是说 MP4 文件就是由一个个 Box 组成的,可以将其理解为一个数据块,它由 Header+Data 组成,Data 可以存储媒体元数据和实际的音视频码流数据。Box 里面可以直接存储数据块但是也可以包含其它类型的 Box,我们把这种 Box 又称为 container box

(2)Sample
简单理解为采样,对于视频可以理解为一帧数据,音频一帧数据就是一段固定时间的音频数据,可以由多个 Sample 数据组成,简而言之:存储媒体数据的单位是 sample。

(3)Track
表示一些 sample 的集合,对于媒体数据而言就是一个视频序列或者音频序列,我们常说的音频轨和视频轨可以对照到这个概念上。当然除了 Video Track 和 Audio Track 还可以有非媒体数据,比如 Hint Track,这种类型的 Track 就不包含媒体数据,可以包含一些将其他数据打包成媒体数据的指示信息或者字幕信息。简单来说:Track 就是电影中可以独立操作的媒体单位。

(4)Chunk
一个 track 的连续几个 sample 组成的单元被称为 chunk,每个 chunk 在文件中有一个偏移量,整个偏移量从文件头算起,在这个 chunk 内,sample 是连续存储的。
这样就可以理解为 MP4 文件里面有多个 Track, 一个 Track 又是由多个 Chunk 组成,每个 Chunk 里面包含着一组连续的 Sample,正是因为定义了上述几个概念,MP4 这种封装格式才容易实现灵活、高效、开放的特性,所以要仔细理解。

2.MP4 整体结构

2.1 MP4 结构概览

MP4 格式是一个 box 的格式,box 容器套 box 子容器,box 子容器再套 box 子容器。

一个 box 由两部分组成:box header、box body。

  • box header:box 的元数据,比如 box type、box size。
  • box body:box 的数据部分,实际存储的内容跟 box 类型有关,比如 mdat 中 body 部分存储的媒体数据。
    box header 中,只有 type、size 是必选字段。当 size==1 时,存在 largesize 字段。如果 size==0,表示该 box 为文件的最后一个 box。在部分 box 中,还存在 version、flags 字段,这样的 box 叫做 Full Box。当 box body 中嵌套其他 box 时,这样的 box 叫做 container box。
    mp4box 图示如下:

  • 其中:
  • ftyp(file type box):文件头,记录一些兼容性信息
  • moov(movie box):记录媒体信息
  • mdat(media data):媒体负载

完整的 Box 结构:

每个 Box 承载的数据内容如下:

2.2 Box 结构

mp4 封装格式采用称为 box 的结构来组织数据。结构如下:

+-+-+-+-+-+-+-+-+-+-+
  |  header  |  body  |
  +-+-+-+-+-+-+-+-+-+-+

其它所有 box 都在语法上继承自此基本 box 结构。

2.2.1 box header

box 分为普通 box 和 fullbox。
(1)普通 box header 结构如下:

字段类型描述
size4 Bytes包含 box header 的整个 box 的大小
type4 Bytes4 个 ascii 值,如果是 "uuid",则表示此 box 为用户自定义,可忽略
large size8 Bytessize=1 时才有的字段,用于扩展,例如 mdat box 会需要此字段

(2)fullbox 在上面的基础上新增了 2 个字段:

字段类型描述
version1 Bytes版本号
flags3 Bytes标识

2.2.2 box body

一个 box 可能会包含其它多个 box,此种 box 称为 container box。因此 box body 可能是一种具体 box 类型,也有可能是其它 box。
虽然 Box 的类型非常多,大概有 70 多种,但是并不是都是必须的,一般的 MP4 文件都是含有必须的 Box 和个别非必须 Box,我用 MP4info 这种工具分析了一首 MP4 的文件,具体的 Box 显示如下:

通过上述工具分析出来的结果,我们大概可以总结出 MP4 以下几个特点:

  1. MP4 文件就是由一个个 Box 组成,其中 Box 还可以相互嵌套,排列紧凑没有多的冗余数据;
  2. Box 类型并没有很多,主要是由必须的 ftyp、moov、mdat 组成,还有 free,udta 非必须 box 组成即去掉这两种 box 对于播放音视频也没有啥影响。
  3. Moov 一般存储媒体元数据,比较复杂嵌套层次比较深,后面会详细解释各个 box 的字段含义和组成。

2.3 ftyp(File Type Box)

ftyp 一般出现在文件的开头,用来指示该 mp4 文件使用的标准规范:

字段类型描述
major_brand4 bytes主版本
minor_version4 bytes次版本
compatible_brands[]4 bytes指定兼容的版本,注意此字段是一个 list,即可以包含多个 4 bytes 版本号

一个示例如下:

2.4 moov(Movie Box)

  1. moov 是一个 container box,一个文件只有一个,其包含的所有 box 用于描述媒体信息(metadata)。
  2. moov 的位置可以紧随着 ftyp 出现,也可以出现在文件末尾。
  3. 由于是一个 container box,所以除了 box header,其 box body 就是其它的 box。

子 Box:

  • mvhd(moov header):用于简单描述一些所有媒体共享的信息。
  • trak:即 track,轨道,用于描述音频流或视频流信息,可以有多个轨道,如上出现了 2 次,分别表示一路音频和一路视频流。
  • udta(user data):用户自定义,可忽略。

一个示例如下:
(1)结构

(2)数据

(3)成分

子 Box:mvhd
用于简单描述一些所有媒体共享的信息。

子 Box:trak
track,轨道,用于描述音频流或视频流信息,可以有多个轨道,如上出现了 2 次,分别表示一路音频和一路视频流。

2.5 mvhd(Movie Header Box)

mvhd 作为媒体信息的 header 出现 (注意此 header 不是 box header,而是 moov 媒体信息的 header),用于描述一些所有媒体共享的基本信息。
mvhd 语法继承自 fullbox,注意下述示例出现的 version 和 flags 字段属于 fullbox header。
Box Body:

2.6 trak(track)

  1. trak box 是一个 container box,其子 box 包含了该 track 的媒体信息。
  2. 一个 mp4 文件可以包含多个 track,track 之间是独立的,trak box 用于描述每一路媒体流。
  3. 一般情况下有两个 trak,分别对应音频流和视频流。

十年编程老舅:C/C++ 音视频开发学习路线 + 资料

一个示例如下:

其中:

  • tkhd(track header box):用于简单描述该路媒体流的信息,如时长,宽度等。
  • mdia(media box):用于详细描述该路媒体流的信息
  • edts(edit Box):子 Box 为 elst(Edit List Box),它的作用是使某个 track 的时间戳产生偏移。

2.7 tkhd(track header box)

  1. tkhd 作为媒体信息的 header 出现 (注意此 header 不是 box header,而是 track 媒体信息的 header),用于描述一些该 track 的基本信息。
  2. tkhd 语法继承自 fullbox,注意下述示例出现的 version 和 flags 字段属于 fullbox header。
    Box Body:

2.8 edts(edit Box)

它下边有一个 elst(Edit List Box),它的作用是使某个 track 的时间戳产生偏移。看一下一些字段:

  • segment_duration: 表示该 edit 段的时长,以 Movie Header Box(mvhd)中的 timescale 为单位, 即 segment_duration/timescale = 实际时长(单位 s)
  • media_time: 表示该 edit 段的起始时间,以 track 中 Media Header Box(mdhd)中的 timescale 为单位。如果值为 - 1(FFFFFF),表示是空 edit,一个 track 中最后一个 edit 不能为空。
  • media_rate: edit 段的速率为 0 的话,edit 段相当于一个”dwell”,即画面停止。画面会在 media_time 点上停止 segment_duration 时间。否则这个值始终为 1。
  • 需要注意的问题:

为使 PTS 从 0 开始,media_time 字段一般设置为第一个 CTTS 的值,计算 PTS 和 DTS 的时候,他们分别都减去 media_time 字段的值就可以将 PTS 调整为从 0 开始的值。
如果 media_time 是从一个比较大的值,则表示要求 PTS 值大于该值时画面才进行显示,这时应该将第一个大于或等于该值的 PTS 设置为 0,其他的 PTS 和 DTS 也相应做调整。
2.9 mdia(media box)

  1. 定义了 track 媒体类型以及 sample 数据,描述 sample 信息。
  2. 它是一个 container box,它必须包含 mdhd,hdlr 和 minf。

一个示例如下:

其中:

  • mdhd(Media Header Box):用于简单描述该路媒体流的信息。
  • hdlr(Handler Reference Box):主要定义了 track 类型。
  • stbl(Media Information Box):用于描述该路媒体流的解码相关信息和音视频位置等信息。

2.10 mdhd(Media Header Box)

  1. mdhd 作为媒体信息的 header 出现 (注意此 header 不是 box header,而是 media 媒体信息的 header),用于描述一些该 media 的基本信息。
  2. mdhd 和 tkhd ,内容大致都是一样的。不过 tkhd 通常是对指定的 track 设定相关属性和内容。而 mdhd 是针对于独立的 media 来设置的。
  3. mdhd 语法继承自 fullbox,注意下述示例出现的 version 和 flags 字段属于 fullbox header。
    Box Body:

注:timescale 同 mvhd 中的 timescale,但是需要注意虽然意义相同,但是值有可能不同,下边 stts,ctts 等时间戳的计算都是以 mdhd 中的 timescale。

2.11 hdlr(Handler Reference Box)

  1. 主要解释了媒体的播放过程信息。声明当前 track 的类型,以及对应的处理器(handler)。
  2. hdlr 语法继承自 fullbox,注意下述示例出现的 version 和 flags 字段属于 fullbox header。
    Box Body:

2.12 minf(Media Information box)

  1. 解释 track 媒体数据的 handler-specific 信息,media handler 用这些信息将媒体时间映射到媒体数据并进行处理。minf 同样是个 container box,其内部需要关注的内容是 stbl,这也是 moov 中最复杂的部分。
  2. 一般情况下,“minf”包含一个 header box,一个 “dinf” 和一个 “stbl”,其中,header box 根据 track type(即 media handler type)分为“vmhd”、“smhd”、“hmhd” 和“nmhd”,“dinf”为 data information box,“stbl”为 sample table box。

2.13 *mhd (Media Info Header Box)

可分为 “vmhd”、“smhd”、“hmhd” 和“nmhd”,比如视频类型则为 vmhd,音频类型为 smhd。
(1)vmhd

  • graphics mode:视频合成模式,为 0 时拷贝原始图像,否则与 opcolor 进行合成。
  • opcolor:一组 (red,green,blue),graphics modes 使用。
    (2)smhd
  • balance:立体声平衡,[8.8] 格式值,一般为 0 表示中间,-1.0 表示全部左声道,1.0 表示全部右声道。

2.14 dinf(Data Information Box)

  1. 描述了如何定位媒体信息,是一个 container box。
  2. “dinf” 一般包含一个 “dref”(data reference box)
  3. “dref”下会包含若干个 “url” 或“urn”,这些 box 组成一个表,用来定位 track 数据。简单的说,track 可以被分成若干段,每一段都可以根据 “url” 或“urn”指向的地址来获取数据,sample 描述中会用这些片段的序号将这些片段组成一个完整的 track。一般情况下,当数据被完全包含在文件中时,“url”或 “urn” 中的定位字符串是空的。

2.15 stbl(Sample Table Box)

在介绍 stbl box 之前,需要先介绍一下 mp4 中定义的 sample 与 chunk:

  • sample:ISO/IEC 14496-12 中定义 samples 之间不能共享同一个时间戳,因此,在音视频 track 中,一个 sample 代表一个视频或音频帧。
  • chunk:多个 sample 的集合,实际上音视频 track 中,chunk 与 sample 一一对应。

stbl box 是一个 container box,是整个 track 中最重要的一个 box,其子 box 描述了该路媒体流的解码相关信息、音视频位置信息、时间戳信息等。

MP4 文件的媒体数据部分在 mdat box 里,而 stbl 则包含了这些媒体数据的索引以及时间信息。
一个示例如下:

其中:

  • stsd(sample description box):存储了编码类型和初始化解码器需要的信息,并与具体编解码器类型有关。
  • stts(time to sample box):存储了该 track 每个 sample 到 dts 的时间映射关系。
  • stss(sync sample box):针对视频 track,关键帧所属 sample 的序号。
  • ctts(composition time to sample box):存储了该 track 中,每个 sample 的 cts 与 dts 的时间差。
  • stsc/stz2(sample to chunk box):存储了该 track 中每个 sample 与 chunk 的映射关系。
  • stsz(sample size box):存储了该 track 中每个 sample 的字节大小。
  • stco/co64(chunk offset box):存储了该 track 中每个 chunk 在文件中的偏移。

2.16 stsd(sample description box)

主要存储了编码类型和初始化解码器需要的信息。这里以视频为例,包含子 box:avc1,表示是 H264 的视频。

2.16.1 h264 stsd

对于 h264 视频,典型结构如下:

其上 (只列出 avc1 与 avcC,其余 box 可忽略):

  • avc1,是 avc/h264/mpeg-4 part 10 视频编解码格式的代称,是一个 container box,但是 box body 也携带自身的信息。
    Box Body:

avcC(AVC Video Stream Definition Box),存储 sps && pps,即在 ISO/IEC 14496-15 中定义的 AVCDecoderConfigurationRecord 结构

注:在 srs 中,解析 avcc/AVCDecoderConfigurationRecord 结构解析参见 srs/trunk/src/kernel/srs_kerner_codec.cpp::SrsFormat::avc_demux_sps_pps() 函数。

2.16.2 aac stsd

对于 aac 音频,典型结构如下:

可以看到,aac stsd 结构比较复杂,box 众多。实际上,在 ISO/IEC 14496-3 中,定义了 AudioSpecificConfig 类型,aac stsd 结构主要信息就来自 AudioSpecificConfig。
具体不做分析,可以参看 srs 中:

  • 解析 AudioSpecificConfig 结构的 srs/trunk/src/kernel/srs_kerner_codec.cpp::SrsFormat::audio_aac_sequence_header_demux() 函数
  • 封装 aac stsd 结构的 srs/trunk/src/kernel/srs_kernel_mp4.cpp::SrsMp4Encoder::flush() 函数

2.17 stts(time to sample box)

  1. 存储了该 track 每个 sample 到 dts 的时间映射关系。
  2. 包含了一个压缩版本的表,通过这个表可以从解码时间映射到 sample 序号。表中的每一项是连续相同的编码时间增量 (Decode Delta) 的个数和编码时间增量。通过把时间增量累加就可以建立一个完整的 time to sample 表。

这里为了节约条目的个数,采用了压缩存储的方式,即 sample_count 个连续的 sample 如果 sample_delta 时长一样,那么用一个条目就能表示了。
一个音频 track 的示例如下:

一个视频 track 的示例如下:

2.18 ctts(composition time to sample box)

  1. 存储了该 track 中,每个 sample 的 pts 与 dts 时间差 (cts = pts - dts):
  2. 如果一个视频只有 I 帧和 P 帧,则 ctts 这个表就不需要了,因为解码顺序和显示顺序是一致的,但是如果视频中存在 B 帧,则需要 ctts。

注意:

  • 此 box 在 dts 和 pts 不一样的情况下必须存在,如果一样,不用包含此 box。
  • 如果 box 的 version=0,意味着所有 sample 都满足 pts >= dts,因而差值用一个无符号的数字表示。只要存在一个 pts < dts,那么必须使用 version=1、有符号差值来表示。
  • 关于 ctts 的生成,可以参看 srs/trunk/src/kernel/srs_kernel_mp4.cpp::SrsMp4SampleManager::write_track() 函数 pts、dts、cts 满足公式:pts - dts = cts。

2.19 stss(sync sample box)

它包含 media 中的关键帧的 sample 表。关键帧是为了支持随机访问。如果此表不存在,说明每一个 sample 都是一个关键帧。

一个视频示例如下:

2.20 stsc/stz2(sample to chunk box)

存储了该 track 中每个 sample 与 chunk 的映射关系。

一个音频示例如下:

  • 第一组 chunk 的 first_chunk 序号为 1,每个 chunk 的 sample 个数为 1,因为第二组 chunk 的 first_chunk 序号为 2,可知第一组 chunk 中只有一个 chunk。
  • 第二组 chunk 的 first_chunk 序号为 2,每个 chunk 的 sample 个数为 2,因为第三组 chunk 的 first_chunk 序号为 24,可知第二组 chunk 中有 22 个 chunk,有 44 个 sample。
  • 这个并不是说,这个视频流只有 3 个 sample,也就是只有 3 帧,不可能的,而是第三,第四行省略了,也就是说,第三跟第四,等等,后面的 chunk 里面都只有 1 个 sample,跟第二个 chunk 一样。本视频流有 239 个 chunk。因为本视频流一共 240 帧,第一个 chunk 里面有 2 帧,后面的都是 1 帧,所以计算出来只有 239 个 chunk。

2.21 stsz(sample size box)

包含 sample 的数量和每个 sample 的字节大小,这个 box 相对来说体积比较大的。表明视频帧或者音频帧大小,FFmpeg 里面的 AVPacket 的 size 数据大小,就是从这个 box 中来的。

2.22 stco/co64(chunk offset box)

  1. Chunk Offset 表存储了每个 chunk 在文件中的位置,这样就可以直接在文件中找到媒体数据,而不用解析 box。
  2. 需要注意的是一旦前面的 box 有了任何改变,这张表都要重新建立。

stco 有两种形式,如果你的视频过大的话,就有可能造成 chunkoffset 超过 32bit 的限制。所以,这里针对大 Video 额外创建了一个 co64 的 Box。它的功效等价于 stco,也是用来表示 sample 在 mdat box 中的位置。只是,里面 chunk_offset 是 64bit 的。

  • 需要注意,这里 stco 只是指定的每个 Chunk 在文件中的偏移位置,并没有给出每个 Sample 在文件中的偏移。想要获得每个 Sample 的偏移位置,需要结合 Sample Size box 和 Sample-To-Chunk 计算后取得。

2.23 udta(user data box)

用户自定义数据。

2.24 free(free space box)

  1. “free” 中的内容是无关紧要的,可以被忽略。该 box 被删除后,不会对播放产生任何影响。
  2. Ftyp 可以是 free 或 skip。

2.25 mdat(media data box)

  1. mdat 就是具体的编码后的数据。
  2. mdat 也是一个 box,拥有 box header 和 box body。
  3. mdat 可以引用外部的数据,参见 moov –> udta –> meta,这里不讨论,只讨论数据存储在本文件中的形式。
  4. 对于 box body 部分,采用一个一个 samples 的形式进行存储,即一个一个音频帧或视频帧的形式进行存储。
  5. 码流组织方式采用 avcc 格式,即 AUD + slice size + slice 的形式。

十年编程老舅:C/C++ 音视频开发学习路线 + 资料