关于mysql:5分钟入门MP4文件格式

4次阅读

共计 22797 个字符,预计需要花费 57 分钟才能阅读完成。

写在后面

本文次要内容包含,什么是 MP4、MP4 文件的根本构造、Box 的根本构造、常见且重要的 box 介绍、一般 MP4 与 fMP4 的区别、如何通过代码解析 MP4 文件 等。

写作背景:最近常常答复团队小伙伴对于直播 & 短视频的问题,比方“flv.js 的实现原理”、“为什么设计同学给的 mp4 文件浏览器里播放不了、但本地能够失常播放”、“MP4 兼容性很好,可不可以用来做直播”等。

在解答的过程中,发现常常波及 MP4 协定的介绍。之前这块有简略理解过并做了笔记,这里略微整顿一下,顺便作为团队参考文档,如有错漏,敬请指出。

什么是 MP4

首先,介绍下封装格局。多媒体封装格局(也叫容器格局),是指依照肯定的规定,将视频数据、音频数据等,放到一个文件中。常见的 MKV、AVI 以及本文介绍的 MP4 等,都是封装格局。

MP4 是最常见的封装格局之一,因为其跨平台的个性而失去广泛应用。MP4 文件的后缀为.mp4,基本上支流的播放器、浏览器都反对 MP4 格局。

MP4 文件的格局次要由 MPEG-4 Part 12、MPEG-4 Part 14 两局部进行定义。其中,MPEG-4 Part 12 定义了 ISO 根底媒体文件格式,用来存储基于工夫的媒体内容。MPEG-4 Part 14 理论定义了 MP4 文件格式,在 MPEG-4 Part 12 的根底上进行扩大。

对从事直播、音视频相干工作的同学,很有必要理解 MP4 格局,上面简略介绍下。

MP4 文件格式概览

MP4 文件由多个 box 组成,每个 box 存储不同的信息,且 box 之间是树状构造,如下图所示。

box 类型有很多,上面是 3 个比拟重要的顶层 box:

  • ftyp:File Type Box,形容文件听从的 MP4 标准与版本;
  • moov:Movie Box,媒体的 metadata 信息,有且仅有一个。
  • mdat:Media Data Box,寄存理论的媒体数据,个别有多个;

尽管 box 类型有很多,但根本构造都是一样的。下一节会先介绍 box 的构造,而后再对常见的 box 进行进一步解说。

下表是常见的 box,略微看下有个大抵的印象就好,而后间接跳到下一节。

MP4 Box 简介

1 个 box 由两局部组成:box header、box body。

  1. box header:box 的元数据,比方 box type、box size。
  2. box body:box 的数据局部,理论存储的内容跟 box 类型无关,比方 mdat 中 body 局部存储的媒体数据。

box header 中,只有 type、size 是必选字段。当 size== 0 时,存在 largesize 字段。在局部 box 中,还存在 version、flags 字段,这样的 box 叫做 Full Box。当 box body 中嵌套其余 box 时,这样的 box 叫做 container box。

Box Header

字段定义如下:

  • type:box 类型,包含“预约义类型”、“自定义扩大类型”,占 4 个字节;

    • 预约义类型:比方 ftyp、moov、mdat 等预约义好的类型;
    • 自定义扩大类型:如果 type==uuid,则示意是自定义扩大类型。size(或 largesize)随后的 16 字节,为自定义类型的值(extended_type)
  • size:蕴含 box header 在内的整个 box 的大小,单位是字节。当 size 为 0 或 1 时,须要非凡解决:

    • size 等于 0:box 的大小由后续的 largesize 确定(个别只有装载媒体数据的 mdat box 会用到 largesize);
    • size 等于 1:以后 box 为文件的最初一个 box,通常蕴含在 mdat box 中;
  • largesize:box 的大小,占 8 个字节;
  • extended_type:自定义扩大类型,占 16 个字节;

Box 的伪代码如下:

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

Box Body

box 数据体,不同 box 蕴含的内容不同,须要参考具体 box 的定义。有的 box body 很简略,比方 ftyp。有的 box 比较复杂,可能嵌套了其余 box,比方 moov。

Box vs FullBox

在 Box 的根底上,扩大出了 FullBox 类型。相比 Box,FullBox 多了 version、flags 字段。

  • version:以后 box 的版本,为扩大做筹备,占 1 个字节;
  • flags:标记位,占 24 位,含意由具体的 box 本人定义;

