mp4实例分析

avatar
web前端开发

本文通过对一个真实mp4文件进行层层剖析,来了解一下mp4文件的具体组成。

我们借助一个在线的mp4解析工具mp4parse来进行分析和验证。

名词定义

在解析mp4封装格式之前,我们需要先了解mp4涉及的名词定义,如下所示:

  • box:也叫Atom,是mp4文件的基本组成单位,由唯一类型标识符(type)和长度(size)定义的面向对象的构建块,包括header和data两部分
  • Fullbox:基于box的扩展,在box的基础上增加了versino(1字节)和flags(3字节)
  • Containerbox:当一个box的data中包括其他box时,该box被称为一个container box
  • header:box头文件,包括size和type两部分,如果是fullbox,则还包括versino(1字节)和flags(3字节)
  • data:box的真实数据,可以是具体数据也可以是嵌套的box
  • sample:可以看成媒体数据的基本组成单位。video sample表示一帧数据或者一组连续视频帧。audio sample 是一段连续的压缩音频,包含一定数量的音频采样
  • chunk:同一个媒体类型连续几个sample组成的集合。每个chunk的sample数量不固定,可以一样,可以不一样
  • track:一个包含连续时间戳的完整媒体资源,一般mp4文件包括audio track和video track

box结构可以由下图来所示:

image.png
图片来自MP4文件格式详解--结构概述

mp4结构组成

mp4文件是由一个一个的box组成,box可以看成是mp4的基本组成单元。具体有哪写box见ISO_IEC_14496-12文档 table 1,也可以参考这篇文章

实例分析

我们以一个具体的mp4文件来分析下每个具体box。每个box从二进制数据、box结构、字段解析表等几个部分来进行介绍。分析所用的视频点击这里下载。

ftyp(file type box)

ftyp是mp4文件的标识box,在文件中有且只有一个,并放在文件最开始的位置。其中包含了当前视频遵循的具体规范及其版本号。

image.png

moov(movie box)

Moov box是个container box,具体数据存储在子box当中,有且只有一个。主要用来存储mp4元数据信息,比如音视频时长,timescale等。一般在点播文件中为了快速打开视频文件,放在mdat之前,紧随ftyp出现。如果是录播的数据,一般放在mdat 之后。

image.png

mvhd(Movie Header Box )

Mvhd box 是moov的子box, 定义了媒体相关的整体信息,如文件创建、修改时间、视频时长,timescale等。

image.png

trak(track box)

Trak 是个container box,是moov的子box。 moov中可以有一个或者多个trak box。每个trak中包含了该track的元数据信息。

image.png

tkhd(track header box)

tkhd是trak的子box,是个full box,该Track的媒体整体信息包括时长、图像的宽度和高度等。一般默认header中的flags字段的默认值是7,通过按位或操作获得,既track_enabled(0x000001)、track_in_movie(0x000002)、track_in_preview(0x000004)三种值的按位或结果值。falgs对应的值有以下几种:

  • Track_enabled:值为0x000001,表示这个track是启用的,当值为0x000000,表示这个track没有启用;
  • Track_in_movie:值为0x000002,表示当前track在播放时会用到;
  • Track_in_preview:值为0x000004,表示当前track用于预览模式;
  • Track_size_is_aspect_ratio: 值为0x000008,表示宽高不以像素为单位表示

image.png

image.png

mdia(media box)

Mdia box是trak的子box,是个container box,所以只有box header ,data部分是子box,需要进一步解析

image.png

mdhd(media header box)

mdhd是mdia的子box,是个full box。该Box里面主要定义了媒体无关的整体信息,其中最关心的两个字段是timescale和duration,分别表示了该Track的时间戳和时长信息

image.png

hdlr(Handler Reference Box)

hdlr是mdia的子box,是个full box。主要用来标识track的类型。

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;   
}

image.png

Minf(media information box)

minf是mdia的子box,是一个container box,该box中包括了track中的所有关键信息,主要包括vmhd、dinf、stbl三个box

image.png

vmhd(video media header box)

vmhd box是minf的子box,是full box。该box定义了特定的颜色和图形模式信息

aligned(8) class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 1) {
    template unsigned int(16)  graphicsmode = 0;       
    template unsigned int(16)[3]  opcolor = {0, 0, 0}; 
}

image.png

dinf(data information box)

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

