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,這時你會發現,你有抓取的區塊是可以播放。


下載相關執行檔


相關文章

Capture a image block on angular

最近需要使用到一個圖片切割的工具,在angular的package中找了一下,沒有找到比較合適,因為只需要支援html5的瀏覽器,就實作了一個間單的工具(ngImageEditor)。

以下是demo實際執行的畫面:

explain

使用與安裝:

Demo

demo

Install

1
bower install ngImageEditor

Support

  • IE9+
  • chrome
  • firefox

設定editor的attributes

1
<div img-src="imgSrc" ng-image-editor="imageEditor" selected="selected"></div>

載入要切割的圖片

1
$scope.imgSrc='/images/head.jpeg';

設定切割的區塊位置及大小

1
$scope.selected = {width:150,height:150,top:0,left:0};

將區塊大小的image取出

1
$scope.imageEditor.toDataURL();

實作方式主要是透過canvas作轉換,至於cross domain,必須是同個domain,否則要從server上設定,允許哪個domain可以存取作編輯。

在server上設定你要跨網域的domain:

1
Access-Control-Allow-Origin: http://your.domain.com

在javascript中呼叫img.src之前,加入以下設定:

1
img.crossOrigin = "Anonymous";

可參考這篇

Copy canvas on html5

要複製整張canvas,可以使用getImageDataputImageData。透過getImageData這個method,將整張canvas的資料取出來,在經由putImageData將資料覆蓋上去。

Copy:

取得整張canvas的data:

1
2
var ctx = canvas.getContext("2d");
var imageData = ctx.getImageData( 0, 0, canvas.width, canvas.height );

在將imageData的資料,塞到copyCanvas對應的data區塊:(在這預設兩張canvas大小都相同)

1
2
var cctx = copyCanvas.getContext("2d");
cctx.putImageData( imageData, 0 ,0 );

這樣你就會看到第一張的canvas,完整的copy到第二張上面。

Example:

最初的兩張canvas

style

1
2
3
canvas {                     
border:1px solid gray;
}

html

1
2
<canvas id="main" width="300" height="200"></canvas>
<canvas id="copy" width="300" height="200"></canvas>

javascript

1
2
3
4
5
6
7
8
9
10
11
//left canvas
var canvas = document.querySelector( "#main" );
var ctx = canvas.getContext("2d");
ctx.fillStyle="#FF0000";
ctx.fillRect(0,0,150,75);

//right canvas
var copy = document.querySelector( "#copy" );
var cctx = copy.getContext("2d");
cctx.fillStyle="#FF00FF";
cctx.fillRect(20,20,150,75);
  • 把左邊的canvas透過drawImage畫到右邊的canvas:

    1
    cctx.drawImage( canvas, 0 ,0 );

    透過drawImage畫到右邊canvas

  • 將左邊的canvas的資料,透過putImageData取代右邊canvas資料:

    1
    2
    var imageData = ctx.getImageData( 0, 0, canvas.width, canvas.height );
    cctx.putImageData( imageData, 0 ,0 );

    透過putImageData畫到右邊canvas

如果使用drawImage,透明部份將不會畫上去,沒辦法完整複製,而putImageData,則是完全取代某區塊的的bytes。

Fixed a placeholder feature on IE (angularjs)

Placeholder要支援老舊的IE7~IE9,使用jQuery也都有一堆plugin可以使用了。如果要在angularjs上使用,但是又不想載入jQuery,這時候可以使用angularjs-placeholder module。

使用angularjs-placeholder

載入angular module:

1
<script type="text/javascript" src="../src/angularjs-placeholder.js"></script>

定義需要使用到的module,和初始化ng-app:

1
2
3
4
<script type="text/javascript">
var app = angular.module( "demoApp", ["html5.placeholder"] );
angular.bootstrap( document, [app.name] );
</script>

實作angularjs-placeholder遇到的小問題

