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

相關文章
相關標籤/搜索