c实现mp4解封装

前序

最近为了更加深入了解音视频demux这块的功能,准备着手写个demuxer,提取视频流。

MP4简介

MP4的定义

MP4是一种常用的视音频流封装格式,按照指定的协议来存放媒体数据;因为mp4是基于苹果QuickTime文件格式,所以与mov有很多相同之处,在苹果开发者平台可以看到详细的有关封装文档(https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25615)

MP4的封装格式

  • MP4格式预览—mp4是由多个box嵌套组成的
    在这里插入图片描述

  • MP4主要的顶部box

    ftyp box:描述MP4锁遵循的规范和版本

    mdat box :存放媒体数据

    moov box:存放媒体参数(pps、sps等)相关信息和用于索引媒体数据存储位置的信息

  • MP4常用box
    在这里插入图片描述

Box类型详解

Box格式

在这里插入图片描述

  1. size字段为整个box的大小,包括box header和box body
  2. type为box的类型,通常为四字节的字符串,例如ftyp
  3. 当size == 0时,box的大小为large size
  4. 当box为full box时,存在version和flags字段,具体含义因box不同而不同
  5. 若box没有嵌套其他box,例如ftyp box,则box body部分根据具体规范解析相应字段;若box为container box,则box body部分嵌套其它box,还需一步步解套获取最终的数据

ftyp box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

  • major_brand:比如常见的 isom、mp41、mp42、avc1、qt等。它表示“最好”基于哪种格式来解析当前的文件。举例,major_brand 是 A,compatible_brands 是 A1,当解码器同时支持 A、A1 规范时,最好使用A规范来解码当前媒体文件,如果不支持A规范,但支持A1规范,那么,可以使用A1规范来解码;

  • minor_version:提供 major_brand 的说明信息,比如版本号,不得用来判断媒体文件是否符合某个标准/规范;

  • compatible_brands:文件兼容的brand列表。比如 mp41 的兼容 brand 为 isom。通过兼容列表里的 brand 规范,可以将文件 部分(或全部)解码出来;

mvhd box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • version :一字节用于指定mvhd的版本

    • flags:3字节,预留

    • Create time:媒体创建时间,与UTC时间不同的是,此时间是从1904年1月1日0:0:0开始计算的,而utc是从1970年1月1日0:0:0开始计算,故Create time须要减去时间差换算成utc时间

      // 注:66年时间差不是66*365*24*3600来计算
      creation_time_utc = creation_time - (66年时间差) = creation_time - 2082844800
      
    • Modification time:媒体最后被修改的时间,计算方式同Create time

    • Timescale:一秒包含的时间单位(整数)。举个例子,如果timescale等于1000,那么,一秒包含1000个时间单位(后面track等的时间,都要用这个来换算,比如track的duration为10,000,那么,track的实际时长为10,000/1000=10s);

    • Duration:影片时长(整数),根据文件中的track的信息推导出来,等于时间最长的track的duration;

    • Preferred rate:推荐的播放速率,32位整数,高16位、低16位分别代表整数部分、小数部分([16.16]),举例 0x0001 0000 代表1.0,正常播放速度;

    • Preferred volume:播放音量,16位整数,高8位、低8位分别代表整数部分、小数部分([8.8]),举例 0x01 00 表示 1.0,即最大音量;

    • Matrix struct:视频的转换矩阵,详情看

      Basic Data Types

    • Next_track_ID:32位整数,非0,一般可以忽略不计。当要添加一个新的track到这个影片时,可以使用的track id,必须比当前已经使用的track id要大。也就是说,添加新的track时,需要遍历所有track,确认可用的track id;

tkhd box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • version:tkhd box的版本;
    • flags:按位或操作获得,默认值是7(0x000001 | 0x000002 | 0x000004),表示这个track是启用的、用于播放的 且 用于预览的。
      • Track_enabled:值为0x000001,表示这个track是启用的,当值为0x000000,表示这个track没有启用;
      • Track_in_movie:值为0x000002,表示当前track在播放时会用到;
      • Track_in_preview:值为0x000004,表示当前track用于预览模式;
    • Creation time:当前track的创建时间;
    • Modification time:当前track的最近修改时间;
    • Track ID:当前track的唯一标识,不能为0,不能重复;
    • Duration:当前track的完整时长(需要除以timescale得到具体秒数);
    • Layer:视频轨道的叠加顺序,数字越小越靠近观看者,比如1比2靠上,0比1靠上;
    • Alternate_group:当前track的分组ID,alternate_group值相同的track在同一个分组里面。同个分组里的track,同一时间只能有一个track处于播放状态。当alternate_group为0时,表示当前track没有跟其他track处于同个分组。一个分组里面,也可以只有一个track;
    • Volume:audio track的音量,介于0.0~1.0之间;
    • Matrix structure:视频的变换矩阵;
    • Track width:视频的宽
    • Track height:视频的高

hdlr box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:hdlr box的版本
    • Flags:置0
    • Component type:四字节子串定义handler的类型;此字段只有两种值合法:'mhlr’(media handlers)和’dhlr’(data handlers)
    • Component subtype:针对Component type进行细分类型,例如’vide’定义为视频数据,'soun’定义为音频数据
    • Component manufacturer:保留,置0
    • Component flags:保留,置0
    • Component flags mask:保留,置0
    • Component name:子串指定Component 的名字,可能为空

mdat box

  • 数据结构分布图
    在这里插入图片描述

    注意:取到的frame前四个字节为frame数据的长度字节,须要偏移去掉

stbl box

