MP4格式研究
mp4解析在线地址:mp4parser
一个mp4就是一个容器: 每个mp4是由多个box组成,每个box可以嵌套子box,这个box称为container box。
- Box:每个Box由Header和Data组成。
- Header:包含了整个Box的长度size和类型type。当size==0时,代表这是文件中最后一个Box;当size==1时,意味着Box长度需要更多bits来描述,在后面会定义一个64bits的largesize描述Box的长度;当type是uuid时,代表Box中的数据是用户自定义扩展类型。
- Data:是Box的实际数据,可以是纯数据也可以是更多的子Boxes。
mp4分有传统的regular mp4, 和为适应流媒体的fragmented Mp4(fMp4)。
- regular mp4 主要由ftyp,moov/mdat,mdat/moov等box组成。
- ftypbox,在文件的开始位置,描述的文件的版本、兼容协议等;
- moovbox,这个box中不包含具体媒体数据,但包含本文件中所有媒体数据的宏观描述信息,moov box下有mvhd和trak box。
- mvhd中记录了创建时间、修改时间、时间度量标尺、可播放时长等信息。
- trak中的一系列子box描述了每个媒体轨道的具体信息。
在fMp4格式中包含一系列的segments(moof+mdat的组合),这些segments可以被独立的request(利用byte-range request),这有利于在不同质量级别的码流之间做码率切换操作
在regular mp4中,如果我们要在两个码流之间做码率切换,就需要找到两个码流中对应时间点的byte position,然而这时候我们只有一个巨大的mdat box,要在这里面找到一个具体的byte position无疑是复杂的。而且,在regular mp4中,有时moov会在巨大的mdat box之后,这也会影响起播的速度。
moofbox(此box存在于fmp4中),这个box是视频分片的描述信息。并不是MP4文件必须的部分,但在我们常见的可在线播放的MP4格式文件中确是重中之重。
- mdatbox,实际媒体数据。我们最终解码播放的数据都在这里面。
- 其他box
- Free Space Box(free或skip) “free”中的内容是无关紧要的,可以被忽略。该box被删除后,不会对播放产生任何影响。
- sidx box是segment index box, 是fmp4的分片索引box。开发DASH时,需要解析这里的box,定位到对应的视频数据。
sidx解析规则:
box的header部分,分别为4B的size大小,4B的type类型(Unicode 值,需要转码成字符串)。
box的data部分,关于DASH最有用的是referenced_size(单位:字节)和subsegment_duration(单位:毫秒)。个数就是fmp4的分片数目,数目为:type|size|duration * reference_count
sidx内容规则如下图:
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标签是不支持流媒体播放的,那么它怎么在不完全加载全部视频文件的情况下,就开始播放视频的呢?原理是,首先(请求range: bytes=0-)加载mp4头部的一小部分(chunk),解析出ftyp和moov,用来定位一帧的位置,在发出对应的range请求,来实现定位播放,以及提前播放。是最终在内存中下载开始播放位置的整个视频。这个视频后端需要提供range请求服务。
- 为了让video支持流媒体播放,需要使用MSE,MSE原理是开辟一块内存,提供存放流媒体的原始数据,用以video标签能够播放fmp4视频。
- 为了让video标签能够播放本地blob数据,有两种方法:
- 使用data uri 格式的数据,可以使用FileReader对象将blob数据转成data uri。格式为:data:[<MIME type>][;charset=<charset>][;base64],<encoded data>
- 使用URL对象,指向blob对象。格式为:blob:same origin/pointer
- 使用data uri的话,编码会转成base64,会增大文件size,并且是直接打在页面上,有损性能,使用URL对象的话,解决如上问题
- URL对象可以使用URL.createObjectURL(blob)生成。
- URL对象可以指向硬盘或者内存空间中的文件,以URL的形式赋给video,audio或者img等标签,来使得浏览器获取本地文件,减少http请求数量。
其他技术知识点
- 传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。若果设置为arraybuffer,就可以直接获得arraybuffer对象。blob还需要通过FileReader转成arraybuffer。
- cors请求,当设置了header头时,会触发浏览器的预检option请求。
- 播放器全屏:
- 由于video标签使用requestFullScreen,会漏出shadow dom。而自定义的控制栏被隐藏。所以应该使用video外层的节点进行全屏。
- 退出全屏需要特殊处理一下esc键,需要通过document.fullscreenEnabled || window.fullScreen || document.webkitIsFullScreen ||
- document.msFullscreenEnabled检测下当前是否在全屏状态下。否则在退出全屏并检测esc时,浏览器会吧esc键给屏蔽掉。 理解有误,最新理解是退出全屏需要特殊处理一下esc键,需要检测下当前是否在全屏状态下。否则在退出全屏并检测esc时,浏览器会吧esc键给屏蔽掉。
- document.fullscreenElement: 当前处于全屏状态的元素 element。
- document.fullscreenEnabled: 标记 fullscreen 当前是否可用。
- 当进入/退出 全屏模式时,会触发 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标签,视频资源地址必须添加明确的视频扩展名,或者添加source标签,明确指定type。否则播放不了。-
-
视频网站使用的流媒体技术方案
- youtube DASH video/webm ajax get url后加range CORS请求分片
- 腾讯视频 HLS m3u8+ts CORS请求
- 爱奇艺 RTMP f4v url后加range CORS请求分片
- bilibili DASH m4s 在请求体内加range 触发options来请求分片
- 优酷 HLS m3u8+ts url后加参数 CORS请求分片 但是切换分辨率有缝隙
网友评论