在實作angularjs-placeholder module時,在IE7發現一個小問題,無法取得placeholder的值,但是在有的jQuery情況,都會幫忙處理掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* input default: <input placeholder="Please,enter your name!" type="text" />
*/


//use native

var attr = input.getAttribute("placeholder");

// attr == null
console.log( attr );

//use jqlite
var $elem = angular.element( input );

// attr == null
console.log( $elem.attr( "placeholder" ) );

所以如果要在IE7取得placeholder,這時候native有提供另一個api可以使用。

1
2
3
4
5
6
var node = input.getAttributeNode( "placeholder" );

var attr = node?node.nodeValue:node;

//Please,enter your name!
console.log( attr );

jquery uploader

使用ajax upload還滿常使用到,但是又不需要一些複雜的功能,所以就寫了一個簡單的jquery uploader

需求如下:
  1. 將form經用ajax post出去。
  2. 選擇要post的欄位包括file。
支援:
  1. firefox
  2. chrome
  3. IE10+

使用ajax submit整個form:

//opts.success(result):上傳成功
//opts.progress:處理進度
//opts.fail:上傳失敗

$("form").uploadForm( opts );

uploadForm會自動抓取form的action連結,目前此plugin只能一次抓取一個form upload。

使用ajax submit部份欄位:

//url:要上傳的目標連結

$( "input[name='file'],input[name='id']" ).upload( url, opts );

如果要支援IE10以下瀏覽器,現在已經另外實作iframe uploader,目前可以支援uploadForm這個功能,但是僅支援success這個參數。

github

Camera Snapshot(getUserMedia)

html5有提供一個可以取得視訊的api,使用navigator.getUserMedia,可以取得鏡頭的影像及聲音,example如下。

html:

<!DOCTYPE HTML>    
<html lang="en">   
<head>             
    <meta charset="UTF-8">
    <title></title>
</head>            
<body>             
    <video autoplay></video>
    <img src="">   
    <canvas style="display:none;"></canvas>     
    <script type="text/javascript" src="cameraSnapshot.js"></script>
</body>            
</html>         

cameraSnapshot.js:

(function(){     
    navigator.getUserMedia = navigator.getUserMedia || 
                             navigator.webkitGetUserMedia || 
                             navigator.mozGetUserMedia || 
                             navigator.msGetUserMedia;

    var video = document.querySelector('video');
    var canvas = document.querySelector('canvas');
    var ctx = canvas.getContext('2d');
    var localMediaStream = null;
    var img = document.querySelector('img');

    function snapshot() {
      if (localMediaStream) {
        canvas.width = img.width = video.clientWidth;
        canvas.height = img.height = video.clientHeight;
        ctx.drawImage(video, 0, 0); 
        img.src = canvas.toDataURL('image/webp');
      }             
    }               

    video.addEventListener('click', snapshot, false);

    if( navigator.getUserMedia ){
        navigator.getUserMedia({video: true}, function(stream) {
        video.src = window.URL.createObjectURL(stream);
        localMediaStream = stream;

        },function( error ){
            alert("User denied access to their camera!");  
        });           
    }else{         
        alert("The browser is not support");
    }               

})();           

這個範例主要是使用getUserMedia取得user的授權,將會回傳user所選的鏡頭串流,接著觸發video的click event,把目前video的畫面,繪製在canvas,在轉換成image。支援的瀏覽器

HTML5 Cache Manifest

當網路無法使用的時候,html5提供一個可以預先儲存到本機端,讓沒有網路的情況也可以使用。當需要更新時,只要去修改manifest的版本,browser就會自動做update動作。

如果是使用nginx需要在mime.types,加入以下設定:

text/cache-manifest      mf manifest;

以上動作只是針對server對browser需要吐回header,讓browser做解析,例如是php、java、node.js,這類型的語言,也可以直接在header自行加入。

html example:

