高仿 優酷 播放器 dash-player

預覽

先看效果
http://yangchaojie.top/plugin/dash-playerjavascript

image.png

why do it

若是你點了上面的地址。你就會發現,視頻要加載幾秒時間才能播放,雖然我已經使用的流媒體形式(下面會講)。主要緣由仍是服務器帶寬不夠,像我這樣我的服務器只有1M的速度,一個小短片15M的話就要加載十幾秒才能開始播放。
像youku,B站 這種視頻網站,雖然帶寬高,但訪問的人多,視頻也更大,不會蠢蠢的放的靜態MP4地址上去,等加載完2G的婦聯4,婦聯5都快出來了。html

How to solve it

扯了這麼多,進入正題,怎麼解決大視頻加載問題。html5

本文檔內容使用的操做環境

系統 linux 
瀏覽器 firefox (`必定要使用火狐,先放棄一下chrome`)
編輯器 vscode

有業務經驗的人確定知道,文件體積大就分割嘛,就像分片上傳同樣,瀏覽器不能一次上傳太大文件那就分割上傳。視頻文件大就分段加載。java

看看youku 怎麼作的

來看下youku 上的video.src 上綁定的啥
image.pngnode

什麼是blob:https://...

Blob URL(參考W3C,官方名稱)或Object-URL(參考MDN和方法名稱)與BlobFile對象一塊兒使用。linux

Blob URL只能由瀏覽器在內部生成。URL.createObjectURL()將建立一個特殊的
File 對象、Blob 對象或者 MediaSource 對的引用,git

那麼普通視頻文件地址怎麼轉成blob:https://...

  • 建立一個 File對象 的引用

直接使用 input 選擇本地視頻便可github

<input id="upload" type="file" />    
<video id="preview" src=""></video>
<script>const upload = document.querySelector("#upload");
    const preview = document.querySelector("#preview");
    upload.onchange = function () {
        const file = upload.files[0]; //File對象
        const src = URL.createObjectURL(file);
        // 此時video.scr上的地址就不在是文件路徑,而是指向一塊Blob對象(存儲視頻二進制數據對象的地址)
        preview.src = src;
    };
</script>
  • 建立一個 Blob對象 的引用

    Blob(Binary Large Object) 對象表示一個不可變、原始數據的類文件對象。
    Blob對象表明了一段二進制數據。其它操做二進制數據的接口都是創建在此對象的基礎之上。ajax

File對象其實繼承自Blob對象,並提供了提供了name , lastModifiedDate, size ,type 等基礎元數據。
因此他們互相轉換很容易, File=>Blob

XMLHttpRequest第二版XHR2容許服務器返回二進制數據,建立blob對象的引用更適合加載網絡視頻chrome

function ajax(url, cb) {
    const xhr = new XMLHttpRequest();
    xhr.open("get", url);
    xhr.responseType = "blob"; 
    // "text"-字符串 "blob"-Blob對象 "arraybuffer"-ArrayBuffer對象
    xhr.onload = function () {
        cb(xhr.response);
    };
    xhr.send();
}
ajax('video.mp4', function (res) {
    const src = URL.createObjectURL(res);
    video.src = src;
})

image.png

用調試工具查看視頻標籤的src屬性已經變成一個Blob URL,表面上看起來是否是和各大視頻網站形式一致了,可是考慮一個問題,這種形式要等到請求徹底部視頻數據才能播放,依然面臨大視頻加載緩慢的問題。

答案應該就在對MediaSource 的引用上。

  • 對MediaSource的引用

    什麼是Media Source

    MediaSource包含在Media Source Extensions (MSE)標準中。
    MSE解決的問題:現有架構過於簡單,只能知足一次播放整個曲目的須要,沒法實現拆分/合併數個緩衝文件。
    MSE內容:MSE 使咱們能夠把一般的單個媒體文件的 src 值替換成引用 MediaSource 對象(一個包含即將播放的媒體文件的準備狀態等信息的容器),以及引用多個 SourceBuffer 對象(表明多個組成整個串流的不一樣媒體塊)的元素。MSE 讓咱們可以根據內容獲取的大小和頻率,或是內存佔用詳情(例如何時緩存被回收),進行更加精準地控制。 它是基於它可擴展的 API 創建自適應比特率流客戶端(例如DASH 或 HLS 的客戶端)的基礎。