FullBox 伪代码如下:

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

FullBox 次要在 moov 中的 box 用到,比方 moov.mvhd,前面会介绍到。

aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {// 字段略...}

ftyp(File Type Box)

ftyp 用来指出以后文件遵循的标准,在介绍 ftyp 的细节前,先科普下 isom。

什么是 isom

isom(ISO Base Media file)是在 MPEG-4 Part 12 中定义的一种根底文件格式,MP4、3gp、QT 等常见的封装格局,都是基于这种根底文件格式衍生的。

MP4 文件可能遵循的标准有 mp41、mp42,而 mp41、mp42 又是基于 isom 衍生进去的。

3gp(3GPP):一种容器格局,次要用于 3G 手机上;
QT:QuickTime 的缩写,.qt 文件代表苹果 QuickTime 媒体文件;

ftyp 定义

ftyp 定义如下:

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}  

上面是是 brand 的形容,其实就是具体封装格局对应的代码,用 4 个字节的编码来示意,比方 mp41。

A brand is a four-letter code representing a format or subformat. Each file has a major brand (or primary brand), and also a compatibility list of brands.

ftyp 的几个字段的含意:

  • 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 标准,能够将文件 局部(或全副)解码进去;

在理论应用中,不能把 isom 做为 major_brand,而是须要应用具体的 brand(比方 mp41),因而,对于 isom,没有定义具体的文件扩展名、mime type。

上面是常见的几种 brand,以及对应的文件扩展名、mime type,更多 brand 能够参考 这里。

上面是理论例子的截图,不赘述。

对于 AVC/AVC1

在探讨 MP4 标准时,提到 AVC,有的时候指的是“AVC 文件格式”,有的时候指的是 ”AVC 压缩规范(H.264)”,这里简略做下辨别。

  • AVC 文件格式:基于 ISO 根底文件格式 衍生的,应用的是 AVC 压缩规范,能够认为是 MP4 的扩大格局,对应的 brand 通常是 avc1,在 MPEG-4 PART 15 中定义。
  • AVC 压缩规范(H.264):在 MPEG-4 Part 10 中定义。
  • ISO 根底文件格式 (Base Media File Format) 在 MPEG-4 Part 12 中定义。

moov(Movie Box)

Movie Box,存储 mp4 的 metadata,个别位于 mp4 文件的结尾。

aligned(8) class MovieBox extends Box(‘moov’){}

moov 中,最重要的两个 box 是 mvhd 和 trak:

  • mvhd:Movie Header Box,mp4 文件的整体信息,比方创立工夫、文件时长等;
  • trak:Track Box,一个 mp4 能够蕴含一个或多个轨道(比方视频轨道、音频轨道),轨道相干的信息就在 trak 里。trak 是 container box,至多蕴含两个 box,tkhd、mdia;

mvhd 针对整个影片,tkhd 针对单个 track,mdhd 针对媒体,vmhd 针对视频,smhd 针对音频,能够认为是从 宽泛 > 具体,前者个别是从后者推导进去的。

mvhd(Movie Header Box)

MP4 文件的整体信息,跟具体的视频流、音频流无关,比方创立工夫、文件时长等。

定义如下:

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

字段含意如下:

  • creation_time:文件创建工夫;
  • modification_time:文件批改工夫;
  • timescale:一秒蕴含的工夫单位(整数)。举个例子,如果 timescale 等于 1000,那么,一秒蕴含 1000 个工夫单位(前面 track 等的工夫,都要用这个来换算,比方 track 的 duration 为 10,000,那么,track 的理论时长为 10,000/1000=10s);
  • duration:影片时长(整数),依据文件中的 track 的信息推导进去,等于工夫最长的 track 的 duration;
  • rate:举荐的播放速率,32 位整数,高 16 位、低 16 位别离代表整数局部、小数局部([16.16]),举例 0x0001 0000 代表 1.0,失常播放速度;
  • volume:播放音量,16 位整数,高 8 位、低 8 位别离代表整数局部、小数局部([8.8]),举例 0x01 00 示意 1.0,即最大音量;
  • matrix:视频的转换矩阵,个别能够忽略不计;
  • next_track_ID:32 位整数,非 0,个别能够忽略不计。当要增加一个新的 track 到这个影片时,能够应用的 track id,必须比以后曾经应用的 track id 要大。也就是说,增加新的 track 时,须要遍历所有 track,确认可用的 track id;

