js實現封裝MP4格式檔案並下載

九酒發表於2019-03-03

注:基於bilibili的FLV.js實現

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

MP4檔案格式

綜述

在MP4檔案格式中,整個視訊容器都是由多個box和子box組成,根據box型別主要分為3大類:視訊型別(ftyp)、視訊資料(mdat)、視訊資訊(moov)。視訊資訊(moov)用來描述視訊資料(mdat)。(注:還有一個主要box為moof box,因這裡僅解釋普通MP4格式資料,moof box僅在流式MP4中使用。在流式MP4格式中,box排序、相同box的box body內容格式與普通MP4不一樣,詳情參見擴充套件

視訊引數(moov)中主要的子box 為track,每個track都是一個隨時間變化的媒體序列,時間單位為一個sample,可以是一幀資料,或者音訊(注意,一幀音訊可以分解成多個音訊sample,所以音訊一般用sample作為單位,而不用幀)Sample按照事件順序排列。track裡面的每個sample通過引用關聯到一個sample description。這個sample descriptios定義了怎樣解碼這個sample,例如使用的壓縮演算法。(注:在目前的使用中,該值為1)

注:該文主要介紹普通mp4檔案型別

MP4.box

在Javascript中,Mp4 的所有box全部由通過new Uint8Array() 實現。

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中的資料是使用者自定義擴充套件型別。

Boxheaderbody組成,以32位的4位元組整數儲存方式儲存到記憶體,開頭4個位元組(32位)為box size,後面緊跟的4位為box的型別。Box body可以由資料組成,也可以由子box組成。

一個box的結構如下:

js實現封裝MP4格式檔案並下載

視訊與音訊的引數不一樣,一般情況下一個MP4檔案區分為2個trak,一個為video trak,另一個是audio trak。每個track都有trakId,視訊的trakId為1,音訊的trakId為2。

整個MP4檔案格式如下圖

js實現封裝MP4格式檔案並下載
js實現封裝MP4格式檔案並下載

FTYP box

Ftypbox 是一個由四個字元組成的碼字,用來表示編碼型別、相容協議或者媒體檔案的用途。

在普通MP4檔案中,ftyp box有且僅有一個,在檔案的開始位置。

js實現封裝MP4格式檔案並下載

通過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視訊幀資料型別如下:

js實現封裝MP4格式檔案並下載

注:1、在目前實現中,I幀資料中暫不包含序列引數集(sps)和影像引數集(pps)。

2、以上幀資料僅針對視訊幀資料。

在普通mp4中,在獲取資料之前,需要解析每個幀資料所在位置,每個幀資料都存放在mdat中,而這些幀的資訊全部存放在stbl box 中,所以,若要mp4檔案能夠正常播放,需要在寫mp4檔案時,將所有的幀資料資訊寫入
stbl box中。

js實現封裝MP4格式檔案並下載

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 scale
Duration,其他的使用預設值即可。

js實現封裝MP4格式檔案並下載

Trak box

一個Track box定義了movie中的一個track。一部movie可以包含一個或多個tracks,它們之間相互獨立,各自有各自的時間和空間資訊。每個track box 都有與之關聯的mdat box

Track主要有以下目的:

  1. 包含媒體資料引用和描述

  2. 包含modifier track

  3. 流媒體協議的打包資訊(hint trak),引用或者複用對應的媒體sample data
    Hint 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 boxmdhd box 定義了該boxtimescale
duration(注:這裡的這兩個引數與前面說的mvhd有區別,這裡的這兩個引數都是以一個sample為時間單位的,例:在只有一個視訊trak的情況下,mvhdtimescale為1000,一個sampleduration為40
,那麼這裡的timescale為1000/40,同理這裡的duration演算法與之一樣理解。)

Hdlr box 定義了這段trak的媒體處理元件,以下圖會更清晰的解釋這個box

js實現封裝MP4格式檔案並下載
js實現封裝MP4格式檔案並下載

Minf box

box也是上面的mdia box的子box,其主要用來描述該trak的具體的媒體處理元件內容的。

header box根據trak的型別有2種,vmhd
smhd,兩者沒有什麼特殊的資料,只是為了定義headle的型別。

其子boxdinf box 用來定義媒體處理元件如何獲取媒體資料的,dinf box的子boxdref 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

js實現封裝MP4格式檔案並下載

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編碼中尤為重要,具體介紹在例項中解釋

結構圖如下:

js實現封裝MP4格式檔案並下載

例項:

以從url中接收到的一段經過解封裝後的視訊資料的分析

解封裝的方法 _parseChunks

解封裝後的資料如下

js實現封裝MP4格式檔案並下載

以上資料為視訊資料,大部分來源於flv視訊流資料中的sps

Id 這裡的id是在解碼時寫死的,當是視訊段資料,id=1,音訊,id=2

chromaFormat :色彩取樣格式

js實現封裝MP4格式檔案並下載

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檔案格式如下:

js實現封裝MP4格式檔案並下載

附錄:

MP4檔案格式資料:http://www.52rd.com/Blog/wqyuwss/559/

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

相關文章