簡單的說就是先讓video 加載 MediaSource 對象,但MediaSource 對象沒有視頻具體內容,內容被分割在SourceBuffer 對象中,可能有音頻SourceBuffer,視頻SourceBuffer,字幕SourceBuffer,
image.png

看到buffer就有譜了,能夠向緩存區一點點寫入video數據
SourceBuffer.appendBuffer(source)

source

一個 BufferSource 對象(ArrayBufferView 或 ArrayBuffer),存儲了你要添加到 SourceBuffer 中去的媒體片斷數據。

值得注意的是這裏使用的是追加ArrayBuffer
xhr.responseType 應該等於 "arraybuffer"
ArrayBuffer對象也表明儲存二進制數據的一段內存,
Blob與ArrayBuffer的區別是,除了原始字節之外它還提供了mime type做爲元數據,Blob和ArrayBuffer之間能夠進行轉換。

閱讀文檔,能夠很快寫出一個加載示例

我這裏已經對視頻作好了分割,後面會講如何分割,下面chunk-stream0 表明240分辨率,若是感受加載快能夠換成chunk-stream1 表明480分辨率,甚至chunk-stream2 表明1280分辨率,相應init-stream0 也要變化
<video width="400" controls autoplay="autoplay"></video>

<script>
    const video = document.querySelector('video');
    //視頻資源存放路徑,假設下面有5個分段視頻 video1.mp4 ~ video5.mp4,第一個段爲初始化視頻init.mp4
    const assetURL = "http://yangchaojie.top/allow_origin/mpd/";
    //視頻格式和編碼信息,主要爲判斷瀏覽器是否支持視頻格式,但若是信息和視頻不符可能會報錯
    const mimeCodec = 'video/mp4';
    if ('MediaSource' in window) {
        const mediaSource = new MediaSource();
        video.pause();
        video.src = URL.createObjectURL(mediaSource); //將video與MediaSource綁定,此處生成一個Blob URL
        mediaSource.addEventListener('sourceopen', sourceOpen); //能夠理解爲容器打開
    } else {
        //瀏覽器不支持該視頻格式
        console.error('Unsupported MIME type or codec: ', mimeCodec);
    }

    function sourceOpen() {
        const mediaSource = this;
        const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
        let i = 1;
        function getNextVideo(url) {
            //ajax代碼實現翻看上文,數據請求類型爲arraybuffer
            ajax(url, function (buf) {
                //往容器中添加請求到的數據,不會影響當下的視頻播放。

                sourceBuffer.appendBuffer(new Uint8Array(buf));
            });
        }
        //每次appendBuffer數據更新完以後就會觸發
        sourceBuffer.addEventListener("updateend", function () {
            if (i === 1) {
                //第一個初始化視頻加載完就開始播放
                video.play();
            }
            if (i < 12) {
                //一段視頻加載完成後,請求下一段視頻
                getNextVideo(`${assetURL}/chunk-stream0-000${String(i).padStart(2, 0)}.m4s`);
            }
            if (i === 12) {
                //所有視頻片斷加載完關閉容器
                mediaSource.endOfStream();
                URL.revokeObjectURL(video.src); //Blob URL已經使用並加載,不須要再次使用的話能夠釋放掉。
            }
            i++;
        });
        //加載初始視頻
        getNextVideo(`${assetURL}/init-stream0.m4s`);
    };

    function ajax(url, cb) {
        const xhr = new XMLHttpRequest();
        xhr.open("get", url);
        xhr.responseType = "arraybuffer"; // "text"-字符串 "blob"-Blob對象 "arraybuffer"-ArrayBuffer對象
        xhr.onload = function () {
            cb(xhr.response);
        };
        xhr.send();
    }
