Lucius's Blog

音视频学习-MSE实践篇

上一篇大致讲解了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
2
3
4
5
6
7
>>> MP4Box -dash 5000 -rap -segment-name output/segment_ mse.mp4

// _dash.mpd: 将其改为xml或者直接在编辑器打开,就会发现里面放着的是整个媒体文件的信息以及每一段segment的位置。
// *_init.mp4: 初始的mp4文件,相当于视频头,在这个头文件中包含了完整的视频元信息(ftyp + moov),具体的可以使用 MP4Box <init video> -info 查看。
// *.m4s: 即上面提到的Segments文件,每个m4s仅包含媒体信息 (moof + mdat),而播放器是不能直接播放这个文件的,需要用支持DASH的播放器从init文件开始播放。
// -rap: random access point
// -segment-name: 生成的切片名字前缀,如segment_1.m4s

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
22
var 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
28
if (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
36
function 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 segment

1
2
3
4
5
6
7
8
9
function 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
4
const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 没给出的代码基本跟上面一致
function initVideo() {
getSeg(initRange, baseFile, appendToBuffer);
}

function getSeg(range, url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.setRequestHeader('Range', 'bytes=' + range);
xhr.responseType = 'arraybuffer';
xhr.send();

try {
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState === xhr.DONE) {
callback(xhr.response);
}
}, false);
} catch(e) {
console.error(e.message);
}
}

4、不断的添加media segment

1
2
3
4
5
6
7
8
9
10
function 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的实践,关于媒体的学习先告一段落。但是这一部分本身就是概念很多坑很多,若以后有什么可以分享或者什么坑,我都会到此记录下来。

参考资料

我只是试下能不能被赞赏😳