MP4 格式:最少加载多少数据就能渲染出视频首帧?优化短视频播放体验必须先了解它丨音视频基础

2022-06-13 12:22:15 浏览数 (1)

危地马拉·圣卡塔琳娜皮努拉

(本文基本逻辑:MP4 封装格式概览 → 重要 Box 具体信息介绍 → 实战中对 MP4 Box 信息的使用)

MP4 也称为 MPEG-4 第 14 部分,是继承 MPEG-4 第 12 部分的 ISO 基础媒体文件格式并略作扩展而来,定义于标准 ISO/IEC 14496-14 中,是一种标准的数字多媒体容器格式。

在现在互联网使用的视频中,MP4 是最常见的格式之一,尤其是短视频。如果我们要对短视频的播放体验做优化,了解 MP4 的格式是非常必要的。所以本文我们将介绍一下如下内容:

  • MP4 格式概览
  • File Type Box(ftyp) 介绍
  • Movie Box(moov) 及其重要子 Box 的介绍
  • Media Data Box(mdat) 介绍
  • 实战解析案例:
    • moov 和 MP4 视频的秒开:moov Box 位置对 MP4 秒开的影响。
    • MP4 视频的预加载:最少加载多少数据可以渲染出 MP4 视频首帧。

1、MP4 格式概览

MP4 文件的数据都是封装在一个又一个名为 Box 的单元中。一个 MP4 文件由若干个 Box/FullBox 组成,每个 Box 包含了 Header 和 Data。FullBox 是 Box 的扩展,其包含的 Header 增加了 version(8bits) 和 flags(24bits) 部分。Header 部分包含了 size(32bits) 和 type(32bits) 部分。size 用于描述整个 Box 的长度,type 用于描述 Box 的类型。当 size 为 0 时,表示这是文件中最后一个 Box;当 size 为 1 时,表示 Box 长度需要更多 bits 来描述,这时在后面会定义一个 64bits 的 largesize 来描述 Box 的长度。当 type 是 uuid 时,代表 Box 中的数据是用户自定义扩展类型。

Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) {
    unsigned int(32) size;
    unsigned int(32) type = boxtype;
    if (size==1) {
        unsigned int(64) largesize;
    } else if (size==0) {
        // box extends to end of file
    }
    if (boxtype==‘uuid’) {
        unsigned int(8)[16] usertype = extended_type;
    }
}

FullBox 的数据格式定义:

代码语言:javascript复制
aligned(8) class FullBox(unsigned int(32) boxtype, unsigned int(8) v, bit(24) f) extends Box(boxtype) {
    unsigned int(8) version = v;
    bit(24) flags = f;
}

下图是对 Box/FullBox 结构的描述:

Box 有不同的类型,有着不同的数据结构,Box 中还可以包含其他 Box。Box 的类型详见下表(其中 * 表示当父 Box 存在时,则必须包含该 Box):

在众多类型的 Box 中,最常见的第一层级 Box 有 3 个,分别是 ftypmoovmdat

下面就着重介绍和分析一下这几个 Box 以及他们包含的子 Box。

2、File Type Box(ftyp) 解析

ftyp,即 File Type Box,包含文件的类型、版本、兼容信息等。在一个 MP4 文件中,该 Box 有且只有一个,并且需要尽可能放在文件最开始的位置,除非有必要的固定长度的文件签名信息 Box 可以放在该 Box 前面,其他非固定长度的 Box 数据都必须放在它后面。

ftyp Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class FileTypeBox extends Box(‘ftyp’) {
    unsigned int(32) major_brand;
    unsigned int(32) minor_version;
    unsigned int(32) compatible_brands[]; // to end of the box
}

下面是一个 MP4 文件的 ftyp Box 的 16 进制示例数据:

按照上文所述,前 4 个字节 00 00 00 20 表示该 Box 的 size,即 32 字节。接着 4 个字节 66 74 79 70 是该 Box 的 type,即 ftyp。接下来 4 个字节 69 73 6F 6D 是主 brand,表示该文件所遵循的标准规格,这里是 isom,即遵循 ISO Base Media File Format。接下来的 4 个字节 00 00 02 00 表示的是这个 Box 格式的版本号。接下来的 16 个字节则是兼容的 compatible brands,即该文件兼容的其他标准规格,这里是 isomiso2avc1mp41

