MPEG-DASH video concept

在html5中影片播放常用方式大致上分為兩種:

  • Progress Download
  • Adaptive Streaming

Progress Download

這是一種最容易實踐的方式,主要是透過http Range的header,跟server要取需要的檔案區塊,然後存在暫存檔案。實作面只需要透過video.src或者source.src指定好來源檔案,再搭配有支援range header的server,如nginxapache等的。

Adaptive Streaming

Progress類似,最大不同會先將原先的影片切片,依照描述檔或當初切割的時間,去抓取所需要的片段。同樣可以透過http Range去針對單個檔案抓去區塊,或者直接分割成多個檔案,依照url去區分需要抓取的檔案。若在html5中,可透過MSE(Media Source Extensions),將片段資料寫入。


MPEG-DASH串流概念

MPEG-DASH (Dynamic Adaptive Streaming over HTTP)本身就是Adaptive Streaming的一種,與apple的HLS (HTTP Live Streaming)是同樣的作用。在MPEG-DASH規格中的MPD描述檔案,主要是用來描述檔案的mimeTypecodecssegment資訊等等的,MPD實際上是一個xml檔案。

在瀏覽器的實作方式,主要是透過MediaSource這個元件,建立一個sourceBuffer,接著透過MPD所描述的Initialization這個初始區塊,依照Range去向server端要取所需要的segment,最後透過sourceBuffer.appendBuffer將抓取的資料塞入,即可完成初始化。然後你只要依照時間推算出你所需要的SegmentURL區塊,用一樣的方式把資料塞回sourceBuffer.appendBuffer即可。

每一個segment的時間公式計算如下:
1
time = (size * 8) / bitrate

套用MPD中的SegmentURL

1
2
var ranges = mediaRange.split('-');
var time = (ranges[1] - ranges[0]) * 8 / bandwidth;

產生MPD格式

需要產生MPD檔,及對應的mp4,你可以透過MP4Box建立:

1
MP4Box -dash 10000 -frag 1000 -rap bunny.mp4

若是無法產出MPD檔案,代表你的mp4格式是需要經過處理,你可以透過mp4fragment去轉換:

1
mp4fragment ~/Movies/bunny.mp4 fragmented.mp4

如果你不想透過mp4fragment,你也能使用ffmpeg

1
ffmpeg -i bunny.mp4 -movflags frag_keyframe+empty_moov fragmented.mp4

在你執行完MP4Box -dash 10000 -frag 1000 -rap bunny.mp4後,應該會得到.mpdinit.mp4

1
2
├── bunny_dash.mpd
└── bunny_dashinit.mp4

產出來的xml格式會如下:

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
<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H30M0.157S" maxSegmentDuration="PT0H0M10.000S" profiles="urn:mpeg:dash:profile:full:2011">
<ProgramInformation moreInformationURL="http://gpac.sourceforge.net">
<Title>bunny_dash.mpd generated by GPAC</Title>
</ProgramInformation>

<Period duration="PT0H30M0.157S">
<AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720" maxFrameRate="7" par="16:9" lang="und">
<ContentComponent id="1" contentType="video" />
<ContentComponent id="2" contentType="audio" />
<Representation id="1" mimeType="video/mp4" codecs="avc3.64001f,mp4a.40.2" width="1280" height="720" frameRate="7" sar="1:1" audioSamplingRate="44100" startWithSAP="1" bandwidth="400359">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>bunny_dashinit.mp4</BaseURL>
<SegmentList timescale="1000" duration="10000">
<Initialization range="0-3330"/>
<SegmentURL mediaRange="3331-826541" indexRange="3331-3482"/>
<SegmentURL mediaRange="826542-1664049" indexRange="826542-826693"/>
<SegmentURL mediaRange="1664050-2506385" indexRange="1664050-1664201"/>
<SegmentURL mediaRange="2506386-2936161" indexRange="2506386-2506489"/>
<SegmentURL mediaRange="2936162-3767192" indexRange="2936162-2936313"/>
...
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>

其中一定會使用到的有RepresentationmimeTypecodecs作為MediaSource.addSourceBuffer初始化用,而bandwidth屬性則會與SegmentURLmediaRange用來計算時間,Initialization則是剛開始初始化區需要。


在HTML5中使用MPD格式

在這邊以AJAX已經取得MPD檔案為前提來實作,主要都是片段程式碼,若你要直接看完整範例,可直接參考微軟的MPEG-DASH實作範例

首先需要先建立MediaSource元件:

1
var mediaSource = new MediaSource();

接著透過URL.createObjectURL,將URL指定給video.src

1
videoElement.src = URL.createObjectURL(mediaSource);

註冊sourceopen事件,對sourceBuffer做初始化:

1
2
3
4
5
6
7
8
9
mediaSource.addEventListener('sourceopen', function (e) {
try {
videoSource = mediaSource.addSourceBuffer(`${mimeType} codecs="${codecs}"`);
initVideo();
} catch (e) {
log('Exception calling addSourceBuffer for video', e);
return;
}
},false);

參照MPD檔案中,抓取Initialization裡面的range屬性,向server要取初始化的區塊,再塞入到buffer之中:

1
2
3
4
function initVideo() {
var range = Initialization.getAttribute('range');
fetchRange(range);
}

fetchRange的function中,主要是透過http的range header,向server取得所需要的區塊,再塞入到buffer之中,換句話說,你只要載入Initializationrange區塊後,即可載入你所需要的SegmentURL片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function fetchRange(range) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
var url = 'http://localhost/bunny_dashinit.mp4';

xhr.open('GET', url);
xhr.setRequestHeader("Range", "bytes=" + range);
xhr.send();
xhr.responseType = 'arraybuffer';

xhr.addEventListener("readystatechange", function () {
if (xhr.readyState == xhr.DONE) { //wait for video to load
try {
videoSource.appendBuffer(new Uint8Array(xhr.response));
resolve();
} catch (e) {
log('Exception while appending', e);
reject();
}
}
}, false);
});
}

若你不想透過range header,向server取得資料,你也可以直切分割成多個檔案,直接透過url的方式指定:

1
MP4Box -dash 4000 -frag 4000  -rap -segment-name output/segment_ bunny.mp4

透過上面指令,最後你應該會取得到多個分割檔案,及segment_init和bunny_dash.mpd檔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
├── bunny.mp4
├── bunny_dash.mpd
└── output
├── segment_1.m4s
├── segment_10.m4s
├── segment_100.m4s
├── segment_101.m4s
├── segment_102.m4s
├── segment_103.m4s
├── segment_104.m4s
├── segment_105.m4s
├── segment_106.m4s
├── segment_107.m4s
├── segment_108.m4s
├── segment_109.m4s
├── segment_11.m4s
├── segment_110.m4s
├── segment_111.m4s
├── segment_112.m4s
├── segment_113.m4s
├── segment_114.m4s
...
└── segment_init.mp4

若是針對上面檔案沒做處理,或者沒依照SegmentURLrange,直接塞入MediaSource中的sourceBuffer,你會發現MediaSource將會被abort。按照原先mp4檔案,你仍然可透過range,抓取部分區塊,最後組成一個blob檔案,透過URL.createObjectURL,設定到video.src,這時你會發現,你有抓取的區塊是可以播放。


下載相關執行檔


相關文章