tkhd(Track Box)

单个 track 的 metadata,蕴含如下字段:

  • 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:视频的变换矩阵;
  • width、height:视频的宽高;

定义如下:

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

例子如下:

hdlr(Handler Reference Box)

申明以后 track 的类型,以及对应的处理器(handler)。

handler_type 的取值包含:

  • vide(0x76 69 64 65),video track;
  • soun(0x73 6f 75 6e),audio track;
  • hint(0x68 69 6e 74),hint track;

name 为 utf8 字符串,对 handler 进行形容,比方 L-SMASH Video Handler(参考 这里)。

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

stbl(Sample Table Box)

MP4 文件的媒体数据局部在 mdat box 里,而 stbl 则蕴含了这些媒体数据的索引以及工夫信息,理解 stbl 对解码、渲染 MP4 文件很要害。

在 MP4 文件中,媒体数据被分成多个 chunk,每个 chunk 可蕴含多个 sample,而 sample 则由帧组成(通常 1 个 sample 对应 1 个帧),关系如下:

stbl 中比拟要害的 box 蕴含 stsd、stco、stsc、stsz、stts、stss、ctts。上面先来个概要的介绍,而后再一一解说细节。

stco / stsc / stsz / stts / stss / ctts / stsd 概述

上面是这几个 box 概要的介绍:

  • stsd:给出视频、音频的编码、宽高、音量等信息,以及每个 sample 中蕴含多少个 frame;
  • stco:thunk 在文件中的偏移;
  • stsc:每个 thunk 中蕴含几个 sample;
  • stsz:每个 sample 的 size(单位是字节);
  • stts:每个 sample 的时长;
  • stss:哪些 sample 是关键帧;
  • ctts:帧解码到渲染的工夫差值,通常用在 B 帧的场景;

stsd(Sample Description Box)

stsd 给出 sample 的形容信息,这外面蕴含了在解码阶段须要用到的任意初始化信息,比方 编码 等。对于视频、音频来说,所须要的初始化信息不同,这里以视频为例。

伪代码如下:

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

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

// AudioSampleEntry、HintSampleEntry 定义略过


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

在 SampleDescriptionBox 中,handler_type 参数 为 track 的类型(soun、vide、hint),entry_count 变量代表以后 box 中 smaple description 的条目数。

stsc 中,sample_description_index 就是指向这些 smaple description 的索引。

针对不同的 handler_type,SampleDescriptionBox 后续利用不同的 SampleEntry 类型,比方 video track 为 VisualSampleEntry。

VisualSampleEntry 蕴含如下字段:

  • data_reference_index:当 MP4 文件的数据局部,能够被宰割成多个片段,每一段对应一个索引,并别离通过 URL 地址来获取,此时,data_reference_index 指向对应的片段(比拟少用到);
  • width、height:视频的宽高,单位是像素;
  • horizresolution、vertresolution:程度、垂直方向的分辨率(像素 / 英寸),16.16 定点数,默认是 0x00480000(72dpi);
  • frame_count:一个 sample 中蕴含多少个 frame,对 video track 来说,默认是 1;
  • compressorname:仅供参考的名字,通常用于展现,占 32 个字节,比方 AVC Coding。第一个字节,示意这个名字理论要占用 N 个字节的长度。第 2 到第 N + 1 个字节,存储这个名字。第 N + 2 到 32 个字节为填充字节。compressorname 能够设置为 0;
  • depth:位图的深度信息,比方 0x0018(24),示意不带 alpha 通道的图片;

In video tracks, the frame_count field must be 1 unless the specification for the media format explicitly documents this template field and permits larger values. That specification must document both how the individual frames of video are found (their size information) and their timing established. That timing might be as simple as dividing the sample duration by the frame count to establish the frame duration.

例子如下:

stco(Chunk Offset Box)

chunk 在文件中的偏移量。针对小文件、大文件,有两种不同的 box 类型,别离是 stco、co64,它们的构造是一样的,只是字段长度不同。

chunk_offset 指的是在文件自身中的 offset,而不是某个 box 外部的偏移。