<!DOCTYPE HTML>        
<html lang="en" manifest="test.manifest">       
<head>                 
    <meta charset="UTF-8">
    <title></title> 
</head>                
<body>                 
    <script type="text/javascript" src="test.js"></script>
    <script type="text/javascript">
        test.hello();
    </script>
    hello
</body>                
</html>    

test.manifest:

CACHE MANIFEST

# VERSION 0.1 

#需要cache path
CACHE:
test.js

#需要連線至網路的path
NETWORK:

#找不到path,將去替換path
FALLBACK: 

以上這種作法,會有一個問題,如果當page不是靜態的,而是要做成動態方式,page會預先被cache住,即時在manifest裡面的network設定,也還是有一樣的情況。網路上查到可以使用iframe去內嵌入一個cache.html,但是這幫助並不大,實際頁面上讀取的static檔案,還是會重新去讀取,會與iframe裡面的cache沒有相關,反而會覆蓋iframe cache裡面的檔案(路徑相同,ex:test.js)。

如果只是要cache js的話,那目前有個作法,可以解決這個問題,但不是一個好的解決方式,如下。

index html:

<!DOCTYPE HTML>        
<html lang="en">       
<head>                 
    <meta charset="UTF-8">
    <title></title> 
</head>                
<body>                 

    <script type="text/javascript">
        function onload(){
            document.querySelectorAll("iframe")[0].contentWindow.test.hello();        
        }
    </script>
    <iframe src="cache.html" style="display:none;" ></iframe>
    <script>
          document.querySelectorAll("iframe")[0].onload = onload;
    </script>
</body>                
</html>     

cache html:

<!DOCTYPE HTML>                                    
<html lang="en" manifest="test.manifest">          
<head>                                             
    <meta charset="UTF-8">                         
    <title></title>                                
</head>                                            
<body>                                             
    <script type="text/javascript" src="test.js"></script>

</body>                                  
</html>   

test.js:

test = { 
    hello:function(){
        console.log("hello");
    }   
};

test.manifest:

CACHE MANIFEST

# VERSION 0.1 

#
CACHE:
test.js

#
NETWORK:

#
FALLBACK:

以上作法,主要是利用iframe讀取到的cache script,在從主要頁面對iframe抓取相關javascript object。

canvas event

在canvas上監聽event,最簡單的方式,就是將event綁在canvas,但是當canvas畫上各種不同的shape時,這時就必須另外實作監聽各個shape的event。另一種方式,則是在canvas上,在疊加一塊image或html5svg(利用現成html的tag),用來監聽不規則的形狀。

用image的方式,則是搭配map和area tag,這種方式再大部份的瀏覽器都支援。

以下是個map tag搭配canvas的example:
<canvas id="canvas" width="200" height="200"></canvas>
<img src="" width="200" height="200" usemap="#map" style="border:0px;position:absolute;z-index:1;opacity:0;" id="mapImage" />

<map name="map" id="map">
  <area id="areaCircle" shape="circle" coords="75,75,10"  href="#" />
</map>

image上的usemap屬性,是用來參照map tag的名稱,而area則是設定監聽的範圍,和綁定event的tag,範例都以圓形為例。圓的coords(x,y,r),r為半徑

var $canvas = $("#canvas");

$("#mapImage").offset({
    top:$canvas.offset().top,
    left:$canvas.offset().left
});
$("#areaCircle").on("click",function(){
    alert("click");
});

//get a reference to the canvas
var ctx = $canvas[0].getContext("2d");