Dinf box是个container box,只包括size和type,主要看data中的子box dref dref(data reference box)

dref是个full box

aligned(8) class DataReferenceBox extends FullBox(‘dref’, version = 0, 0) {
   unsigned int(32)  entry_count;
   for (i=1; i <= entry_count; i++) {
     DataEntryBox(entry_version, entry_flags) data_entry; 
   } 
} 

image.png

URL/URN

URL/URN是个full box,从二进制数据看到,location字段值是空的。因此当前track是一个完成的track。

 aligned(8) class DataEntryUrlBox (bit(24) flags) extends FullBox(‘url ’, version = 0, flags) {
   string   location;
 } 
aligned(8) class DataEntryUrnBox (bit(24) flags) extends FullBox(‘urn ’, version = 0, flags) {
   string   name;
   string   location;
 } 

image.png

stbl(Sample Table Box )

stbl包含了媒体流每一个sample在文件中的offset,pts,duration等信息,是个container box,包括sample description box(stsd)、time to sample box(stts)、composition time to sample box(ctts)、sample to chunk box(stsc)、sample size box(stsz或者stz2)、 time to sample box(stts)、chunk offset box (stco 或 co64)等box。如果要想在mdat表中找到对应的数据,需要从上面这些表中查找到sample对应的offset

主要看下header部分数据,data部分是子box,会在接下来继续分析。

stsd(Sample Description Box )

帧描述表,full box,也是个container box。提供了有关所用编码类型的详细信息,以及该编码所需的任何初始化信息。具体的编解码初始化信息放在子box当中。

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){
        // audio track
        case ‘soun’: 
            AudioSampleEntry();
            break;
        // video track
        case ‘vide’: 
            VisualSampleEntry();
            break;
        // hint track
        case ‘hint’: 
            HintSampleEntry();
            break;
        }
    }

}

image.png

avc1

mp4中使用的视频数据是avc1编码的,既通过固定长度字段进行nalu的分割。box中包含了视频的宽高等信息.

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 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;    
    // other boxes from derived specifications    
    CleanApertureBox      clap;      //   optional      
    PixelAspectRatioBox   pasp;      //   optional   
}

class AVCSampleEntry() extends VisualSampleEntry (‘avc1’){ 
    AVCConfigurationBox config; 
    MPEG4BitRateBox (); // optional 
    MPEG4ExtensionDescriptorsBox (); // optional 
}

image.png

  • avcc

avc1 box的子box,该Box则包含了真实的SPS PPS等信息,包含着视频编解码参数

class AVCConfigurationBox extends Box(‘avcC’) { 
    AVCDecoderConfigurationRecord() AVCConfig; 
}

aligned(8) class AVCDecoderConfigurationRecord { 
    unsigned int(8) configurationVersion = 1; 
    unsigned int(8) AVCProfileIndication; 
    unsigned int(8) profile_compatibility; 
    unsigned int(8) AVCLevelIndication; 
    bit(6) reserved = ‘111111b; 
    unsigned int(2) lengthSizeMinusOne; 
    bit(3) reserved = ‘111b; 
    unsigned int(5) numOfSequenceParameterSets; 
    for (i=0; i< numOfSequenceParameterSets; i++) { 
        unsigned int(16) sequenceParameterSetLength; 
        bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit; 
    } 
    unsigned int(8) numOfPictureParameterSets; 
    for (i=0; i< numOfPictureParameterSets; i++) { 
       unsigned int(16) pictureParameterSetLength; 
        bit(8*pictureParameterSetLength) pictureParameterSetNALUnit; 
    } 
    if( profile_idc == 100 || profile_idc == 110 || 
    profile_idc == 122 || profile_idc == 144 ) { 
        bit(6) reserved = ‘111111b; 
        unsigned int(2) chroma_format; 
        bit(5) reserved = ‘11111b; 
        unsigned int(3) bit_depth_luma_minus8; 
        bit(5) reserved = ‘11111b; 
        unsigned int(3) bit_depth_chroma_minus8; 
        unsigned int(8) numOfSequenceParameterSetExt; 
        for (i=0; i< numOfSequenceParameterSetExt; i++) { 
            unsigned int(16) sequenceParameterSetExtLength; 
            bit(8*sequenceParameterSetExtLength) sequenceParameterSetExtNALUnit; 
        } 
    } 
 }