在构建 mp4 文件的时候,须要特地留神 moov 所处的地位,它对于 chunk_offset 的值是有影响的。有一些 MP4 文件的 moov 在文件开端,为了优化首帧速度,须要将 moov 移到文件后面,此时,须要对 chunk_offset 进行改写。

stco 定义如下:

# Box Type:‘stco’,‘co64’# Container: Sample Table Box (‘stbl’) Mandatory: Yes
# Quantity: Exactly one variant must be present

aligned(8) class ChunkOffsetBox
    extends FullBox(‘stco’, version = 0, 0) {unsigned int(32) entry_count;
    for (i=1; i u 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 u entry_count; i++) {unsigned int(64)  chunk_offset;
    }
}

如下例子所示,第一个 chunk 的 offset 是 47564,第二个 chunk 的偏移是 120579,其余相似。

stsc(Sample To Chunk Box)

sample 以 chunk 为单位分成多个组。chunk 的 size 能够是不同的,chunk 外面的 sample 的 size 也能够是不同的。

  • entry_count:有多少个表项(每个表项,蕴含 first_chunk、samples_per_chunk、sample_description_index 信息);
  • first_chunk:以后表项中,对应的第一个 chunk 的序号;
  • samples_per_chunk:每个 chunk 蕴含的 sample 数;
  • sample_description_index:指向 stsd 中 sample description 的索引值(参考 stsd 大节);
aligned(8) class SampleToChunkBox
    extends FullBox(‘stsc’, version = 0, 0) {unsigned int(32) entry_count;
    for (i=1; i u entry_count; i++) {unsigned int(32) first_chunk;
        unsigned int(32) samples_per_chunk; 
        unsigned int(32) sample_description_index;
    }
}

后面形容比拟形象,这里看个例子,这里示意的是:

  • 序号 1~15 的 chunk,每个 chunk 蕴含 15 个 sample;
  • 序号 16 的 chunk,蕴含 30 个 sample;
  • 序号 17 以及之后的 chunk,每个 chunk 蕴含 28 个 sample;
  • 以上所有 chunk 中的 sample,对应的 sample description 的索引都是 1;
first_chunk samples_per_chunk sample_description_index
1 15 1
16 30 1
17 28 1

stsz(Sample Size Boxes)

每个 sample 的大小(字节),依据 sample_size 字段,能够晓得以后 track 蕴含了多少个 sample(或帧)。

有两种不同的 box 类型,stsz、stz2。

stsz:

  • sample_size:默认的 sample 大小(单位是 byte),通常为 0。如果 sample_size 不为 0,那么,所有的 sample 都是同样的大小。如果 sample_size 为 0,那么,sample 的大小可能不一样。
  • sample_count:以后 track 外面的 sample 数目。如果 sample_size==0,那么,sample_count 等于上面 entry 的条目;
  • entry_size:单个 sample 的大小(如果 sample_size== 0 的话);
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 u sample_count; i++) {unsigned int(32)  entry_size;
        }
    }
}

stz2:

  • field_size:entry 表中,每个 entry_size 占据的位数(bit),可选的值为 4、8、16。4 比拟非凡,当 field_size 等于 4 时,一个字节上蕴含两个 entry,高 4 位为 entry[i],低 4 位为 entry[i+1];
  • sample_count:等于上面 entry 的条目;
  • entry_size:sample 的大小。
aligned(8) class CompactSampleSizeBox extends FullBox(‘stz2’, version = 0, 0) {unsigned int(24) reserved = 0;
    unisgned int(8) field_size;
    unsigned int(32) sample_count;
    for (i=1; i u sample_count; i++) {unsigned int(field_size) entry_size;
    }
}

例子如下:

stts(Decoding Time to Sample Box)

stts 蕴含了 DTS 到 sample number 的映射表,次要用来推导每个帧的时长。

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:stts 中蕴含的 entry 条目数;
  • sample_count:单个 entry 中,具备雷同时长(duration 或 sample_delta)的间断 sample 的个数。
  • sample_delta:sample 的时长(以 timescale 为计量)

还是看例子,如下图,entry_count 为 3,前 250 个 sample 的时长为 1000,第 251 个 sample 时长为 999,第 252~283 个 sample 的时长为 1000。

假如 timescale 为 1000,则理论时长须要除以 1000。

stss(Sync Sample Box)

