上一篇大致讲解了MSE的原理,不能光说不练。我们就来简单的来个例子。当然没有flv的解析,也没有fmp4的转换。主要是MPEG-DASH + MSE的操练。
首先先抓来一个mp4的视频,我们就命名为mse.mp4
吧。然后这里涉及到了切片的操作。其实切片跟HLS的原理是一样的,目的就是把一个长视频切成一小段一小段的,然后再去请求它们。这样能达到当需要切换码率的时候,可以通过请求不同的片段来达到目的。
MPEG-DASH 概念
MPEG-DASH, Dynamic Adaptive Streaming over HTTP。本身就是Adaptive Streaming的一种,与HLS是同样的作用。在MPEG-DASH规范中的MPD描述文件,主要是用来描述媒体文件的mimeType、codecs、segment信息等,MPD实际上就是一个xml文件。
切片方式
所以我们通过MP4Box
对mp4进行MPEG-DASH切片。参考MPEG-DASH Content Generation with MP4Box and x264 - Bitmovin
MPEG-DASH切片一般有以下两种方式:
- Adaptive Streaming:跟上面其实意思差不多,也是片段化操作,但是这个会事先把片段都切分好。也就是说我请求的是下一个片段而不是根据时间戳来请求。
- Progress Download:通过http Range header,获取每一个片段的range值,从而不断的拉取range的媒体流。举个例子,媒体文件就一个,但是我不断的请求媒体文件的播放起始时间戳之间的媒体流,从而达到片段化的操作。
Adaptive Streaming
1 | >>> MP4Box -dash 5000 -rap -segment-name output/segment_ mse.mp4 |
1、将mpd文件改为xml文件,并获取里面我们想要的信息,如mimeType,codecs。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var xhr = new XMLHttpRequest();
xhr.open('get', baseUrl + 'mse_dash.xml', true);
xhr.responseType = 'text';
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState === xhr.DONE) {
var output = xhr.response;
var parser = new DOMParser();
var xmlData = parser.parseFromString(tempoutput, "text/xml", 0);
getFileType(xmlData);
}
};
function getFileType(data) {
try {
var file = data.querySelectorAll('Representation')[0];
mimeType = file.getAttribute('mimeType');
codecs = file.getAttribute('codecs');
} catch(e) { }
}
2、实例化Media Source对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28if (window.MediaSource) {
var mediaSource = new window.MediaSource();
var url = URL.createObjectURL(mediaSource);
videoEle = document.getElementsByTagName('video')[0];
videoEle.src = url;
mediaSource.addEventListener('sourceopen', function(e) {
try {
var tempStr = mimeType + '; codecs="' + codecs + '"';
if (MediaSource.isTypeSupported(tempStr)) {
videoSource = mediaSource.addSourceBuffer(tempStr);
videoSource.addEventListener('updateend', nextSegment);
initVideo();
}
} catch(e) {
console.error('source open error', e.message);
return;
}
}, false);
mediaSource.addEventListener('sourceclose', function(e) {
console.log('media source close...', e.message);
}, false);
mediaSource.addEventListener('error', function(e) {
console.log('media source error...', e.message);
}, false);
}
3、加载媒体文件初始化片段的信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36function initVideo() {
getSeg(baseUrl + 'output/segment_init.mp4', appendToBuffer);
var playPromise = videoEle.play();
// 这里做下promise的处理,参考这里https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
if (playPromise !== undefined) {
playPromise.then(function() {
console.log('play promise then');
}).catch(function(err) {
console.log('play promise error', err);
})
}
}
function getSeg(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function(e) {
if (xhr.status != 200) return false;
callback(xhr.response);
};
xhr.send();
}
function appendToBuffer(videoChunk) {
if (videoChunk) {
try {
videoSource.appendBuffer(new Uint8Array(videoChunk));
} catch(e) {
console.error(e.message);
}
}
}
4、不断的添加media segment1
2
3
4
5
6
7
8
9function nextSegment() {
++index;
if (index <= maxChunksIndex) {
var url = baseUrl + 'output/segment_' + index + '.m4s';
getSeg(url, appendToBuffer);
} else {
videoSource.removeEventListener('updateend', nextSegment);
}
}
Progress Download
跟之前一样,先进行媒体流的片段切片。1
2
3# 这里不再解释
>>> MP4Box -dash 10000 -frag 10000 -rap mse.mp4
# 假如上面的操作没有出现MPD文件,那么需要通过ffmpeg转为fmp4文件,再用MP4Box切片
Progress Download跟Adaptive Streaming不同的是,它需要计算每一个segment duration。1
2
3
4const time = (size * 8) / bitrate
// 即在MPD获取相对应的信息之后,代码如下
var ranges = mediaRange.split('-');
var time = (ranges[1] - ranges[0]) * 8 / bandwidth;
实际上,关于Progress Download,可以移步到构建简单的 MPEG-DASH 流媒体播放器,这里只是对其做最简单的演示。
1、第一步其实跟上面的差不多,只是多了几个字段,bandwidth(做segment duration计算),segments(每一个segment字段),initRange(就是上面的segment_init.mp4
)。
2、实例化Media Source对象。一样的操作,熟悉的味道。
3、加载媒体文件初始化片段的信息。
1 | // 没给出的代码基本跟上面一致 |
4、不断的添加media segment1
2
3
4
5
6
7
8
9
10function nextSegment() {
if (index <= segments.length - 1) {
var segment = segments[index];
var range = segment.getAttribute('mediaRange');
getSeg(range, baseFile, appendToBuffer);
++index;
} else {
videoSource.removeEventListener('updateend', nextSegment);
}
}
到这里为止,已经讲得差不多了。基本上从flv.js的解析再到mse的实践,关于媒体的学习先告一段落。但是这一部分本身就是概念很多坑很多,若以后有什么可以分享或者什么坑,我都会到此记录下来。