image.png

stts(Decoding Time to Sample Box )

该box是stbl的子box,是个full box。该表中存储了dts(解码时间戳)与sample的对应关系,可以计算出每个sample的dts。为了节省存储空间,不是记录每帧的dts,而是把连续的且每帧的duration相等的sample分为一组。

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;
   }
 } 

 

image.png

stss(Sync Sample Box )

stss是stbl的子box,是个full box。包含了每个关键帧在所有帧中的数组下标+1,即关键帧队列从1开始算起。在音频track中不存在这个box

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;
   }
 } 

 

image.png

ctts(Composition Time to Sample Box )

Ctts box是full box,是存储pts与dts差值的box。如果没有B帧,没有该box。存储形式与stts类似,存储的值由sample duration变成了当前sample 的pts 与dts的差值。这里通过这个表和stts就可以计算出Sample的PTS。

Pts(n) = offset(n) + dts(n)

 aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, version, 0) {
   unsigned int(32)  entry_count;
   int i;
   if (version==0) {
       for (i=0; i < entry_count; i++) {
         unsigned int(32)  sample_count;
         unsigned int(32)  sample_offset;
       } 
    } 
    else if (version == 1) {
       for (i=0; i < entry_count; i++) {
         unsigned int(32)  sample_count;
         signed   int(32)  sample_offset;
       } 
    }
} 

注:只截取了部分数据

image.png

stsc(Sample To Chunk Box )

该表记录了每个chunk中包含了多少个sample,如果连续的chunk包含的sample数量相同,会放到同一个集合当中。每个集合包含如下三个字段:

first_chunk:当前集合最开始chunk的索引值,从1开始

samples_per_chunk:每个chunk个有多少个sample

sample_description_index: 描述采样点的采样描述项的索引值,范围为1到样本描述表中的表项数目

举个例子:

第一个chunk包含10个sample,第二个chunk包含10个sample,第三个chunk包含12个sample

第一个chunk与第二个chunk有相同的sample,所以会放在一个集合中,且first_chunk为1。第三个chunk的sample与前两个都不相同,在另一个集合成,该集合的first_chunk为3,因为该集合当中最开始的chunk的索引值是3。

每个集合的值如下表所示:

first_chunksamples_per_chunksample_description_index
1101
3121
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; 
   } 
} 

image.png

stsz(sample size box)

每个Sample的Size即包含的字节数.包含了媒体中全部sample的数目和一张给出每个sample大小的表。这个box相对来说体积是比较大的。

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;
      }
   }
} 

注:包含部分数据

image.png

stco(Chunk Offset Box )

full box。该Box存储了chunk Offset,表示了每个chunk在文件中的位置,这样我们就能找到了chunk在文件的偏移量,然后根据其它表的关联关系就可以读取每个sample的大小。

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

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;
   }
 } 

image.png

mdat(media data box)

mdat box存储真实的媒体数据。从mp4parse 中看到mdat的偏移量为40.

字段字节数数据对应值含义
Box size400 42 1D 4D4332877box大小
Box type46D 64 61 74mdatASCII值
data

如何找到对应的sample数据呢。我们来试下。

从stco中知道,第一个sample的偏移量是48。从stsz中知道第一个sample的大小为766个字节。所以

00 00 02 7F开始的数据是视频的第一帧数据。

该视频中存储的是avc1数据,avc1数据的由如下格式组成:

avc1 data = NALU length + NALU header + Nalu Data....... NALU length + NALU header + Nalu Data

NALU header的占用字节数从avcc中的lengthSizeMinusOne字段知道为4个字节,表示当前nal的长度(nalu header length + nalu data length)。

所以第一个nal的长度为00 00 02 7F,既639个字节。但是上面提到第一个sample的大小为766。为什么不对呢,我们接下来再看下。

第一个nal长度为639,加上最开始的偏移量48和nalu length的长度4个字节。第二个nal的偏移量为48+ 4 +639 = 691。我们看下偏移量691(0x2b3)的数据为00 00 00 77,既119。(4 + 119 ) + (4+639 )= 766。现在可以得出结论,第一个sample当中包含了两个nalu。

以此类推,可以找个每个sample对应的data

总结

这篇文章主要是通过分析一个真正的mp4文件来了解和熟悉mp4的结构。对于一些刚入门的同学来说,熟悉mp4会容易一些。