mp4 文件中,关键帧所在的 sample 序号。如果没有 stss 的话,所有的 sample 中都是关键帧。

  • entry_count:entry 的条目数,能够认为是关键帧的数目;
  • sample_number:关键帧对应的 sample 的序号;(从 1 开始计算)
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;
   }
}

例子如下,第 1、31、61、91、121…271 个 sample 是关键帧。

ctts(Composition Time to Sample Box)

从解码(dts)到渲染(pts)之间的差值。

对于只有 I 帧、P 帧的视频来说,解码程序、渲染程序是统一的,此时,ctts 没必要存在。

对于存在 B 帧的视频来说,ctts 就须要存在了。当 PTS、DTS 不相等时,就须要 ctts 了,公式为 CT(n) = DT(n) + CTTS(n)。

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

例子如下,不赘述:

fMP4(Fragmented mp4)

fMP4 跟一般 mp4 根本文件构造是一样的。一般 mp4 用于点播场景,fmp4 通常用于直播场景。

它们有以下差异:

  • 一般 mp4 的时长、内容通常是固定的。fMP4 时长、内容通常不固定,能够边生成边播放;
  • 一般 mp4 残缺的 metadata 都在 moov 里,须要加载完 moov box 后,能力对 mdat 中的媒体数据进行解码渲染;
  • fMP4 中,媒体数据的 metadata 在 moof box 中,moof 跟 mdat(通常)结对呈现。moof 中蕴含了 sample duration、sample size 等信息,因而,fMP4 能够边生成边播放;

举例来说,一般 mp4、fMP4 顶层 box 构造可能如下。以下是通过笔者编写的 MP4 解析小工具打印进去,代码在文末给出。

// 一般 mp4
ftyp size=32(8+24) curTotalSize=32
moov size=4238(8+4230) curTotalSize=4270
mdat size=1124105(8+1124097) curTotalSize=1128375

// fmp4
ftyp size=36(8+28) curTotalSize=36
moov size=1227(8+1219) curTotalSize=1263
moof size=1252(8+1244) curTotalSize=2515
mdat size=65895(8+65887) curTotalSize=68410
moof size=612(8+604) curTotalSize=69022
mdat size=100386(8+100378) curTotalSize=169408

怎么判断 mp4 文件是一般 mp4,还是 fMP4 呢?个别能够看下是否存在存在 mvex(Movie Extends Box)。

mvex(Movie Extends Box)

当存在 mvex 时,示意以后文件是 fmp4(非谨严)。此时,sample 相干的 metadata 不在 moov 里,须要通过解析 moof box 来取得。

伪代码如下:

aligned(8) class MovieExtendsBox extends Box(‘mvex’){}

mehd(Movie Extends Header Box)

mehd 是可选的,用来申明影片的残缺时长(fragment_duration)。如果不存在,则须要遍历所有的 fragment,来取得残缺的时长。对于 fmp4 的场景,fragment_duration 个别没方法提前预知。

aligned(8) class MovieExtendsHeaderBox extends FullBox(‘mehd’, version, 0) {if (version==1) {unsigned int(64)  fragment_duration;
    } else { // version==0
        unsigned int(32)  fragment_duration;
    }
}

trex(Track Extends Box)

用来给 fMP4 的 sample 设置各种默认值,比方时长、大小等。

aligned(8) class TrackExtendsBox extends FullBox(‘trex’, 0, 0){unsigned int(32) track_ID;
    unsigned int(32) default_sample_description_index; 
    unsigned int(32) default_sample_duration;
    unsigned int(32) default_sample_size;
    unsigned int(32) default_sample_flags
}

字段含意如下:

  • track_id:对应的 track 的 ID,比方 video track、audio track 的 ID;
  • default_sample_description_index:sample description 的默认 index(指向 stsd);
  • default_sample_duration:sample 默认时长,个别为 0;
  • default_sample_size:sample 默认大小,个别为 0;
  • default_sample_flags:sample 的默认 flag,个别为 0;

default_sample_flags 占 4 个字节,比较复杂,构造如下:

