js实现封装MP4格式文件并下载

注:基于bilibili的FLV.js实现javascript

flv.js的github地址:github.com/Bilibili/fl…html

MP4文件格式

综述

在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

MP4.box

在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

Boxheaderbody组成,以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文件格式以下图

FTYP box

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

Mdat box 中包含了MP4文件的媒体数据,在文件中的位置能够在moov的前面,也能够在moov的后面,因咱们这里用到MP4文件格式用来写mp4文件,须要计算每一帧媒体数据在文件中的偏移量,为了方便计算,mdat放置moov前面。

Mdat box数据格式单一,无子box。主要分为box headerbox bodybox header中存放box sizebox typemdat),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 sizelarge size中,一样在写mp4文件时,若须要large size,须要将box size位配置为1。

Moov box

Moov box中存放着媒体信息,上面提到的stbl里存放帧信息,属于媒体信息,也在moov box里。Moov box 用来描述媒体数据。

Moov box 主要包含 mvhdtrakmvex三种子box。

Mvhd 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 scaleDuration,其余的使用默认值便可。

Trak box

一个Track box定义了movie中的一个track。一部movie能够包含一个或多个tracks,它们之间相互独立,各自有各自的时间和空间信息。每一个track box 都有与之关联的mdat box

Track主要有如下目的:

  1. 包含媒体数据引用和描述

  2. 包含modifier track

  3. 流媒体协议的打包信息(hint trak),引用或者复用对应的媒体sample dataHint tracksmodifier tracks必须保证完整性,同时和至少一个media track一块儿存在。换句话说,即便hint tracks复制了对应的媒体sample datamedia tracks 也不能从一部hinted movie中删除。

    写mp4时仅用到第一个目的,因此这里只介绍媒体数据的引用和描述。

    一个trak box通常主要包含了tkhd box、 edts box 、mdia box

Tkhd 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

Elst box

该box为edst box的惟一子box,不是全部的MP4文件都有edst box,这个box是使其对应的trak box的时间戳产生偏移。暂时未发现须要该偏移量的地方,编码时也未对该box进行编码。

Mdia box

box定义了trak box的类型和sample的信息。

header box--- mdhd box 定义了该boxtimescaleduration(注:这里的这两个参数与前面说的mvhd有区别,这里的这两个参数都是以一个sample为时间单位的,例:在只有一个视频trak的状况下,mvhdtimescale为1000,一个sampleduration为40 ,那么这里的timescale为1000/40,同理这里的duration算法与之同样理解。)

Hdlr box 定义了这段trak的媒体处理组件,如下图会更清晰的解释这个box

Minf box

box也是上面的mdia box的子box,其主要用来描述该trak的具体的媒体处理组件内容的。

header box根据trak的类型有2种,vmhdsmhd,二者没有什么特殊的数据,只是为了定义headle的类型。

其子box --- dinf box 用来定义媒体处理组件如何获取媒体数据的,dinf box的子box --- dref box用来定义数据引用方式,这里使用时无需使用该box,所以这里不作详细解释,虽然不使用该box,可是在编码mp4文件时,该box为必选项,只不过不使用时将dref中的引用方式的数量默认为0,其引用的信息默认为url且为空便可。

Stbl box

Sample Table Boxstbl)是上面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

以上子boxMP4编码中尤其重要,具体介绍在实例中解释

结构图以下:

实例:

以从url中接收到的一段通过解封装后的视频数据的分析

解封装的方法 _parseChunks

解封装后的数据以下

以上数据为视频数据,大部分来源于flv视频流数据中的sps

Id 这里的id是在解码时写死的,当是视频段数据,id=1,音频,id=2

chromaFormat :色彩采样格式

bitDepth:图像灰度

8 : 256色位图

24 : 真彩色

Levelleve_idc 比特流所遵照的级别

profileprofile_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文件时重点使用的参数有unitsisKeyframe,写入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中写数据时须要先写数据长度,那么还须要传一个拼帧后的sampleunit 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;
}
复制代码

Typebox的类型,方法中的第三行表示获取参数中除去第一个参数的其余参数,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视频流暂无音频数据流,编写该封装方法时仅对视频数据进行了编码,音频部分待有音频数据流时开始。

存在问题:

  1. 目前仅支持视频编码

扩展

流式MP4

流式Mp4文件又称fmp4文件(fragment MP4),与普通MP4文件相比,fmp4文件有如下特色:

  1. 内容与metadata分开保存

  2. Track之间相互独立

  3. Videoaudio能够被单独请求

  4. 视频质量可不断变化

  5. Tracks可多种语言

  6. 无需文件所有加载完成即可进行传输

    流式Mp4文件中每个fragment都是一个完整的MP4数据,ftyp boxMoov box绑定,描述数据的类型、兼容协议以及视频参数。在视频参数发生变动时,会再次出现ftyp boxmoov boxmdat box用来存储视频碎片数据,moof用来描述mdat,在fmp4中,mdat boxmoof绑定存在。

    流式MP4文件格式以下:

附录:

MP4文件格式资料:http://www.52rd.com/Blog/wqyuwss/559/

MP4结构分析工具(Mp4Reader): http://jchblog.u.qiniudn.com/software/MP4Reader_v0.9.0.6.zip

相关文章
相关标签/搜索