</script>

查看控制檯 已經不斷的請求分割的片斷
image.png

但你確定發現沒有音頻信息,對的,你音響壞了,
這裏咱們只加載了視頻信息,並無加載音頻,因此咱們還要加載音頻

<video width="400" controls autoplay="autoplay"></video>
    <script>
        const video = document.querySelector('video');
        //視頻資源存放路徑,假設下面有5個分段視頻 video1.mp4 ~ video5.mp4,第一個段爲初始化視頻init.mp4
        const assetURL = "http://yangchaojie.top/allow_origin/mpd/";
        //視頻格式和編碼信息,主要爲判斷瀏覽器是否支持視頻格式,但若是信息和視頻不符可能會報錯
        const mimeCodec = 'video/mp4';
        if ('MediaSource' in window) {
            const mediaSource = new MediaSource();
            video.pause();
            video.src = URL.createObjectURL(mediaSource); //將video與MediaSource綁定,此處生成一個Blob URL
            mediaSource.addEventListener('sourceopen', sourceOpen); //能夠理解爲容器打開
        } else {
            //瀏覽器不支持該視頻格式
            console.error('Unsupported MIME type or codec: ', mimeCodec);
        }
        function appendVideo(mediaSource) {
            const sourceBuffer = mediaSource.addSourceBuffer('video/mp4');
            let i = 1;
            function getNextVideo(url) {
                //ajax代碼實現翻看上文,數據請求類型爲arraybuffer
                ajax(url, function (buf) {
                    //往容器中添加請求到的數據,不會影響當下的視頻播放。

                    sourceBuffer.appendBuffer(new Uint8Array(buf));
                });
            }
            //每次appendBuffer數據更新完以後就會觸發
            sourceBuffer.addEventListener("updateend", function () {
                if (i === 1) {
                    //第一個初始化視頻加載完就開始播放
                    video.play();
                }
                if (i < 12) {
                    //一段視頻加載完成後,請求下一段視頻
                    getNextVideo(`${assetURL}/chunk-stream0-000${String(i).padStart(2, 0)}.m4s`);
                }
                if (i === 12) {
                    //所有視頻片斷加載完關閉容器
                    mediaSource.endOfStream();
                    URL.revokeObjectURL(video.src); //Blob URL已經使用並加載,不須要再次使用的話能夠釋放掉。
                }
                i++;
            });
            //加載初始視頻
            getNextVideo(`${assetURL}/init-stream0.m4s`);
        }
        function appendAudio(mediaSource) {
            const sourceBuffer = mediaSource.addSourceBuffer('audio/mp4');
            let i = 1;
            function getNextVideo(url) {
                //ajax代碼實現翻看上文,數據請求類型爲arraybuffer
                ajax(url, function (buf) {
                    //往容器中添加請求到的數據,不會影響當下的視頻播放。

                    sourceBuffer.appendBuffer(new Uint8Array(buf));
                });
            }
            //每次appendBuffer數據更新完以後就會觸發
            sourceBuffer.addEventListener("updateend", function () {
                if (i === 1) {
                    //第一個初始化視頻加載完就開始播放
                    video.play();
                }
                if (i < 12) {
                    //一段視頻加載完成後,請求下一段視頻
                    getNextVideo(`${assetURL}/chunk-stream3-000${String(i).padStart(2, 0)}.m4s`);
                }
                if (i === 12) {
                    //所有視頻片斷加載完關閉容器
                    mediaSource.endOfStream();
                    URL.revokeObjectURL(video.src); //Blob URL已經使用並加載,不須要再次使用的話能夠釋放掉。
                }
                i++;
            });
            //加載初始視頻
            getNextVideo(`${assetURL}/init-stream3.m4s`);
        }
        function sourceOpen() {
            const mediaSource = this;
            appendVideo(mediaSource)
            appendAudio(mediaSource)
        };

        function ajax(url, cb) {
            const xhr = new XMLHttpRequest();
            xhr.open("get", url);
            xhr.responseType = "arraybuffer"; // "text"-字符串 "blob"-Blob對象 "arraybuffer"-ArrayBuffer對象
            xhr.onload = function () {
                cb(xhr.response);
            };
            xhr.send();
        }
    </script>