老版本标准里,前 6 位都是保留位,新版标准里,只有前 4 位是保留位。is_leading 含意不是很直观,下一大节会专门解说下。

  • reserved:4 bits,保留位;
  • is_leading:2 bits,是否 leading sample,可能的取值包含:

    • 0:以后 sample 不确定是否 leading sample;(个别设为这个值)
    • 1:以后 sample 是 leading sample,并依赖于 referenced I frame 后面的 sample,因而无奈被解码;
    • 2:以后 sample 不是 leading sample;
    • 3:以后 sample 是 leading sample,不依赖于 referenced I frame 后面的 sample,因而能够被解码;
  • sample_depends_on:2 bits,是否依赖其余 sample,可能的取值包含:

    • 0:不分明是否依赖其余 sample;
    • 1:依赖其余 sample(不是 I 帧);
    • 2:不依赖其余 sample(I 帧);
    • 3:保留值;
  • sample_is_depended_on:2 bits,是否被其余 sample 依赖,可能的取值包含:

    • 0:不分明是否有其余 sample 依赖以后 sample;
    • 1:其余 sample 可能依赖以后 sample;
    • 2:其余 sample 不依赖以后 sample;
    • 3:保留值;
  • sample_has_redundancy:2 bits,是否有冗余编码,可能的取值包含:

    • 0:不分明是否存在冗余编码;
    • 1:存在冗余编码;
    • 2:不存在冗余编码;
    • 3:保留值;
  • sample_padding_value:3 bits,填充值;
  • sample_is_non_sync_sample:1 bits,不是关键帧;
  • sample_degradation_priority:16 bits,降级解决的优先级(个别针对如流传过程中呈现的问题);

例子如下:

对于 is_leading

is_leading 不是特地好解释,这里贴上原文,不便大家了解。

A leading sample (usually a picture in video) is defined relative to a reference sample, which is the immediately prior sample that is marked as“sample_depends_on”having no dependency (an I picture). A leading sample has both a composition time before the reference sample, and possibly also a decoding dependency on a sample before the reference sample. Therefore if, for example, playback and decoding were to start at the reference sample, those samples marked as leading would not be needed and might not be decodable. A leading sample itself must therefore not be marked as having no dependency.

为不便解说,上面的 leading frame 对应 leading sample,referenced frame 对应 referenced samle。

以 H264 编码 为例,H264 中存在 I 帧、P 帧、B 帧。因为 B 帧 的存在,视频帧的 解码程序、渲染程序 可能不统一。

mp4 文件的特点之一,就是反对随机地位播放。比方,在视频网站上,能够拖动进度条快进。

很多时候,进度条定位的那个时刻,对应的不肯定是 I 帧。为了可能顺利播放,须要往前查找最近的一个 I 帧,如果可能的话,从最近的 I 帧 开始解码播放(也就是说,不肯定能从后面最近的 I 帧播放)。

将下面形容的此刻定位到的帧,称作 leading frame。leading frame 后面最近的一个 I 帧,叫做 referenced frame。

回顾下 is_leading 为 1 或 3 的状况,同样都是 leading frame,什么时候能够解码(decodable),什么时候不能解码(not decodable)?

1: this sample is a leading sample that has a dependency before the referenced I‐picture (and is therefore not decodable);
3: this sample is a leading sample that has no dependency before the referenced I‐picture (and is therefore decodable);

1、is_leading 为 1 的例子:如下所示,帧 2(leading frame)解码依赖 帧 1、帧 3(referenced frame)。在视频流里,从 帧 2 往前查找,最近的 I 帧 是 帧 3。哪怕曾经解码了 帧 3,帧 2 也解不进去。

2、is_leading 为 3 的例子:如下所示,此时,帧 2(leading frame)能够解码进去。

moof(Movie Fragment Box)

moof 是个 container box,相干 metadata 在内嵌 box 里,比方 mfhd、tfhd、trun 等。

伪代码如下:

aligned(8) class MovieFragmentBox extends Box(‘moof’){}

mfhd(Movie Fragment Header Box)

构造比较简单,sequence_number 为 movie fragment 的序列号。依据 movie fragment 产生的程序,从 1 开始递增。

aligned(8) class MovieFragmentHeaderBox extends FullBox(‘mfhd’, 0, 0){unsigned int(32)  sequence_number;
}

traf(Track Fragment Box)

aligned(8) class TrackFragmentBox extends Box(‘traf’){}

对 fmp4 来说,数据被气氛多个 movie fragment。一个 movie fragment 可蕴含多个 track fragment(每个 track 蕴含 0 或多个 track fragment)。每个 track fragment 中,能够蕴含多个该 track 的 sample。