主要存放了媒体参数(pps、sps、vps等)相关信息和用于解析mdat中视音频数据的关键信息

  • stsd:给出视音频的相关参数信息,有高宽、音量、位深度和每个sample多少个frame
  • stco:chunk在文件中的偏移
  • stsc:每个chunk中包含几个sample
  • stsz:每个sample的size(单位是字节)
  • stts:每个sample的时长
  • stss:哪些sample是关键帧

stsd box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:stsd box的版本
    • Flags:置0
    • Number of entries:Sample description table的个数
    • Sample description table:以视频为例,此时Sample description table字段中为若干个视频编码相关的box,例如avc1 box
      • avc1 box

      • avcC box(包含了视频关键参数,在ISO/IEC 14496-15中定义)

        • 字段分布图
          在这里插入图片描述

        • 字段解析

          • num_of_sps:sps的个数
          • sps_length:sps的长度
          • sps_nal_unit:长度为sps_length的sps
          • num_of_pps:pps的个数
          • pps_length:pps的长度
          • pps_nal_unit:长度为pps_length的pps

          其他字段可以自行在ISO/IEC 14496-15中查到

stco box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:stco box的版本
    • Flags:置0
    • Number of entries:chunk的个数
    • Chunk offset table:每个chunk在整个视频文件的偏移值,每个值的长度为4字节

stsc box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:stsc box的版本
    • Flags:置0
    • Number of entries:”Sample-to-chunk table”的条数
    • Sample-to-chunk table:
      • First chunk:chunk的索引
      • Samples per chunk:从’First chunk’开始,每个chunk中sample的个数
      • Sample description ID:stsd box中‘Sample description table’的下标
  • Sample-to-chunk table示意图
    在这里插入图片描述

    • chunk1-chunk2:每个chunk中有3个sample,并且Sample description ID为23
    • chunk3-chunk4:每个chunk中有1个sample,并且Sample description ID为23
    • chunk5:每个chunk中有3个sample,并且Sample description ID为24

stsz box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:stsz box的版本
    • Flags:置0
    • Sample size:为0则表示所有sample的大小不一定一样,不为0则表示所有sample的大小一样
    • Number of entries:”Sample size table”的条数
    • Sample size table:每个sample的size,每个sample size的长度为4字节

stts box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:stts box的版本
    • Flags:置0
    • Number of entries:“Time-to-sample table”的条数
    • Time-to-sample table:
      • Sample count:具有相同“Sample duration”的个数
      • Sample duration:sample的时长(以timescale为计量)
  • Time-to-sample table:示意图
    在这里插入图片描述

    sample1 - sample4的sample duration是4

stss box

  • 字段分布图
    在这里插入图片描述

  • 字段解析

    • Version:stts box的版本
    • Flags:置0
    • Number of entries:“Sync sample table”的条数
    • Sync sample table:关键帧对应的sample index

demuxer demo的实现(视频数据部分)

  1. 获取sps pps参数

    1. 解析stsd box,其中contain avc1 box和avcC box(此步骤详解见上文)

    2. 解析avcC box可以获取到sps和pps

      以下为ISO/IEC 14496-15中解析avcC的伪代码

    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 =111111’b; 
    	 unsigned int(2) lengthSizeMinusOne; 
    	 bit(3) reserved =111’b; 
    	 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 =111111’b; 
    		 unsigned int(2) chroma_format; 
    		 bit(5) reserved =11111’b; 
    		 unsigned int(3) bit_depth_luma_minus8; 
    		 bit(5) reserved =11111’b; 
    		 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; 
    		 } 
    	 } 
    }
    
  2. 获取关键帧位置

    解析stss box可以知道哪一个sample中包含关键帧

  3. 获取chunk位置

    解析stco box可以获取到每个chunk在视频文件中的索引

  4. 获取每个chunk中sample个数

    解析stsc box可以获取到每个chunk包含多少个sample

  5. 获取sample大小

    解析stsz box可以获取到每个sample的大小

  6. 获取frame位置(demo视频文件一个sample只包含一个frame,所以sample的位置和大小就是frame的位置和大小)

    1. 根据stsd解析到每个sample中有多少个frame
    2. 然后再根据trunk的位置和sample的大小来定位frame起始地址
    3. mdat中frame的数据格式为: | 4字节数据长度 | frame数据|,所以根据字节长度读取相应个数frame
  7. 获取到一帧数据后

    1. 判断当前frame为I帧,则添加写入(start_code+sps) + (start_code+pps) + (start_code + frame数据)到输出文件
    2. 判断当前frame不为I帧,则写入(start_code + frame数据)到输出文件
  8. 保存成h264文件,可使用ffplay和potplay播放

注意:有些非字串的字段为大端字节序,须要转换

总结:

  1. 解析非字符串的数据时,需要注意大小端的问题
  2. 解析对应的box获取到sps、vps、pps
  3. 解析对应的box拿到视频帧数据
  4. 将视频帧写入本地文件的时候要注意
    1. 视频帧前四个字节为视频帧数据长度
    2. 若为I帧则需要加上sps、vps、pps
    3. 视频帧注意加上start_code

工具介绍

  1. mp4info—可以看到相关box的字节信息,但发现对avcC的解析漏掉了几个字节
    在这里插入图片描述

  2. mp4 exploer—可以更加直观的看到视音频数据信息
    在这里插入图片描述

源码

https://github.com/TaoChou/demuxer-c

参考

  1. https://zhuanlan.zhihu.com/p/333765990
  2. https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
  3. ISO/IEC 14496-15

猜你喜欢

转载自blog.csdn.net/u011598479/article/details/128135487