虽然这个 Box 是 MP4 文件所必须的,但是通常我们并不太关注这里的信息,所以这里不再多讲。

3、Movie Box(moov) 解析

moov,即 Movie Box,包含文件中所有媒体数据的宏观描述信息。实际的音视频数据都存储在 mdat 中,那么多的数据,我们怎么确定每一帧数据的位置呢,这就需要解析 moov 中的数据来得到实际音视频数据的索引。

moov Box 可以说是 MP4 文件中最重要的 Box,一般播放器的实现都需要读取到 moov 的数据才能开始播放流程。

moov Box 是一个 container box,所以它的数据格式定义比较简单:

代码语言:javascript复制
aligned(8) class MovieBox extends Box(‘moov’){
}

moov 通常包含 1 个 mvhd 和若干个 trak。

3.1、Movie Header Box(moov/mvhd)

mvhd,Movie Header Box,包含与具体媒体数据无关,但与整体播放相关的信息,比如 timescale、duration 等信息。

mvhd Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {
    if (version==1) {
        unsigned int(64) creation_time;
        unsigned int(64) modification_time;
        unsigned int(32) timescale;
        unsigned int(64) duration;
    } else { // version==0
        unsigned int(32) creation_time;
        unsigned int(32) modification_time;
        unsigned int(32) timescale;
        unsigned int(32) duration;
    }
    template int(32) rate = 0x00010000; // typically 1.0
    template int(16) volume = 0x0100; // typically, full volume
    const bit(16) reserved = 0;
    const unsigned int(32)[2] reserved = 0;
    template int(32)[9] matrix = { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
    // Unity matrix
    bit(32)[6] pre_defined = 0;
    unsigned int(32) next_track_ID;
}

下面是一些字段的含义:

  • version:该 Box 的版本号。
  • creation_time:创建时间。
  • modification_time:最新修改时间。
  • timescale:表示 1s 的时长被切割的单元数,它体现了时间的精度,后面表示时长的字段均以 timescale 定义的精度来计量时间。比如,timescale 是 1000,则表示 1s 的时间被切割为 1000 个单元,每个单元即 1/1000s,即 1ms,也就是说该视频的计时精度是毫秒。
  • duration:这个值是从后面 trak 的时长派生来的,表示视频时长,以 timescale 定义的精度计量。比如,timescale 是 1000,duration 是 29782,则表示时长是 29782 * 1 / 1000 秒,即 29.782 秒。
  • rate:表示播放速率,是以 16.16 形式的定点数表示。比如,0x00010000 表示播放速率是 1.0,对应的是正常的播放速率。
  • volume:表示音量,是以 8.8 形式的定点数表示。比如,0x0100 表示音量是 1.0,对应最大音量。
  • matrix:表示视频的图形变换矩阵数据。这里的默认值是 { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
  • next_track_ID:下一条 track 的 ID,这个值不能为 0。一般,对于一个 MP4 文件来说这个值是现有的最大 track ID 加 1。比如,一个 MP4 包含音频和视频两个 track,对应的 ID 分别为 1 和 2,那么 next_track_ID 一般就为 3。

3.2、Track Box(moov/trak)

trak,即 Track Box。一个 trak Box 包含一个单独轨道的信息。每个 trak 都是独立的,包含自己的空域和时域信息。每个 trak 包含一个 tkhd 和 mdia。有两种类型的 trak Box:media track 和 hint track。media track 包含媒体轨道的信息,一个文件至少会包含一个 media track 类型的 Box;hint track 包含用于流媒体协议的打包信息。

trak Box 是一个 container box,所以它的数据格式定义是:

代码语言:javascript复制
aligned(8) class TrackBox extends Box(‘trak’) {
}

3.3、Track Header Box(moov/trak/tkhd)

tkhd,即 Track Header Box,包含该轨道的创建时间、标识该轨道 的 ID、轨道的播放时长、音量、宽高等信息。

tkhd Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class TrackHeaderBox extends FullBox(‘tkhd’, version, flags){
    if (version==1) {
        unsigned int(64) creation_time;
        unsigned int(64) modification_time;
        unsigned int(32) track_ID;
        const unsigned int(32) reserved = 0;
        unsigned int(64) duration;
    } else { // version==0
        unsigned int(32) creation_time;
        unsigned int(32) modification_time;
        unsigned int(32) track_ID;
        const unsigned int(32) reserved = 0;
        unsigned int(32) duration;
    }
    const unsigned int(32)[2] reserved = 0;
    template int(16) layer = 0;
    template int(16) alternate_group = 0;
    template int(16) volume = {if track_is_audio 0x0100 else 0};
    const unsigned int(16) reserved = 0;
    template int(32)[9] matrix= { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
    // unity matrix
    unsigned int(32) width;
    unsigned int(32) height;
}

下面是一些字段的含义:

  • version:该 Box 的版本号。这里是 0 或 1。
  • flags:有这些定义值。
  • creation_time:创建时间。
  • modification_time:最近修改时间。
  • track_ID:当前 track 的 ID。非 0,并且不可重复。
  • duration:当前 track 的时长。使用的 timescale 是来自于 Movie Header Box(mvhd)。
  • layer:视频轨道的前后排序。数值越小则越靠近观看者。通常这个值默认为 0。
  • alternate_group:可替代的备份数据组。如果为 0,表示当前 track 没有备份的 track 数据;非 0,则表示可能存在 group 号相同的 track 数据作为备份。当然一个 group 可能只有一个 track。
  • volume:表示音量,是以 8.8 形式的定点数表示。比如,0x0100 表示音量是 1.0,对应最大音量。只对音频轨道有效。可以多音频轨道组合,并复用 mvhd 的音量;也可以不同的音频轨道设置不同的音量。
  • matrix:表示视频的图形变换矩阵数据。这里的默认值是 { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
  • width & height:表示视频轨道尺寸,各自是以 16.16 形式的定点数表示。这个尺寸不用和图像 sample 的尺寸一致,所有图像 sample 在做矩阵变换前都会缩放处理为该尺寸。

3.4、Media Box(moov/trak/mdia)

mdia,即 Media Box。包含声明当前轨道信息的所有对象。这个 Box 下面包含众多类型的子 Box。

mdia Box 是一个 container box,所以它的数据格式定义是:

代码语言:javascript复制
aligned(8) class MediaBox extends Box(‘mdia’) {
}

3.5、Media Header Box(moov/trak/mdia/mdhd)

mdhd,Media Header Box。

mdhd Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class MediaHeaderBox extends FullBox(‘mdhd’, version, 0) {
    if (version==1) {
        unsigned int(64) creation_time;
        unsigned int(64) modification_time;
        unsigned int(32) timescale;
        unsigned int(64) duration;
    } else { // version==0
        unsigned int(32) creation_time;
        unsigned int(32) modification_time;
        unsigned int(32) timescale;
        unsigned int(32) duration;
    }
    bit(1) pad = 0;
    unsigned int(5)[3] language; // ISO-639-2/T language code
    unsigned int(16) pre_defined = 0;
}

下面是一些字段的含义:

  • version:该 Box 的版本号。这里是 0 或 1。
  • creation_time:创建时间。
  • modification_time:最近修改时间。
  • timescale:表示 1s 的时长被切割的单元数,它体现了时间的精度。
  • duration:表示当前媒体数据时长,以 timescale 定义的精度计量。
  • pad:占位符。
  • language:该字段总长为 15bit,通常是和 pad 字段组合成为 2 字节的长度。
  • pre_defined:无作用,默认 0。

3.6、Handler Reference Box(moov/trak/mdia/hdlr)

hdlr,Handler Reference Box,表示该 track 数据的处理方式,对应的类型包括:Video Track、Audio Track 或者 Hint Track。这个 Box 可以作为 mdia 的子 Box ,也可以作为 meta 的子 Box。

hdlr Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
    unsigned int(32) pre_defined = 0;
    unsigned int(32) handler_type;
    const unsigned int(32)[3] reserved = 0;
    string name;
}

下面是一些字段的含义:

  • handler_type:当作为 mdia 的子 Box 时,该值有下面几种:
    • vide:Video track。
    • soun:Audio track。
    • hint:Hint track。
  • name:这个主要是写名字方便人阅读的。

3.7、Media Information Box(moov/trak/mdia/minf)

minf,即 Media Information Box,包含了描述该 trak 中媒体数据的所有特征信息。其中 minf 下 最重要的 Box 是 stbl。

minf Box 是一个 container box,所以它的数据格式定义是:

代码语言:javascript复制
aligned(8) class MediaInformationBox extends Box(‘minf’) {
}

3.8、Sample Table Box(moov/trak/mdia/minf/stbl)

stbl,即 Sample Table Box,是包含媒体数据信息最多的 Box,也是最复杂的 Box。主要包含了时间和媒体采样数据的索引表,使用这部分数据可以按照时间检索出采样数据的位置、类型(是否 I 帧)、大小、实际偏移位置。

如果当前 track 不包含数据,那么 stbl Box 不需要包含任何子 Box。反之,Sample Description(stsd)、Sample Size(stsz)、Sample To Chunk(stsc)、Chunk Offset(stco) 这些子 Box 是必须包含的。此外,Sync Sample(stss) 这个 Box 虽然是可选的,但也比较重要,它是关键帧的序号表,如果没有这个 Box 则表示所有采样都是关键帧。

stbl Box 是一个 container box,所以它的数据格式定义是:

代码语言:javascript复制
aligned(8) class SampleTableBox extends Box(‘stbl’) {
}

3.9、Sample Description Box(moov/trak/mdia/minf/stbl/stsd)

stsd,即 Sample Description Box,这里主要包含了采样数据的细节信息,包括编码类型以及解码需要的各种初始化数据信息。

stsd 由于需要兼顾 video track、audio track、hint track 各种类型,所以它的数据格式定义比较复杂一些:

代码语言:javascript复制
aligned(8) abstract class SampleEntry (unsigned int(32) format) extends Box(format) {
    const unsigned int(8)[6] reserved = 0;
    unsigned int(16) data_reference_index;
} 

class HintSampleEntry() extends SampleEntry (protocol) {
    unsigned int(8) data [];
}

// Visual Sequences
class VisualSampleEntry(codingname) extends SampleEntry (codingname) {
    unsigned int(16) pre_defined = 0;
    const unsigned int(16) reserved = 0;
    unsigned int(32)[3] pre_defined = 0;
    unsigned int(16) width;
    unsigned int(16) height;
    template unsigned int(32) horizresolution = 0x00480000; // 72 dpi
    template unsigned int(32) vertresolution = 0x00480000; // 72 dpi
    const unsigned int(32) reserved = 0;
    template unsigned int(16) frame_count = 1;
    string[32] compressorname;
    template unsigned int(16) depth = 0x0018;
    int(16) pre_defined = -1;
} 

// Audio Sequences
class AudioSampleEntry(codingname) extends SampleEntry (codingname) {
    const unsigned int(32)[2] reserved = 0;
    template unsigned int(16) channelcount = 2;
    template unsigned int(16) samplesize = 16;
    unsigned int(16) pre_defined = 0;
    const unsigned int(16) reserved = 0 ;
    template unsigned int(32) samplerate = {timescale of media}<<16;
} 

aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type) extends FullBox('stsd', 0, 0){
    int i ;
    unsigned int(32) entry_count;
    for (i = 1 ; i <= entry_count ; i  ){
        switch (handler_type){
            case ‘soun’: // for audio tracks
                AudioSampleEntry();
                break;
            case ‘vide’: // for video tracks
                VisualSampleEntry();
                break;
            case ‘hint’: // Hint track
                HintSampleEntry();
                break;
        }
    }
}

一些字段的含义:

  • entry_count:下面条目表中条目的数量。
  • SampleEntry:对应的 sample 条目。
  • data_reference_index:利用这个索引可以检索与当前 sample description 关联的数据。数据引用存储在 Data Reference Box(dref)。
  • ChannelCount:音频通道数,值为 1(mono,单声道) 或 2(stereo,立体音)。
  • SampleSize:音频采样大小,默认大小是 16bits。
  • SampleRate:音频采样率,以 16.16 定点数。通常是 44100.0、48000.0 等。
  • resolution:图像分辨率,以 16.16 定点数表示。单位是 pixels-per-inch。
  • frame_count:每个 sample 包含的帧数。一般是 1 个 sample 对应 1 帧。
  • Compressorname:一个用于信息展示的名字。
  • depth:图像位深。比如,0x0018 表示不带 alpha 值的颜色位深。
  • width & height:当前 sample 的最大尺寸,单位是像素。

下面是两个示例数据:

视频 track 对应的 stsd Box:

我们看到上面还在 avcC 条目里包含了视频的 SPS 和 PPS 的信息,这些都是视频解码需要的信息。SPS 和 PPS 是 H.264 流中的元信息,在 MP4 文件中单独存放在 avcC 中。转换的时候,还需要将 SPS 和 PPS 提取出来,添加上 0x00000001,放在 H.264 视频流的开始位置。

对于 H.265,其元信息在 hvcC 类型 Box 中。

音频 track 对应的 stsd Box:

3.10、Time To Sample Box(moov/trak/mdia/minf/stbl/stts(ctts))

Time To Sample Box 分为 Decoding Time To Sample Box(stts) 和 Composition Time To Sample Box(ctts) 两种。

stts 包含的信息是相邻两帧的解码间隔时间。

stts Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class TimeToSampleBox extends FullBox(’stts’, version = 0, 0) {
    unsigned int(32) entry_count;
    int i;
    for (i = 0; i < entry_count; i  ) {
        unsigned int(32) sample_count;
        unsigned int(32) sample_delta;
    }
}

一些字段的含义:

  • entry_count:记录条目的数量。
  • sample_count:记录连续相同 delta 的条目数量。
  • sample_delta:记录以 timescale 为精度的时间长度。

ctts 包含的信息是 decoding time 和 composition time 的差值。

ctts Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, version = 0, 0) {
    unsigned int(32) entry_count;
    int i;
    for (i=0; i < entry_count; i  ) {
        unsigned int(32) sample_count;
        unsigned int(32) sample_offset;
    }
}
  • entry_count:记录条目的数量。
  • sample_count:记录连续相同 offset 的条目数量。
  • sample_offset:记录以 timescale 为精度的时间长度。

下面是一个示例:

表中有一序列的 I、P、B 帧,他们是按照解码时间排列的。表中给出了各帧的 Decoding Time(DT) 和 Composition Time(CT),并据此计算出了对应的 Decode delta 和 Composition offset。

下面则是与上面对应的,在 stts 存储的 Decode delta 的信息。因为,连续的 14 个 sample 的 Decode delta 都是 10,所以一条数据即可记录。DT 和 stts 表数据的计算公式:DT(n 1) = DT(n) STTS(n)

下面是对应的在 ctts 存储的 Composition offset 的信息。CT、DT 和 ctts 表数据的计算公式:CT(n) = DT(n) CTTS(n)

Time To Sample Box 中记录的时间信息,通常可以用来通过时间做 seek 操作。

3.11、Sample Size Box(moov/trak/mdia/minf/stbl/stsz)

stsz,即 Sample Size Boxe,包含每个 Sample 的大小。

stsz Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class SampleSizeBox extends FullBox(‘stsz’, version = 0, 0) {
    unsigned int(32) sample_size;
    unsigned int(32) sample_count;
    if (sample_size == 0) {
        for (i = 1; i <= sample_count; i  ) {
            unsigned int(32) entry_size;
        }
    }
}

一些字段的含义:

  • sample_size:如果所有的 sample 都是一样的大小,那么这个字段的值就对应这个大小。如果 sample 的大小各自不同,那么这个字段的值就是 0,这时候每个 sample 的大小存储在 sample size table 中。
  • sample_count:表示 sample 的数量。
  • entry_size:表示 sample size table 中每个 sample 的大小。

下图是一个视频 track 的 stsz 的示例:

3.12、Chunk Offset Box(moov/trak/mdia/minf/stbl/stco(co64))

stco,即 Chunk Offset Box,每个 Chunk 的偏移。这个偏移是相对文件初始位置的偏移。所以这里需要注意的一点是,当修改 mdat Box 之前其他 Box 的信息时,会影响到 Chunk Offset,这里的记录则需要做对应的更新。如果视频文件较大,Offset 用 32 位表示不下,就用 co64 Box 通过 64 位来表示。

在 MP4 文件中,Chunk 是最小的基本单位,而不是 Sample。一个 Chunk 里可以包含单个或多个 Sample。这里是为了优化数据的 I/O 读取效率。

stco(co64) Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class ChunkOffsetBox extends FullBox(‘stco’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i=1; i <= entry_count; i  ) {
        unsigned int(32) chunk_offset;
    }
}