每个 track fragment 中,蕴含多个 track run,每个 track run 代表一组间断的 sample。

tfhd(Track Fragment Header Box)

tfhd 用来设置 track fragment 中 的 sample 的 metadata 的默认值。

伪代码如下,除了 track_ID,其余都是 可选字段。

aligned(8) class TrackFragmentHeaderBox extends FullBox(‘tfhd’, 0, tf_flags){unsigned int(32) track_ID;
    // all the following are optional fields 
    unsigned int(64) base_data_offset; 
    unsigned int(32) sample_description_index; 
    unsigned int(32) default_sample_duration; 
    unsigned int(32) default_sample_size; 
    unsigned int(32) default_sample_flags
}

sample_description_index、default_sample_duration、default_sample_size 没什么好讲的,这里只解说下 tf_flags、base_data_offset。

首先是 tf_flags,不同 flag 的值如下(同样是求按位求或):

  • 0x000001 base‐data‐offset‐present:存在 base_data_offset 字段,示意 数据地位 绝对于整个文件的 根底偏移量。
  • 0x000002 sample‐description‐index‐present:存在 sample_description_index 字段;
  • 0x000008 default‐sample‐duration‐present:存在 default_sample_duration 字段;
  • 0x000010 default‐sample‐size‐present:存在 default_sample_size 字段;
  • 0x000020 default‐sample‐flags‐present:存在 default_sample_flags 字段;
  • 0x010000 duration‐is‐empty:示意以后时间段不存在 sample,default_sample_duration 如果存在则为 0,;
  • 0x020000 default‐base‐is‐moof:如果 base‐data‐offset‐present 为 1,则疏忽这个 flag。如果 base‐data‐offset‐present 为 0,则以后 track fragment 的 base_data_offset 是从 moof 的第一个字节开始计算;

sample 地位计算公式为 base_data_offset + data_offset,其中,data_offset 每个 sample 独自定义。如果未显式提供 base_data_offset,则 sample 的地位的通常是基于 moof 的绝对地位。

举个例子,比方 tf_flags 等于 57,示意 存在 base_data_offset、default_sample_duration、default_sample_flags。

base_data_offset 为 1263(ftyp、moov 的 size 之和为 1263)。

trun(Track Fragment Run Box)

trun 伪代码如下:

aligned(8) class TrackRunBox extends FullBox(‘trun’, version, tr_flags) {unsigned int(32)  sample_count;
   // the following are optional fields
   signed int(32) data_offset;
   unsigned int(32)  first_sample_flags;
   // all fields in the following array are optional
   {unsigned int(32)  sample_duration;
      unsigned int(32)  sample_size;
      unsigned int(32)  sample_flags
      if (version == 0)
         {unsigned int(32) sample_composition_time_offset; }
      else
         {signed int(32) sample_composition_time_offset; }
   }[sample_count]
}

后面听过,track run 示意一组间断的 sample,其中:

  • sample_count:sample 的数目;
  • data_offset:数据局部的偏移量;
  • first_sample_flags:可选,针对以后 track run 中 第一个 sample 的设置;

tr_flags 如下,大同小异:

  • 0x000001 data‐offset‐present:存在 data_offset 字段;
  • 0x000004 first‐sample‐flags‐present:存在 first_sample_flags 字段,这个字段的值,只会笼罩第一个 sample 的 flag 设置;当 first_sample_flags 存在时,sample_flags 则不存在;
  • 0x000100 sample‐duration‐present:每个 sample 都有本人的 sample_duration,否则应用默认值;
  • 0x000200 sample‐size‐present:每个 sample 都有本人的 sample_size,否则应用默认值;
  • 0x000400 sample‐flags‐present:每个 sample 都有本人的 sample_flags,否则应用默认值;
  • 0x000800 sample‐composition‐time‐offsets‐present:每个 sample 都有本人的 sample_composition_time_offset;
  • 0x000004 first‐sample‐flags‐present,笼罩第一个 sample 的设置,这样就能够把一组 sample 中的第一个帧设置为关键帧,其余的设置为非关键帧;

举例如下,tr_flags 为 2565。此时,存在 data_offset、first_sample_flags、sample_size、sample_composition_time_offset。

编程实际:解析 MP4 文件构造

