注:基于bilibili的FLV.js实现javascript
flv.js的github地址:github.com/Bilibili/fl…html
在MP4文件格式中,整个视频容器都是由多个box和子box组成,根据box类型主要分为3大类:视频类型(ftyp
)、视频数据(mdat
)、视频信息(moov
)。视频信息(moov
)用来描述视频数据(mdat
)。(注:还有一个主要box为moof box
,因这里仅解释普通MP4格式数据,moof box
仅在流式MP4中使用。在流式MP4格式中,box排序、相同box的box body
内容格式与普通MP4不同,详情参见扩展)html5
视频参数(moov
)中主要的子box 为track
,每一个track
都是一个随时间变化的媒体序列,时间单位为一个sample,能够是一帧数据,或者音频(注意,一帧音频能够分解成多个音频sample,因此音频通常用sample做为单位,而不用帧)。Sample
按照事件顺序排列。track里面的每一个sample经过引用关联到一个sample description
。这个sample descriptios
定义了怎样解码这个sample
,例如使用的压缩算法。(注:在目前的使用中,该值为1)java
注:该文主要介绍普通mp4文件类型ios
在Javascript中,Mp4 的全部box所有由经过new Uint8Array()
实现。git
box前8位为预留位,这8位中前4位为数据size,当size值为0时,表示该box为文件的最后一个box(仅存在于mdat box
中),当size值为1时,表示该box的size为large size
(8位),真正的box size要在largesize中获得(一样仅存在于mdat box
中)。后4位为前面box type
的Unicode编码。当type是uuid时,表明Box中的数据是用户自定义扩展类型。es6
Box
由header
和body
组成,以32位的4字节整数存储方式存储到内存,开头4个字节(32位)为box size
,后面紧跟的4位为box的类型。Box body
能够由数据组成,也能够由子box组成。github
一个box的结构以下:算法
视频与音频的参数不同,通常状况下一个MP4文件区分为2个trak,一个为video trak
,另外一个是audio trak
。每一个track都有trakId,视频的trakId为1,音频的trakId为2。数组
整个MP4文件格式以下图
Ftypbox 是一个由四个字符组成的码字,用来表示编码类型、兼容协议或者媒体文件的用途。
在普通MP4文件中,ftyp box
有且仅有一个,在文件的开始位置。
经过MP4reader工具,能够看出ftyp box
的结构
Box size(4字节):0x00000024:box的长度是36字节;
Boxt type(4字节):0x66747970:“ftyp”的ASCII码,box的类型;
major_brand(4字节):0x69736f6d:“isom“的ASCII码;
minor_version(4字节):0x00000200:isom的版本号;
compatible_brands(12字节):说明该文件兼容isom, iso2, avc1, mp41 四种协议。
Ftyp更多兼容协议 : www.ftyps.com/
Mdat box
中包含了MP4
文件的媒体数据,在文件中的位置能够在moov
的前面,也能够在moov
的后面,因咱们这里用到MP4
文件格式用来写mp4文件,须要计算每一帧媒体数据在文件中的偏移量,为了方便计算,mdat
放置moov
前面。
Mdat box
数据格式单一,无子box。主要分为box header
和box body
,box header
中存放box size
和box type
(mdat
),box body
中存放全部媒体数据,媒体数据以sample为数据单元。
这里使用时,视频数据中,每个sample
是一个视频帧,存放sample
时,须要根据帧数据类型进行拼帧处理后存放。
H.264
视频帧数据类型以下:
注:一、在目前实现中,I帧数据中暂不包含序列参数集(sps
)和图像参数集(pps
)。
二、以上帧数据仅针对视频帧数据。
在普通mp4
中,在获取数据以前,须要解析每一个帧数据所在位置,每一个帧数据都存放在mdat中,而这些帧的信息所有存放在stbl box
中,因此,若要mp4文件可以正常播放,须要在写mp4文件时,将全部的帧数据信息写入 stbl box
中。
Mdat box
中,可能会使用到box的large size
,当数据足够大,没法用4个字节来描述时,便会使用到large size
。在读取MP4文件时,当mdat box
的size位为1时,真正的box size
在large size
中,一样在写mp4文件时,若须要large size
,须要将box size
位配置为1。
Moov box
中存放着媒体信息,上面提到的stbl里存放帧信息,属于媒体信息,也在moov box
里。Moov box
用来描述媒体数据。
Moov box
主要包含 mvhd
、trak
、mvex
三种子box。
Mvhd box
定义了整个文件的特性
字段 | 长度(字节) | 描述 |
---|---|---|
尺寸 | 4 | 这个movie header atom的字节数 |
类型 | 4 | Mvhd |
版本 | 1 | 这个movie header atom的版本 |
标志 | 3 | 扩展的movie header标志,这里为0 |
生成时间 | 4 | Movie atom的起始时间。基准时间是1904-1-1 0:00 AM |
修订时间 | 4 | Movie atom的修订时间。基准时间是1904-1-1 0:00 AM |
Time scale | 4 | 本文件的全部时间描述所采用的单位 |
Duration | 4 | 媒体可播放时长 |
播放速度 | 4 | 播放此movie的速度。1.0为正常播放速度 |
播放音量 | 2 | 播放此movie的音量。1.0为最大音量 |
保留 | 10 | 这里为0 |
矩阵结构 | 36 | 该矩阵定义了此movie中两个坐标空间的映射关系 |
预览时间 | 4 | 开始预览此movie的时间,写文件时该值为0 |
预览duration | 4 | 以movie的time scale为单位,预览的duration,写文件时该值为0 |
Poster time | 4 | The time value of the time of the movie poster. |
Selection time | 4 | The time value for the start time of the current selection. |
Selection duration | 4 | The duration of the current selection in movie time scale units. |
当前时间 | 4 | 当前时间 |
下一个track ID | 4 | 下一个待添加track的ID值。0不是一个有效的ID值。 |
这里写mp4
时须要传入的参数为Time scale
和 Duration
,其余的使用默认值便可。
一个Track box
定义了movie中的一个track
。一部movie
能够包含一个或多个tracks
,它们之间相互独立,各自有各自的时间和空间信息。每一个track box
都有与之关联的mdat box
。
Track
主要有如下目的:
包含媒体数据引用和描述
包含modifier track
流媒体协议的打包信息(hint trak
),引用或者复用对应的媒体sample data
。 Hint tracks
和modifier tracks
必须保证完整性,同时和至少一个media track
一块儿存在。换句话说,即便hint tracks
复制了对应的媒体sample data
,media tracks
也不能从一部hinted movie
中删除。
写mp4时仅用到第一个目的,因此这里只介绍媒体数据的引用和描述。
一个trak box通常主要包含了tkhd box、 edts box 、mdia box
用来描述trak box的header 信息,定义了一个trak的时间、空间、音量信息。
字段 | 长度(字节) | 描述 |
---|---|---|
尺寸 | 4 | 这个atom的字节数 |
类型 | 4 | tkhd |
版本 | 1 | 这个atom的版本 |
标志 | 3 | 有效的标志是: (1)0x0001 - the track is (2)0x0002 - the track is used in the movie(3)0x0004 - the track is used in the movie’s previe·0x0008 - the track is used in the movie’s poster |
生成时间 | 4 | Movie atom的起始时间。基准时间是1904-1-1 0:00 AM |
修订时间 | 4 | Movie atom的修订时间。基准时间是1904-1-1 0:00 AM |
Track ID | 4 | 惟一标志该track的一个非零值。 |
保留 | 4 | 这里为0 |
Duration | 4 | 该track的时长,若该trak为videotrak,其时长来源于elst,若无elst,则取mvhd的时长 |
保留 | 8 | 这里为0 |
Layer | 2 | The track’s spatial priority in its movie. The QuickTime Movie Toolbox uses this value to determine how tracks overlay one another. Tracks with lower layer values are displayed in front of tracks with higher layer values. |
Alternate group | 2 | A collection of movie tracks that contain alternate data for oneanother |
音量 | 2 | 播放此track的音量。1.0为正常音量 |
保留 | 2 | 这里为0 |
矩阵结构 | 36 | 该矩阵定义了此track中两个坐标空间的映射关系 |
宽度 | 4 | 若是该track是video track,此值为图像的宽度,若为audio,为0 |
高度 | 4 | 若是该track是video track,此值为图像的高度,若为audio,为0 |
该box为edst box
的惟一子box
,不是全部的MP4文件都有edst box
,这个box
是使其对应的trak box
的时间戳产生偏移。暂时未发现须要该偏移量的地方,编码时也未对该box进行编码。
该box
定义了trak box
的类型和sample
的信息。
其header box
--- mdhd box
定义了该box
的timescale
和duration
(注:这里的这两个参数与前面说的mvhd
有区别,这里的这两个参数都是以一个sample
为时间单位的,例:在只有一个视频trak
的状况下,mvhd
的timescale
为1000,一个sample
的duration
为40 ,那么这里的timescale
为1000/40,同理这里的duration
算法与之同样理解。)
Hdlr box
定义了这段trak
的媒体处理组件,如下图会更清晰的解释这个box
该box
也是上面的mdia box
的子box
,其主要用来描述该trak
的具体的媒体处理组件内容的。
其header box
根据trak
的类型有2种,vmhd
和smhd
,二者没有什么特殊的数据,只是为了定义headle
的类型。
其子box
--- dinf box
用来定义媒体处理组件如何获取媒体数据的,dinf box
的子box
--- dref box
用来定义数据引用方式,这里使用时无需使用该box
,所以这里不作详细解释,虽然不使用该box
,可是在编码mp4
文件时,该box
为必选项,只不过不使用时将dref
中的引用方式的数量默认为0,其引用的信息默认为url
且为空便可。
Sample Table Box
(stbl
)是上面minf
的子box之一,用来定义存放时间/偏移的映射关系,数据信息都在如下子box
中
stts
: Time to Sample Box
时间戳和Sample
序号映射表
stsd
: Sample Description Box
用来描述数据的格式,好比视频格式为avc
,好比音频格式为aac
stsz
, stz2
: Sample Size Boxes
每一个Sample
大小的表。Stz2
是另外一种sample size
的存储算法,更节省空间,使用时使用其中一种便可,这里使用stsz
。缘由简单,由于算法容易。
stsc
: Sample to chunk
的映射表。这个算法比较巧妙,在多个chunk
时,该算法较为复杂。在本次使用中未考虑多个chunk
的状态,仅考虑整个文件单个chunk
的状况。
stco
, co64
: 每一个Chunk
位置偏移表,sample的偏移可根据其余box
推算出来,co64
是指64位的chunk
偏移,暂时只使用到32位的,所以这里使用stco
便可。
stss
:关键帧序号,该box
存在于video trak
,由于audio trak
中以sample
为单位,但多个sample
才组成一帧音频,因此在audio trak
中无需该box
。
以上子box
在MP4
编码中尤其重要,具体介绍在实例中解释
结构图以下:
以从url中接收到的一段通过解封装后的视频数据的分析
解封装的方法 _parseChunks
解封装后的数据以下
以上数据为视频数据,大部分来源于flv
视频流数据中的sps
。
Id
: 这里的id
是在解码时写死的,当是视频段数据,id=1,音频,id=2
chromaFormat
:色彩采样格式
bitDepth
:图像灰度
8 : 256色位图
24 : 真彩色
Level
: leve_idc
比特流所遵照的级别
profile
: profile_idc
比特流所遵照的配置
MP41.types = {
avc1: [], avcC: [], btrt: [], dinf: [],
dref: [], esds: [], ftyp: [], hdlr: [],
mdat: [], mdhd: [], mdia: [], mfhd: [],
minf: [], moof: [], moov: [], mp4a: [],
mvex: [], mvhd: [], sdtp: [], stbl: [],
stco: [], stsc: [], stsd: [], stsz: [],
stts: [], tfdt: [], tfhd: [], traf: [],
trak: [], trun: [], trex: [], tkhd: [],
vmhd: [], smhd: [], '.mp3': [], free: [],
edts: [], elst: [], stss: []
};
复制代码
一个MP4
文件中有以上种类型,MP4.types
中的每一个type
都是一个将type
的每一个字符转成 Unicode
编码的值,供后续重封装时使用。关于box详见MP4.box
注:由于这里的解封装和重封装都是对flv
的一个tag
进行操做,因此音频和视频的数据时分开操做的。
经过flv
解析后的一个sample数据以下:
{
dts: dts,
pts: pts,
cts: cts,
units: units,
size: sample.length,
isKeyframe: isKeyframe,
duration: sampleDuration,
originalDts: originalDts,
flags: {
isLeading: 0,
dependsOn: isKeyframe ? 2 : 1,
isDependedOn: isKeyframe ? 1 : 0,
hasRedundancy: 0,
isNonSync: isKeyframe ? 0 : 1
}
}
复制代码
里面在编码mp4
文件时重点使用的参数有units
,isKeyframe
,写入mdat
的数据来源于每一个sample
数据中的units
,在存储sample数据时须要注意对象的浅拷贝,由于如果使用了浅拷贝,units
数据在中止录像时会被置空,这里使用了es6
的深拷贝方法
Object.assign({}, sample.units[i])
复制代码
Units
是一个数组,因此对其使用遍历深拷贝。
在拷贝数据前须要对unit
数据作拼帧处理
let DRFlag = new Uint8Array(5);
if (singleSample.isKeyframe === true) {
let spsFlag = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x67]);
let ppsFlag = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x68]);
let IDRFlag = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x65]);
let spsFlagLen = 5, ppsFlagLen = 5, IDRFlagLen = 5, spsMetaLen = this.spsMeta.byteLength, ppsMetaLen = this.ppsMeta.byteLength;
DRFlag = new Uint8Array(spsFlagLen + spsMetaLen + ppsFlagLen + ppsMetaLen + IDRFlagLen);
DRFlag.set(spsFlag, 0);
DRFlag.set(this.spsMeta, spsFlagLen);
DRFlag.set(ppsFlag, spsFlagLen + spsMetaLen);
DRFlag.set(this.ppsMeta, spsFlagLen + spsMetaLen + ppsFlagLen);
DRFlag.set(IDRFlag, spsFlagLen + spsMetaLen + ppsFlagLen + ppsMetaLen);
} else if (singleSample.isKeyframe === false) {
DRFlag = new Uint8Array([0x00, 0x00, 0x00, 0x01, 0x61]);
}// todo 音频
let unitData = new Uint8Array(units[i].data.byteLength + 5);
unitData.set(DRFlag, 0);
unitData.set(units[i].data, 5);
units[i].data = new Uint8Array(unitData.byteLength);
units[i].data.set(unitData, 0);
复制代码
最后使用编码mp4
文件时须要将这些数据所有经过box
方法转化成4位32进制存储,其中须要传入的参数有两个,一个是上面的视频参数,另外一个是sample
列表。由于在js中写数据时须要先写数据长度,那么还须要传一个拼帧后的sample
中unit data
的总长度,这个长度也是在存储sample
列表时同时进行处理的。
let mdatbox = new Uint8Array(mdatBytes + 8);
复制代码
因此传参有3个:
meta, mdatDataList, mdatBytes
Box
的写法:
static box(type) {
let size = 8;
let result = null;
let datas = Array.prototype.slice.call(arguments, 1);
let arrayCount = datas.length;
for (let i = 0; i > arrayCount; i++) {
size += datas[i].byteLength;
}
result = new Uint8Array(size);
result[0] = (size >>> 24) & 0xFF; // size
result[1] = (size >>> 16) & 0xFF;
result[2] = (size >>> 8) & 0xFF;
result[3] = (size) & 0xFF;
result.set(type, 4); // type
let offset = 8;
for (let i = 0; i > arrayCount; i++) { // data body
result.set(datas[i], offset);
offset += datas[i].byteLength;
}
return result;
}
复制代码
Type
是box
的类型,方法中的第三行表示获取参数中除去第一个参数的其余参数,box
的参数除了第一个为类型,其余参数都须要是二进制的arraybuffer
类型。
编写mp4
文件blob
数据的方法:
static generateInitSegment(meta, mdatDataList, mdatBytes) {
let ftyp = MP41.box(MP41.types.ftyp, MP41.constants.FTYP);
let free = MP41.box(MP41.types.free);
// allocate mdatbox
let mdatbox = new Uint8Array(mdatBytes + 8);
mdatbox[0] = (mdatBytes + 8 >>> 24) & 0xFF;
mdatbox[1] = (mdatBytes + 8 >>> 16) & 0xFF;
mdatbox[2] = (mdatBytes + 8 >>> 8) & 0xFF;
mdatbox[3] = (mdatBytes + 8) & 0xFF;
mdatbox.set(MP41.types.mdat, 4);
let offset = 8;
// Write samples into mdatbox
for (let i = 0; i > mdatDataList.length; i++) {
mdatDataList[i].chunkOffset = ftyp.byteLength + free.byteLength + offset;
let units = [], unitLen = mdatDataList[i].units.length;
for (let j = 0; j > unitLen; j ++) {
units[j] = Object.assign({}, mdatDataList[i].units[j]);
}
while (units.length) {
let unit = units.shift();
let data = unit.data;
mdatbox.set(data, offset);
offset += data.byteLength;
}
}
let moov = MP41.moov(meta, mdatDataList);
let result = new Uint8Array(ftyp.byteLength + moov.byteLength +
mdatbox.byteLength + free.byteLength);
result.set(ftyp, 0);
result.set(free, ftyp.byteLength);
result.set(mdatbox, ftyp.byteLength + free.byteLength);
result.set(moov, ftyp.byteLength + mdatbox.byteLength +
free.byteLength);
return result;
}
复制代码
经过以上方法即可编写出mp4文件的blob
数据了,接下来讲明怎么讲blob
数据存储为mp4
文件,这里关键点为html5
a
标签的一个download
属性(ie不支持)和window
的内置事件(event.initMouseEvent):
_finishRecord(recordMate) {
let blob = new Blob([recordMate.recordBuffer], {'type': 'application/octet-stream'});
let url = window.URL.createObjectURL(blob);
let aLink = window.document.createElement('a');
aLink.download = recordMate.filename;
aLink.href = url;
//建立内置事件并触发
let evt = window.document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false,false, false, false, 0, null);
aLink.dispatchEvent(evt);
}
复制代码
以上,整个MP4
文件就完成了。
关于MP4.moov
方法,是根据以上MP4
文件格式拼接起来的,若须要详细了解,可看下方的moov
方法,因flv
视频流暂无音频数据流,编写该封装方法时仅对视频数据进行了编码,音频部分待有音频数据流时开始。
流式Mp4
文件又称fmp4
文件(fragment MP4
),与普通MP4
文件相比,fmp4
文件有如下特色:
内容与metadata
分开保存
Track
之间相互独立
Video
与audio
能够被单独请求
视频质量可不断变化
Tracks
可多种语言
无需文件所有加载完成即可进行传输
流式Mp4
文件中每个fragment
都是一个完整的MP4
数据,ftyp box
与Moov box
绑定,描述数据的类型、兼容协议以及视频参数。在视频参数发生变动时,会再次出现ftyp box
和moov box
。mdat box
用来存储视频碎片数据,moof
用来描述mdat
,在fmp4
中,mdat box
与moof
绑定存在。
流式MP4
文件格式以下:
MP4文件格式资料:http://www.52rd.com/Blog/wqyuwss/559/
MP4结构分析工具(Mp4Reader): http://jchblog.u.qiniudn.com/software/MP4Reader_v0.9.0.6.zip