aligned(8) class ChunkLargeOffsetBox extends FullBox(‘co64’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i = 1; i <= entry_count; i  ) {
        unsigned int(64) chunk_offset;
    }
}

一些字段的含义:

  • entry_count:表示下面的条目表的条目数量。其实也就是 Chunk 的数量。
  • chunk_offset:每个 Chunk 的偏移量。

下图是一个视频 track 的 stco 的示例:

3.13、Sample To Chunk Box(moov/trak/mdia/minf/stbl/stsc)

stsc,即 Sample To Chunk Box,包含 Sample 和 Chunk 的映射关系。媒体数据的 sample 采样在文件中是以 chunk 的形式组装起来的。这个 Box 可以用来找到包含 sample 的 chunk,以及 chunk 的位置和描述信息。

stsc Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class SampleToChunkBox extends FullBox(‘stsc’, version = 0, 0) {
    unsigned int(32) entry_count;
    for (i = 1; i <= entry_count; i  ) {
        unsigned int(32) first_chunk;
        unsigned int(32) samples_per_chunk;
        unsigned int(32) sample_description_index;
    }
}

一些字段的含义:

  • entry_count:表示后面表中的条目数量。
  • first_chunk:每个条目开始的 chunk 的位置。这个字段理解起来有点复杂。每个条目可能对应着 1 个或多个 chunk,而这些 chunk 共享后面的 samples_per_chunk 和 sample_description_index 字段。其中的第一个 chunk 则是这里的 first_chunk。
  • samples_per_chunk:每个 chunk 中包含的 sample 的数量。
  • sample_description_index:每个 sample 的描述,一般默认为 1。

