在html5中影片播放常用方式大致上分為兩種:
- Progress Download
- Adaptive Streaming
Progress Download
這是一種最容易實踐的方式,主要是透過http Range的header,跟server要取需要的檔案區塊,然後存在暫存檔案。實作面只需要透過video.src
或者source.src
指定好來源檔案,再搭配有支援range header的server,如nginx、apache等的。
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描述檔案,主要是用來描述檔案的mimeType、codecs、segment資訊等等的,MPD實際上是一個xml檔案。
在瀏覽器的實作方式,主要是透過MediaSource
這個元件,建立一個sourceBuffer
,接著透過MPD所描述的Initialization
這個初始區塊,依照Range
去向server端要取所需要的segment,最後透過sourceBuffer.appendBuffer
將抓取的資料塞入,即可完成初始化。然後你只要依照時間推算出你所需要的SegmentURL
區塊,用一樣的方式把資料塞回sourceBuffer.appendBuffer
即可。
每一個segment的時間公式計算如下:
1 | time = (size * 8) / bitrate |
套用MPD中的SegmentURL
:
1 | var ranges = mediaRange.split('-'); |
產生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
後,應該會得到.mpd
和init.mp4
:
1 | ├── bunny_dash.mpd |
產出來的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>
其中一定會使用到的有Representation
的mimeType
、codecs
作為MediaSource.addSourceBuffer
初始化用,而bandwidth
屬性則會與SegmentURL
的mediaRange
用來計算時間,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
9mediaSource.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 | function initVideo() { |
在fetchRange
的function中,主要是透過http的range
header,向server取得所需要的區塊,再塞入到buffer
之中,換句話說,你只要載入Initialization
的range
區塊後,即可載入你所需要的SegmentURL
片段。
1 | function fetchRange(range) { |
若你不想透過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
若是針對上面檔案沒做處理,或者沒依照SegmentURL
的range
,直接塞入MediaSource
中的sourceBuffer
,你會發現MediaSource
將會被abort
。按照原先mp4檔案,你仍然可透過range
,抓取部分區塊,最後組成一個blob
檔案,透過URL.createObjectURL
,設定到video.src
,這時你會發現,你有抓取的區塊是可以播放。