纸上得来终觉浅,绝知此事要 coding。依据 mp4 文件标准,能够写个繁难的 mp4 文件解析工具,比方前文比照 一般 mp4、fMP4 的 box 构造,就是笔者本人写的剖析脚本。

外围代码如下,残缺代码有点长,能够在 笔者的 github 上找到。

class Box {constructor(boxType, extendedType, buffer) {
        this.type = boxType; // 必选,字符串,4 个字节,box 类型
        this.size = 0; // 必选,整数,4 个字节,box 的大小,单位是字节
        this.headerSize = 8; // 
        this.boxes = [];

        // this.largeSize = 0; // 可选,8 个字节
        // this.extendedType = extendedType || boxType; // 可选,16 个字节
        this._initialize(buffer);
    }

    _initialize(buffer) {this.size = buffer.readUInt32BE(0); // 4 个字节
        this.type = buffer.slice(4, 8).toString(); // 4 个字节

        let offset = 8;

        if (this.size === 1) {this.size = buffer.readUIntBE(8, 8); // 8 个字节,largeSize
            this.headerSize += 8;
            offset = 16;
        } else if (this.size === 1) {// last box}

        if (this.type === 'uuid') {this.type = buffer.slice(offset, 16); // 16 个字节
            this.headerSize += 16;
        }
    }

    setInnerBoxes(buffer, offset = 0) {const innerBoxes = getInnerBoxes(buffer.slice(this.headerSize + offset, this.size));

        innerBoxes.forEach(item => {let { type, buffer} = item;

            type = type.trim(); // 备注,有些 box 类型不肯定四个字母,比方 url、urn

            if (this[type]) {const box = this[type](buffer);
                this.boxes.push(box);
            } else {this.boxes.push('TODO 待实现');
                // console.log(`unknowed type: ${type}`);
            }
        });
    }
}

class FullBox extends Box {constructor(boxType, buffer) {super(boxType, '', buffer);

        const headerSize = this.headerSize;

        this.version = buffer.readUInt8(headerSize); // 必选,1 个字节
        this.flags = buffer.readUIntBE(headerSize + 1, 3); // 必选,3 个字节

        this.headerSize = headerSize + 4;
    }
}

// FileTypeBox、MovieBox、MediaDataBox、MovieFragmentBox 代码有点长这里就不贴了
class Movie {constructor(buffer) {this.boxes = [];
        this.bytesConsumed = 0;

        const innerBoxes = getInnerBoxes(buffer);

        innerBoxes.forEach(item => {const { type, buffer, size} = item;
            if (this[type]) {const box = this[type](buffer);
                this.boxes.push(box);
            } else {// 自定义 box 类型}
            this.bytesConsumed += size;
        });
    }

    ftyp(buffer) {return new FileTypeBox(buffer);
    }

    moov(buffer) {return new MovieBox(buffer);
    }

    mdat(buffer) {return new MediaDataBox(buffer);
    }

    moof(buffer) {return new MovieFragmentBox(buffer);
    }
}

function getInnerBoxes(buffer) {let boxes = [];
    let offset = 0;
    let totalByteLen = buffer.byteLength;

    do {let box = getBox(buffer, offset);
        boxes.push(box);

        offset += box.size;
    } while(offset < totalByteLen);

    return boxes;
}

function getBox(buffer, offset = 0) {let size = buffer.readUInt32BE(offset); // 4 个字节
    let type = buffer.slice(offset + 4, offset + 8).toString(); // 4 个字节

    if (size === 1) {size = buffer.readUIntBE(offset + 8, 8); // 8 个字节,largeSize
    } else if (size === 0) {// last box}

    let boxBuffer = buffer.slice(offset, offset + size);

    return {
        size,
        type,
        buffer: boxBuffer
    };
}

写在前面

受限于工夫,同时为了不便解说,局部内容可能不是很谨严,如有错漏,敬请指出。如有问题,也欢送随时交换。

相干链接

ISO/IEC 14496-12:2015 Information technology — Coding of audio-visual objects — Part 12: ISO base media file format
https://www.iso.org/standard/…

Introduction to QuickTime File Format Specification
https://developer.apple.com/l…

AVC_(file_format)
http://fileformats.archivetea…

AV1 Codec ISO Media File Format Binding
https://aomediacodec.github.i…

正文完
 0