下面是一个视频 track 的 stsc 的示例:

可以看到这里的 entry table 里面只有 2 条数据。这里与上面 stco 示例的视频是同一个视频,stco 的信息显示,该视频 track 有 743 个 chunk。那么这里的 stsc 的 entry table 的数据则表示,第 [1, 2-1] 个 chunk 都包含了 2 个 sample,第 [2, last=743] 个 chunk 都包含了 1 个 sample。所以,总共是 (1 * 2) (743 - 2 1) * 1 = 744 个 sample。这个数量刚好和 stsz 示例中显示的 sample 数量对的上。

3.14、Sync Sample Box(moov/trak/mdia/minf/stbl/stss)

stss,即 Sync Sample Box,包含可随机访问的 Sample 序号列表,一般可以认为是关键帧序号列表。

stss Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class SyncSampleBox extends FullBox(‘stss’, version = 0, 0) {
    unsigned int(32) entry_count;
    int i;
    for (i = 0; i < entry_count; i  ) {
        unsigned int(32) sample_number;
    }
}

一些字段的含义:

  • entry_count:下面条目表的数量。如果是 0 则表示视频流中没有可以随机访问的位置。
  • sample_number:可随机访问的 sample 的序号,一般可以认为是关键帧的序号。

下面是一个视频 track 的 stss 的示例:

4、Media Data Box(mdat) 解析

mdat,即 Media Data Box,包含具体的媒体数据,即实际的音视频数据。比如我们需要的第一帧图片的数据就存放在这个 Box 里。这个 Box 中的数据是没有结构的,所以依赖于 moov 中的信息来进行索引。

另外,值得注意的是 mdat Box 在一个 MP4 文件中不是必须的,可以没有。因为 MP4 是支持将媒体数据放在其他文件中,并通过 moov 中的信息来索引。

mdat Box 的数据格式定义:

代码语言:javascript复制
aligned(8) class MediaDataBox extends Box(‘mdat’) {
    bit(8) data[];
}

5、实战解析

5.1、moov 和 MP4 视频的秒开

了解了 MP4 的文件结构后,我们知道了 moov 这个 Box 包含着对视频来说非常重要的索引信息,所以一般播放器需要拿到这些信息才能完成解码器的初始化,开启播放流程。而 moov Box 在文件中的位置是可以随意放置的。它可以放在包含视频实际数据的 mdat Box 前面,也可以放在其后面。一般来讲,如果不做特别设置,moov 会放在 mdat 后面,因为从正常的处理流程上来讲,当所有的音视频数据都处理完成后,才能确定对应的宏观信息和索引信息,这时候才能确定 moov 的信息。

