jjzjj

c实现mp4解封装

大刀Gus 2024-05-24 原文

文章目录

前序

最近为了更加深入了解音视频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

有关c实现mp4解封装的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  5. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  6. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  7. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

  8. ruby - 实现k最近邻需要哪些数据? - 2

    我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项

  9. ruby-on-rails - 使用 Ruby 正确处理 Stripe 错误和异常以实现一次性收费 - 2

    我查看了Stripedocumentationonerrors,但我仍然无法正确处理/重定向这些错误。基本上无论发生什么,我都希望他们返回到edit操作(通过edit_profile_path)并向他们显示一条消息(无论成功与否)。我在edit操作上有一个表单,它可以POST到update操作。使用有效的信用卡可以正常工作(费用在Stripe仪表板中)。我正在使用Stripe.js。classExtrasController5000,#amountincents:currency=>"usd",:card=>token,:description=>current_user.email)

  10. ruby - Ruby 1.8 的 Shellwords.shellescape 实现 - 2

    虽然1.8.7的构建我似乎有一个向后移植的Shellwords::shellescape版本,但我知道该方法是1.9的一个特性,在1.8的早期版本中绝对不支持.有谁知道我在哪里可以找到(以Gem形式或仅作为片段)针对Ruby转义的Bourne-shell命令的强大独立实现? 最佳答案 您也可以从shellwords.rb中复制您想要的内容。在Ruby的颠覆存储库的主干中(即GPLv2'd):defshellescape(str)#Anemptyargumentwillbeskipped,soreturnemptyquotes.ret

随机推荐