稍微改動下代碼,ok 如今一個基本流媒體播放 搞定
image.png

可是,還有一個問題,很嚴重的問題,就是沒有辦法拖動進度。
由於它不知道整個視頻是什麼樣的,有多長,是否有聲音軌,有幾條等等。只知道加載過的片短視頻是什麼樣的。
因此須要一個描述文件 mpd。

MPD

MPD是一個XML文件,描述了媒體的分段方式,類型和編解碼器(此處爲MP4),視頻的比特率,長度和基本分段大小。MPD文件還能包含音頻信息,您能夠將內容拆分爲視頻和音頻播放器的單獨流(就像上面我拆成了多個流)。

DASH

那麼MPD 文件和上面的分段文件 是怎麼生成的?須要先了解DASH
DASH(Dynamic Adaptive Streaming over HTTP )是一個規範了自適應內容應當如何被獲取的協議。它其實是創建在 MSE 頂部的一個層,用來構建自適應比特率串流客戶端。雖然已經有一個相似的協議了(例如 HTTP 串流直播(HLS)),但 DASH 有最好的跨平臺兼容性。

是一種服務端、客戶端的流媒體解決方案:
服務端:
將視頻內容分割爲一個個分片,每一個分片能夠存在不一樣的編碼形式(不一樣的codec、profile、分辨率、碼率等);
播放器端:
就能夠根據自由選擇須要播放的媒體分片;能夠實現adaptive bitrate streaming技術。不一樣畫質內容無縫切換,提供更好的播放體驗。

我的理解:MSE是標準,描述了媒體文件能夠流式接收,DASH是協議規範了媒體文件如何分軌,如何分段,如何接收。

概念太多,給你們看點實際的

ffmpeg -i  你的文件.mp4 -c copy -use_template 0 -single_file 0  -f dash index.mpd

執行上面命令能夠獲得 所需的 mpd 文件 和 分段後媒體文件
ffmpeg能夠 面向搜索引擎安裝。

有了描述文件MPD ,怎麼解決拖動視頻

很簡單了,mpd已經描述了媒體長度提早告訴mediaSource就好了

MediaSource 接口的屬性 duration 用來獲取或者設置當前媒體展現的時長.
image.png

我使用的視頻長度是1M8.2s,也就是1分鐘+8.2秒=68.2秒

mediaSource.duration = 68.2;

image.png

如今再來看看視頻就有長度,能夠拖動進度了。
image.png

有點小激動

到這裏還只是僅僅正常播放,還有許多問題沒有解決,好比如今拖動進度雖然能夠播放,但仍是要加載完以前內容片斷才行,須要改進成只加載當前,還有切換多分辨率沒有實現,最頭痛的是兼容性,若是你堅持使用chrome或其餘瀏覽器沒有使用火狐,應該是大部分代碼沒有辦法運行。問題還不少,這時候就要找輪子了。(也沒時間搞,由於公司倒閉了,要花點時間找工做,過段時間再深刻研究)

投入 dash.js 懷抱

Dash.js是用JavaScript編寫的開源MPEG-DASH視頻播放器。其目標是提供一個健壯的跨平臺播放器,能夠在須要視頻播放的應用程序中自由重用。
高仿優酷播放器就是應用了dash.js, 可是dash.js也存在一些問題(也不算問題,就是沒有直接提供API),好比不能當即切換分辨率,必須等當前已加載片斷播放完後才能切換,因此在dash.js 基礎上稍加包裹,不敢說封裝,人家已經至關完美。提供一些更易上手的播放器API。dash-player

資源下載

dash-demo

參考文檔

相關文章
相關標籤/搜索