当 moov 放在 mdat 后面时,我们修改视频中 moov/udta 中的用户自定义信息时,不会对 mdat 的 Chunk Offset 造成影响,这样就不需要更新 stco 里的数据,编辑效率较高。但是,当从网络读取和播放 MP4 文件时就需要等待较长时间,直到播放器获取到 moov 的数据后才能初始化解码器并开始播放。

当 moov 放在 mdat 前面时,则与上述情况相反,这时候从网络读取和播放 MP4 文件时,就可以较快获取到 moov 的数据并开始播放。所以一般来说,对于通过网络播放 MP4 视频的场景,都建议将视频处理为 moov 前置。

我们可以使用 FFmpeg 将一个 moov 后置的 MP4 处理为前置:

代码语言:javascript复制
ffmpeg -i slow_play.mp4 -movflags faststart fast_play.mp4
代码语言:javascript复制
在实际开发中,我们通过手机编辑视频时,要么是自己拍摄的,要么是从相册选择的。

在 iOS 中,可以通过系统提供 AVAssetExportSession 来导出视频,其中有一个 shouldOptimizeForNetworkUse 接口可以用来支持 fast start。一般在自己拍摄视频的场景可以这样来做。

代码语言:javascript复制
/* indicates that the output file should be optimized for network use, e.g. that a QuickTime movie file should support "fast start" */
@property (nonatomic) BOOL shouldOptimizeForNetworkUse;

