MP4格式研究
mp4解析在線地址:mp4parser
一個(gè)mp4就是一個(gè)容器: 每個(gè)mp4是由多個(gè)box組成奶躯,每個(gè)box可以嵌套子box,這個(gè)box稱為container box亿驾。
- Box:每個(gè)Box由Header和Data組成嘹黔。
- Header:包含了整個(gè)Box的長度size和類型type。當(dāng)size==0時(shí)莫瞬,代表這是文件中最后一個(gè)Box儡蔓;當(dāng)size==1時(shí),意味著Box長度需要更多bits來描述疼邀,在后面會定義一個(gè)64bits的largesize描述Box的長度喂江;當(dāng)type是uuid時(shí),代表Box中的數(shù)據(jù)是用戶自定義擴(kuò)展類型旁振。
- Data:是Box的實(shí)際數(shù)據(jù)获询,可以是純數(shù)據(jù)也可以是更多的子Boxes。
mp4分有傳統(tǒng)的regular mp4, 和為適應(yīng)流媒體的fragmented Mp4(fMp4)拐袜。
- regular mp4 主要由ftyp吉嚣,moov/mdat,mdat/moov等box組成蹬铺。
- ftypbox尝哆,在文件的開始位置,描述的文件的版本甜攀、兼容協(xié)議等秋泄;
- moovbox,這個(gè)box中不包含具體媒體數(shù)據(jù)规阀,但包含本文件中所有媒體數(shù)據(jù)的宏觀描述信息恒序,moov box下有mvhd和trak box。
- mvhd中記錄了創(chuàng)建時(shí)間姥敛、修改時(shí)間奸焙、時(shí)間度量標(biāo)尺、可播放時(shí)長等信息彤敛。
- trak中的一系列子box描述了每個(gè)媒體軌道的具體信息。
在fMp4格式中包含一系列的segments(moof+mdat的組合)了赌,這些segments可以被獨(dú)立的request(利用byte-range request)墨榄,這有利于在不同質(zhì)量級別的碼流之間做碼率切換操作
在regular mp4中,如果我們要在兩個(gè)碼流之間做碼率切換勿她,就需要找到兩個(gè)碼流中對應(yīng)時(shí)間點(diǎn)的byte position袄秩,然而這時(shí)候我們只有一個(gè)巨大的mdat box,要在這里面找到一個(gè)具體的byte position無疑是復(fù)雜的。而且之剧,在regular mp4中郭卫,有時(shí)moov會在巨大的mdat box之后,這也會影響起播的速度背稼。
moofbox(此box存在于fmp4中)贰军,這個(gè)box是視頻分片的描述信息。并不是MP4文件必須的部分蟹肘,但在我們常見的可在線播放的MP4格式文件中確是重中之重词疼。
- mdatbox,實(shí)際媒體數(shù)據(jù)帘腹。我們最終解碼播放的數(shù)據(jù)都在這里面贰盗。
- 其他box
- Free Space Box(free或skip) “free”中的內(nèi)容是無關(guān)緊要的,可以被忽略阳欲。該box被刪除后舵盈,不會對播放產(chǎn)生任何影響。
- sidx box是segment index box, 是fmp4的分片索引box球化。開發(fā)DASH時(shí)书释,需要解析這里的box,定位到對應(yīng)的視頻數(shù)據(jù)赊窥。
sidx解析規(guī)則:
box的header部分爆惧,分別為4B的size大小,4B的type類型(Unicode 值锨能,需要轉(zhuǎn)碼成字符串)扯再。
box的data部分,關(guān)于DASH最有用的是referenced_size(單位:字節(jié))和subsegment_duration(單位:毫秒)址遇。個(gè)數(shù)就是fmp4的分片數(shù)目熄阻,數(shù)目為:type|size|duration * reference_count
sidx內(nèi)容規(guī)則如下圖:
sidx | 名字 | 大小 |
---|---|---|
header | size | Uint32 |
header | type | Int32 |
header | larsesize(if size==1) | Uint64 |
data | version | Uint8 |
data | none | Uint24 |
data | reference_ID | Uint32 |
data | timescale | Uint32 |
data | earliest_presentation_time | Uint32 |
data | first_offset | Uint32 |
data | reserved | Uint16 |
data | reference_count | Uint16 |
data | type/size/duration * reference_count | Uint32 |
data | SAP * reference_count | Uint32 |
解析代碼如下
const mp4 = {
parseSidx (dataView, offset) {
/*
注意:
此方法只能解析sidx -> dash只需解析sidx就行
以及header里面size不等于1和0的情況 -> 這里需要將來支持一下
以及data部分version為0的情況
*/
let hex2a = function (hex) {
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
};
let trim1 = function (str) {
return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};
const msg = {};
msg.len = dataView.getUint32(offset); offset += 4;//獲取header頭size
let type = dataView.getInt32(offset); offset += 4;
msg.type = trim1(hex2a(type.toString(16)));//獲取header頭type
msg.version = dataView.getUint8(offset); offset += 4;//獲取version
msg.reference_ID = dataView.getUint32(offset);offset += 4;//獲取reference_ID
msg.timescale = dataView.getUint32(offset);offset += 4;//timescale
msg.earliest_presentation_time = dataView.getUint32(offset); offset += 4;//earliest_presentation_time
msg.first_offset = dataView.getUint32(offset); offset += 4;//first_offset
msg.reserved = dataView.getUint16(offset) ;offset += 2;//reserved
msg.reference_count = dataView.getUint16(offset) ;offset += 2;//reference_count
msg.entries = [];
let reference_count = msg.reference_count;
while (reference_count--) {
let entry = {};
let fourBytes = dataView.getUint32(offset); offset += 4;
entry['reference_type'] = (fourBytes >> 31) & 1;
entry['referenced_size'] = (fourBytes & 0x7fffffff);
entry['subsegment_duration'] = dataView.getUint32(offset);offset += 4;
fourBytes = dataView.getUint32(offset);offset += 4;
entry['starts_with_SAP'] = (fourBytes >> 31) & 1;
entry['SAP_type'] = (fourBytes >> 29) & 7;
entry['SAP_delta_time'] = (fourBytes & 0x0fffffff);
msg.entries.push(entry);
}
return msg;
}
};
export {mp4};
video播放原理
- 原生video標(biāo)簽是不支持流媒體播放的,那么它怎么在不完全加載全部視頻文件的情況下倔约,就開始播放視頻的呢秃殉?原理是,首先(請求range: bytes=0-)加載mp4頭部的一小部分(chunk)浸剩,解析出ftyp和moov钾军,用來定位一幀的位置,在發(fā)出對應(yīng)的range請求绢要,來實(shí)現(xiàn)定位播放吏恭,以及提前播放。是最終在內(nèi)存中下載開始播放位置的整個(gè)視頻重罪。這個(gè)視頻后端需要提供range請求服務(wù)樱哼。
- 為了讓video支持流媒體播放哀九,需要使用MSE,MSE原理是開辟一塊內(nèi)存搅幅,提供存放流媒體的原始數(shù)據(jù)阅束,用以video標(biāo)簽?zāi)軌虿シ舊mp4視頻。
- 為了讓video標(biāo)簽?zāi)軌虿シ疟镜豣lob數(shù)據(jù)茄唐,有兩種方法:
- 使用data uri 格式的數(shù)據(jù)息裸,可以使用FileReader對象將blob數(shù)據(jù)轉(zhuǎn)成data uri。格式為:data:[<MIME type>][;charset=<charset>][;base64],<encoded data>
- 使用URL對象琢融,指向blob對象界牡。格式為:blob:same origin/pointer
- 使用data uri的話,編碼會轉(zhuǎn)成base64漾抬,會增大文件size宿亡,并且是直接打在頁面上,有損性能纳令,使用URL對象的話挽荠,解決如上問題
- URL對象可以使用URL.createObjectURL(blob)生成。
- URL對象可以指向硬盤或者內(nèi)存空間中的文件平绩,以URL的形式賦給video圈匆,audio或者img等標(biāo)簽,來使得瀏覽器獲取本地文件捏雌,減少http請求數(shù)量跃赚。
其他技術(shù)知識點(diǎn)
- 傳統(tǒng)上,服務(wù)器通過 AJAX 操作只能返回文本數(shù)據(jù)性湿,即responseType屬性默認(rèn)為text纬傲。XMLHttpRequest第二版XHR2允許服務(wù)器返回二進(jìn)制數(shù)據(jù),這時(shí)分成兩種情況肤频。如果明確知道返回的二進(jìn)制數(shù)據(jù)類型叹括,可以把返回類型(responseType)設(shè)為arraybuffer;如果不知道宵荒,就設(shè)為blob汁雷。若果設(shè)置為arraybuffer,就可以直接獲得arraybuffer對象报咳。blob還需要通過FileReader轉(zhuǎn)成arraybuffer侠讯。
- cors請求,當(dāng)設(shè)置了header頭時(shí)少孝,會觸發(fā)瀏覽器的預(yù)檢option請求继低。
- 播放器全屏:
- 由于video標(biāo)簽使用requestFullScreen,會漏出shadow dom。而自定義的控制欄被隱藏稍走。所以應(yīng)該使用video外層的節(jié)點(diǎn)進(jìn)行全屏袁翁。
- 退出全屏需要特殊處理一下esc鍵,需要通過document.fullscreenEnabled || window.fullScreen || document.webkitIsFullScreen ||
- document.msFullscreenEnabled檢測下當(dāng)前是否在全屏狀態(tài)下婿脸。否則在退出全屏并檢測esc時(shí)粱胜,瀏覽器會吧esc鍵給屏蔽掉。 理解有誤狐树,最新理解是退出全屏需要特殊處理一下esc鍵焙压,需要檢測下當(dāng)前是否在全屏狀態(tài)下。否則在退出全屏并檢測esc時(shí)抑钟,瀏覽器會吧esc鍵給屏蔽掉涯曲。
- document.fullscreenElement: 當(dāng)前處于全屏狀態(tài)的元素 element。
- document.fullscreenEnabled: 標(biāo)記 fullscreen 當(dāng)前是否可用在塔。
- 當(dāng)進(jìn)入/退出 全屏模式時(shí),會觸發(fā) fullscreenchange 事件幻件。
代碼如下:
const api = {
requestFullScreen : () => {
const el = this.model.BaseNode;
const rfs = el.requestFullScreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen;
if(typeof rfs != "undefined" && rfs) {
rfs.call(el);
utils.evt.addEvent(window, 'resize', this.evtHandler.fullWindowOnEscKey);
};
return;
},
exitFullScreen : () => {
if (document.exitFullscreen) {
document.exitFullscreen();
}
else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
}
else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
}
else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
utils.evt.removeEvent(window, 'resize', this.evtHandler.fullWindowOnEscKey);
},
checkFull : () => {
//let isFUll = document.fullscreenEnabled || window.fullScreen || document.webkitIsFullScreen || document.msFullscreenEnabled || false;
let isFull = document.fullscreenElement || document.webkitCurrentFullScreenElement || document.mozFullScreenElement || null;
return isFUll;
},
};
const evtHandler = {
fullWindowOnEscKey : (evt) => {
if (!this.tools.checkFull()) {
utils.dom.removeClassName(this.model.BaseNode, 'krv-fullscreen');
this.tools.exitFullScreen();
}
}
};
-
瀏覽器兼容問題:
- safari瀏覽器video標(biāo)簽,視頻資源地址必須添加明確的視頻擴(kuò)展名蛔溃,或者添加source標(biāo)簽绰沥,明確指定type。否則播放不了贺待。-
-
視頻網(wǎng)站使用的流媒體技術(shù)方案
- youtube DASH video/webm ajax get url后加range CORS請求分片
- 騰訊視頻 HLS m3u8+ts CORS請求
- 愛奇藝 RTMP f4v url后加range CORS請求分片
- bilibili DASH m4s 在請求體內(nèi)加range 觸發(fā)options來請求分片
- 優(yōu)酷 HLS m3u8+ts url后加參數(shù) CORS請求分片 但是切換分辨率有縫隙