//draw a circle
ctx.beginPath();
ctx.arc(75, 75, 10, 0, Math.PI*2, true); 
ctx.closePath();
ctx.fill();
上面程式做的事情,先將image疊在canvas上,綁定好event,接著用canvas畫一個圓。[map tag可參考w3c](http://www.w3schools.com/tags/tag_area.asp)
 
##### svg搭配canvas的example:
<canvas id="canvas" width="200" height="200"></canvas>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg" style="position:absolute;z-index:1;opacity:0;">
   <circle id="circle" cx="75" cy="75" r="10" stroke="black" stroke-width="0" style="cursor:pointer;opacity:0;"/>
</svg>

svgmap tag不同的是,map tag必須指定image作為參照,而svg可以自己產生不同的shape,這裡的範例是用來搭配canvas使用,所以會設定為透明。

$("circle").on("click",function(){
alert("click");
});

var $canvas = $("#canvas");

$("#svg").offset({
top:$canvas.offset().top,
left:$canvas.offset().left
});

//get a reference to the canvas
var ctx = $canvas[0].getContext("2d");

//draw a circle
ctx.beginPath();
ctx.arc(75, 75, 10, 0, Math.PI2, true);
ctx.closePath();
ctx.fill();

*svg的應用方式還有多種,可以參考此頁面

讀取file的內容(FileReader)

讀取file的內容,可以使用FileReader,這功能是html5才加入的,依照不同瀏覽器,支援file相關物件的method,實作完整度不同。

example:
<script type="text/javascript">
  function load() {
    var finput = document.getElementById("file");
    //取得file
    var f = finput.files[0];

    if( f ){
      var r = new FileReader();
      //讀取完畢執行function
      r.onload = function(e) { console.log(e.target.result); };
        //使用utf8邊碼讀取
        //r.readAsText(f);
        //用base64格式讀取
        //r.readAsDataURL(f);
        //使用二進位方式讀取file
        r.readAsBinaryString(f);

       }
    }
</script>

<input type="file" id="file" />
<a href="#" onclick="load()">Load</a>

FileReader還有一些event可以使用,例如onloadstartonprogressonerror….等,可以用來監聽讀取的進度,另外使用dropdragover也可以利用div,製作一個框框,搭配FileReader,讓使用者方便拖拉檔案,直接載入檔案內容。

如果要支援IE8、IE9可直接使用現成的flash FileReader

w3c的定義格式

iframe跨網域(iframe cross domain)

在大部分的瀏覽器中(chrome、firefox、IE8…),可以使用postMessage和onmessage,去互相傳遞資料postMessage負責丟到不同網域,onmessage則是監聽其他網域丟來的資料。

主要domain:
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
<html>
<head>
<script>
window.onmessage = function(){
console.log(arguments);
};
/*if(window.addEventListener){
window.addEventListener("message",function(){
console.log(arguments);
},false);
}else{
window.attachEvent("onmessage", function(){
console.log(arguments);
});
}*/

</script>

</head>
<body>
<iframe src="http://127.0.0.1/otherDomain.html" id="iframe1"></iframe>
<script>
var win = document.getElementById("iframe1").contentWindow;
setTimeout(function(){
win.postMessage("Hello Sparrow!","http://127.0.0.1/");
},1000);
</script>

</body>
</html>
其他domain(otherDomain.html):
1
2
3
4
5
6
7
8
9
10
11
12
<html>
<head>
</head>
<body>
<script>
window.onmessage = function(){
console.log(arguments);
};
parent.postMessage("test","http://localhost/");
</script>

</body>
</html>
那麼如果要運行在IE7、IE6底下呢?

這時候可以建立一個proxy.html利用iframe的src嵌入一個proxy.html至於proxy.html需要抓取網址上的參數透過window.frames[“iframe1”]得方式傳遞。動態載入proxy.html,當資料傳遞完畢,將proxy.html的iframe動態移除,監聽事件可以使用onload去監聽。(換句話說,每次傳遞資料都是一次request,另外也可以使用hash”#”去實作)

圖中灰色的proxy.html可透過window.parent.frames[“iframe1”]的方式,而綠色則要使用parent.parent ,或parent.frames[“parent”]。同顏色可透過js語法直接呼叫,不同顏色必須透過proxy.html去抓取url參數,再丟到同網域的頁面。