如果只是从相册选择的视频,我们可以借由 AVAssetExportSession 来对视频重新打包来实现 moov 前置。当然,我们也可以自己写一段代码,单纯地针对 MP4 文件来完成 moov 前置的操作。

在实际应用场景中,我们还遇到过由于视频 moov 后置并且视频太大(分辨率 1920x1080,码率 5 Mbps,帧率 25 fps)导致在手机浏览器播放失败的情况。如下图:

遇到报错:

代码语言:javascript复制
Client closed connection before receiving entire response

这种情况可能是由于 moov 后置导致浏览器播放内核需要加载的数据太大而触发某些限制导致了主动断开连接。

5.2、MP4 视频的预加载

在视频相关的业务实现中,有很多需要我们关注的用户体验点,视频迅速开播无黑屏是其中两个常见的关注点。

对视频进行预加载是提升这两个体验点的技术方案之一。那么现在问题来了:预加载多少数据比较合适呢?

在 iOS 中,系统提供的 AVPlayer 有一个特性,即使没有开始播放,当 AVPlayer 加载到足够的数据后,它会把视频的第一帧显示出来,就像是视频的封面一样。结合这个特性,如果我们能够预加载一定的数据量保证 AVPlayer 刚好能把视频首帧渲染出来,这样就能确保视频能迅速开播,并且用户一打开视频就能看到画面,实现了无黑屏。那么对于 AVPlayer 来说这个需要的数据量是多少呢?

通过我们对 AVPlayer 的反复试验,我们发现:AVPlayer 拿到第一个关键帧的 sample 数据即可渲染出首帧画面。基于上面我们对 MP4 文件结构的介绍,我们其实可以找出这个数据量的算法:

  • 1)找到 moov 中的视频对应的 track,从中找出 Sync Sample Box(moov/trak/mdia/minf/stbl/stss),找出第一个 sync sample 的序号 x
  • 2)从 Sample To Chunk Box(moov/trak/mdia/minf/stbl/stsc) 中找出序号为 x 的 sample 所在的 chunk 的序号 k,以及该 chunk 中在 sample x 之前的其他 sample 的数量 m
  • 3)结合 Sample Size Box(moov/trak/mdia/minf/stbl/stsz) 中的 sample size table 找出 sample x 的 size 以及其在它前面的 m 个 sample 的 size,计算这些 size 求和得到 s
  • 4)在 Chunk Offset Box(moov/trak/mdia/minf/stbl/stco(co64)) 中找到序号为 k 的 chunk 的 offset 值 t
  • 5)需要加载的数据量则为:t s

我们还可以通过下面的命令:

代码语言:javascript复制
ffprobe -show_frames -select_streams v -skip_frame nokey -show_entries frame=pict_type,pkt_pos,pkt_size,media_type -i <video_path>

找到第一帧视频 I 帧的 pkt_pos 和 pkt_size,二者相加就是从文件开始位置取得第一帧视频 I 帧所需要的字节数。

本文参考

1)ISO/IEC 14496-14

https://www.iso.org/obp/ui/#iso:std:iso-iec:14496:-14:ed-3:v1:en

2)ISO Base Media File Format https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format

(通过上文的介绍,我们了解了 MP4 视频封装格式中重要的 Box 类型及其包含的信息,也看到了实战中可以用这些信息来做什么。我们将在后面继续探讨其他常见的媒体封装格式,敬请期待)

- 完 -

0 人点赞