H5直播之MSE(Media Source Extensions)

參考
w3c media-source
Media Source 系列 - 使用 Media Source Extensions 播放視頻
全面進階 H5 直播
無 Flash 時代,讓直播擁抱 H5(MSE篇)
使用 MediaSource 搭建流式播放器

一、MSE 意義

1.粗識 HTML5 video 標籤和MSE媒體源擴展
當前網頁上能夠搜到的HTML5和MSE相關的內容一抓一大把,本文的目的是儘量用較短的篇幅,簡述瀏覽器爲何要使用HTML5的MSE擴展。這也是在我最開始接觸有關內容時的最大的疑惑。

以往用戶在瀏覽網頁內容尤其是視頻內容時,需要使用像Adobe Flash或是微軟的Silverlight這樣的插件,播放視音頻內容即使是電腦小白也知道,需要媒體播放器的支持,前面提到的插件就是起到媒體播放器的作用。但是使用插件這樣的方式是很不便捷且很不安全的,一些不法分子會在這些插件上動手腳。因此W3C的最新的HTML5標準中,定義了一系列新的元素來避免使用插件,其中就包含了<video>標籤這一大名鼎鼎的元素。

正是使用了<video>標籤,支持HTML5的瀏覽器得以實現無插件就原生支持播放媒體內容,但是對媒體內容的格式有所限制。說到媒體內容,就自然地需要談到媒體的封裝格式和編碼格式,這裏總結一下,原視頻文件通過編碼來壓縮文件大小,再通過封裝將壓縮視音頻、字幕組合到一個容器內,具體內容請大家自行查閱。

我們可以把<video>標籤看做擁有解封裝和解碼功能的瀏覽器自帶播放器。隨着視頻點播、直播等視頻業務的發展,視頻通過流媒體傳輸協議(目前常用的有兩種,MPEG-DASH和Apple的HLS)從服務器端分發給客戶端,媒體內容進一步包含在一層傳輸協議中,這樣<video>就無法識別了。以HLS爲例,將源文件內容分散地封裝到了一個個TS文件中。

僅靠<video>標籤無法識別這樣的TS文件,那麼就引入了MSE拓展來幫助瀏覽器識別並處理TS文件,將其變回原來可識別的媒體容器格式,這樣<video>就可以識別並播放原來的文件了。那麼支持HTML5的瀏覽器就相當於內置了一個能夠解析流協議的播放器。

比如在hls.js 源碼解讀【1】中,介紹的hls.js

hls實際會先通過 ajax(loader 是可以完成自定義的) 請求 m3u8文件,然後會讀取到文件的分片列表,以及視頻的編碼格式,時長等。隨後會按照順序(非 seek )去對分片進行請求,這些也是通過 ajax 請求二進制的文件,然後藉助 Media Source Extensions 將 buffer 內容進行合流,然後組成一個可播的媒體資源文件。

2.爲什麼國內大部分視頻廠商不對PC開放HTML5?
視頻源存在兼容性問題。原生的 HTML5 <video> 元素在 Windows PC 上僅支持 mp4 (H.264 編碼)、webm、ogg 等格式視頻的播放。而由於歷史遺留問題(HTML5 視頻標準最終被廣泛支持以前,Flash 在 Web 視頻播放方面有着統治地位),視頻網站的視頻源和轉碼設置,很多都高清源都是適用於 Flash 播放的 FLV 格式,只有少量低清晰度視頻是 mp4 格式,webm 和 ogg 更是聽都沒聽說過。比如優酷只有高清和標清纔有 MP4 源,超清、1080P 等,基本都是 FLV 和 HLS(M3U8)的視頻源(在 Windows PC 上支持 M3U8 比支持 FLV 更復雜,我們不做過多贅述)。而騰訊視頻,因爲轉型 MP4 比較早,視頻源幾乎全部都是 MP4 和 HLS,所以現在可以在 Mac OS X 上率先支持 PC Web 端的 HTML5 播放器(Safari 下 HLS、Chrome 下 MP4)。

但是 HTML5 是不是就真的沒辦法播放 FLV 等格式視頻了呢?不是。解決方案是 MSE,Media Source Extensions,就是說,HTML5 <video> 不僅可以直接播放上面支持的 mp4、m3u8、webm、ogg 格式,還可以支持由 JS 處理過後的視頻流,這樣我們就可以用 JS 把一些不支持的視頻流格式,轉化爲支持的格式(如 H.264 的 mp4)。B 站開源的 flv.js 就是這個技術的一個典型實現。B 站的 PC HTML5 播放器,就是用 MSE 技術,將 FLV 源用 JS 實時轉碼成 HTML5 支持的視頻流編碼格式(其實就一個文件頭的差異(這裏文件頭改成容器。感謝評論區謙謙的指教,是容器的差異,容器不只是文件頭)),提供給 HTML5 播放器播放。

一些人問我爲什麼不直接採用 MP4 格式,並表示對 FLV 格式的厭惡。這個問題一方面是歷史遺留問題,由於視頻網站前期完全依賴 Flash 播放而選擇 FLV 格式;另一方面,如果仔細研究過 FLV/MP4 封裝格式,你會發現 FLV 格式非常簡潔,而 MP4 內部 box 種類繁雜,結構複雜固實而又有太多冗餘數據。FLV 天生具備流式特徵適合網絡流傳輸,而 MP4 這種使用最廣泛的存儲格式,設計卻並不一定優雅。

3.Media Source Extensions
我們已經可以在 Web 應用程序上無插件地播放視頻和音頻了。但是,現有架構過於簡單,只能滿足一次播放整個曲目的需要,無法實現拆分/合併數個緩衝文件。流媒體直到現在還在使用 Flash 進行服務,以及通過 RTMP 協議進行視頻串流的 Flash 媒體服務器。

MSE 使我們可以把通常的單個媒體文件的 src 值替換成引用 MediaSource 對象(一個包含即將播放的媒體文件的準備狀態等信息的容器),以及引用多個 SourceBuffer 對象(代表多個組成整個串流的不同媒體塊)的元素。MSE 讓我們能夠根據內容獲取的大小和頻率,或是內存佔用詳情(例如什麼時候緩存被回收),進行更加精準地控制。 它是基於它可擴展的 API 建立自適應比特率流客戶端(例如DASH 或 HLS 的客戶端)的基礎。

Download ---》 Response.arrayBuffer(適用fetch/xhr等異步獲取流媒體數據) ---》 SourceBuffer(添加到MediaSource的buffer中) ---》 <vedio/> or <autio/>

二、運行DEMO

參考MDN在線DEMO bufferAll,將HTML代碼及所用的文件frag_bunny.mp4下載到本地,即可運行

<html><head>
    <meta charset="utf-8">
  </head>
  <body>
    <video controls=""></video>
    <script>
      var video = document.querySelector('video');

      var assetURL = 'frag_bunny.mp4';
      // Need to be specific for Blink regarding codecs
      // ./mp4info frag_bunny.mp4 | grep Codec
      var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';

      if ('MediaSource' in window && 
      MediaSource.isTypeSupported(mimeCodec)) {
        var mediaSource = new MediaSource;
        //console.log(mediaSource.readyState); // closed
        video.src = URL.createObjectURL(mediaSource);
        mediaSource.addEventListener('sourceopen', sourceOpen);
      } else {
        console.error('Unsupported MIME type or codec: ', mimeCodec);
      }

      function sourceOpen (e) {
        //console.log(this.readyState); // open
        var mediaSource = e.target;
        var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
        fetchAB(assetURL, function (buf) {
          sourceBuffer.addEventListener('updateend', function (_) {
            mediaSource.endOfStream();
            video.play();
            //console.log(mediaSource.readyState); // ended
          });
          console.log("buf",buf);
          sourceBuffer.appendBuffer(buf);
        });
      };

      function fetchAB (url, cb) {
        console.log(url);
        var xhr = new XMLHttpRequest;
        xhr.open('get', url);
        xhr.responseType = 'arraybuffer';
        xhr.onload = function () {
          cb(xhr.response);
        };
        xhr.send();
      };
    </script>
</body></html>

1.參考MSE(Media Source Extensions)的一點嘗試
遇到的坑是:一開始用的是自己本地隨便找的一個視頻文件,結果報錯:Uncaught DOMException: Failed to execute ‘endOfStream’ on ‘MediaSource’: The MediaSource’s readyState is not ‘open’.原因是該MP4文件不是 framented mp4,不支持這種MSE的播放形式。這裏也提供一個轉換的工具,支持將普通MP4轉爲 framented mp4:Bento4 MP4 & DASH Class Library, SDK and Tools

2.參考mp4文件格式之fragment mp4
對於普通 MP4 文件,整個mp4文件的的meta數據都在文件頭,所有媒體數據爲整體一塊。當文件比較大的時候,meta數據就比較大。這樣對mp4文件的本地播放是沒有問題。但對於一些視頻播放網站而言,用戶的播放器必須下載全meta數據才能開始播放,這就意味着用戶的緩衝時間將因爲mp4文件的存儲結構而延長。目前一種解決方法是將大的mp4文件切成物理分離的多段,使得每段的meta都比較小,從而在一定程度上減少緩衝時間。

對於fragment mp4,mp4文件被分成多個frag分片,而原來的meta數據大大變小,且沒個frag都可以單獨索引、傳輸和播放,這樣就可以解決mp4不能流式傳輸播放的問題。對用戶體驗比較好。然而目前這種格式並不被多數解碼器完整支持,部分播放器加載文件時間過長,而且瀏覽器內嵌播放器也可能不支持播放。

3.參考WebSocket+MSE——HTML5 直播技術解析

fragment mp4 文件

non-fragment mp4 文件
我們可以看到 non-fragment mp4 的最頂層 box 類型非常少,而 fragment mp4 是由一段一段的 moof+mdat 組成的,它們已經包含了足夠的 metadata 信息與數據, 可以直接 seek 到這個位置開始播放。也就是說 fMP4 是一個流式的封裝格式,這樣更適合在網絡中進行流式傳輸,而不需要依賴文件頭的metadata。

Apple在WWDC 2016 大會上宣佈會在 iOS 10、tvOS、macO S的 HLS 中支持 fMP4,可見fMP4 的前景非常的好。

值得一提的是,fMP4、CMAF、ISOBMFF 其實都是類似的東西。

把一個 non-fragment MP4 轉換成 fragment MP4。可以使用 FFmpeg 的 -movflags 來轉換。

對於原始文件爲非 MP4 文件:ffmpeg -i trailer_1080p.mov -c:v copy -c:a copy -movflags frag_keyframe+empty_moov bunny_fragmented.mp4

對於原始文件已經是 MP4 文件:ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov fragmented.mp4

或者使用 mp4fragment:mp4fragment input.mp4 output.mp4

三、URL.createObjectURL

H5直播系列一 Blob File FileReader URL曾經介紹過URL.createObjectURL方法。

//blob參數是一個File對象或者Blob對象.
var objecturl =  window.URL.createObjectURL(blob);

上面的代碼會對二進制數據生成一個 URL,這個 URL 可以放置於任何通常可以放置 URL 的地方,比如 img 標籤的 src 屬性。需要注意的是,即使是同樣的二進制數據,每調用一次 URL.createObjectURL 方法,就會得到一個不一樣的 URL。這個 URL 的存在時間,等同於網頁的存在時間,一旦網頁刷新或卸載,這個 URL 就失效。(File 和 Blob 又何嘗不是這樣呢)除此之外,也可以手動調用 URL.revokeObjectURL 方法,使 URL 失效。

window.URL.revokeObjectURL(objectURL);

舉個簡單的例子。

var blob = new Blob(["Hello hanzichi"]);
var a = document.createElement("a");
a.href = window.URL.createObjectURL(blob);
a.download = "a.txt";
a.textContent = "Download";

document.body.appendChild(a);

頁面上生成了一個超鏈接,點擊它就能下載一個名爲 a.txt 的文件,裏面的內容是 Hello hanzichi。

四、使用createObjectURL將MediaSource和video標籤連接起來

        var mediaSource = new MediaSource;
        //console.log(mediaSource.readyState); // closed
        video.src = URL.createObjectURL(mediaSource);
        mediaSource.addEventListener('sourceopen', sourceOpen);

這裏傳入createObjectURL的不是File或Blob了,而是MediaSource。MS 的實例通過 URL.createObjectURL() 創建的 url 並不會同步連接到 video.src。換句話說,URL.createObjectURL() 只是底層流(MS)和 video.src 的連接中間者,一旦兩者連接到一起之後,該對象就沒用了。那麼什麼時候 MS 纔會和 video.src 連接到一起呢?創建實例都是同步的,但是底層流和 video.src 的連接是異步的。MS 提供了一個 sourceopen 事件給我們進行這項異步處理。一旦連接到一起之後,該 URL object 就沒用了,處於內存節省的目的,可以使用 URL.revokeObjectURL(vidElement.src) 銷燬指定的 URL object。

mediaSource.addEventListener('sourceopen', sourceOpen);
function sourceOpen(){
    URL.revokeObjectURL(vidElement.src)
}

MSE 支持具體的事件

  • sourceopen 綁定到媒體元素後開始觸發
  • sourceclosed 未綁定到媒體元素後開始觸發
  • sourceended 所有數據接收完成後觸發

對應的屬性mediaSource.readyState

  • open MSE 實例,已經綁定到了媒體元素上,等待接受數據或者正在接受數據
  • closed MSE 實例未綁定到了媒體元素上。MS剛創建時就是該狀態。
  • ended MSE 實例,已經綁定到了媒體元素上, 並且所有數據都已經接受到了。當endOfStream()執行完成,會變爲該狀態。

五、設置編碼類型mime 字符串

function sourceOpen(e) {  
  URL.revokeObjectURL(videoMp4.src);
  var mime = 'video/webm; codecs="opus, vp9"';
  // e.target refers to the mediaSource instance.
  // Store it in a variable so it can be used in a closure.
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  // Fetch and process the video.
}

 

var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'

首先,前面的 video/mp4 代表這是一段 mp4 格式封裝的視頻,同理也存在類似 video/webmaudio/mpegaudio/mp4 這樣的 mime 格式。一般情況下,可以通過 canPlayType 這個方法來判斷瀏覽器是否支持當前格式。

後面的這一段 codecs="...." 比較特別,以逗號相隔,分爲兩段:

第一段,'avc1.42E01E',即它用於告訴瀏覽器關於視頻編解碼的一些重要信息,諸如編碼方式、分辨率、幀率、碼率以及對解碼器解碼能力的要求。

在這個例子中,**'avc1' **代表視頻採用 H.264 編碼,隨後是一個分隔點,之後是 3 個兩位的十六進制的數,這 3 個十六進制數分別代表:

  1. AVCProfileIndication(42
  2. profile_compability(E0
  3. AVCLevelIndication(1E

第一個用於標識 H.264 的 profile,後兩個用於標識視頻對於解碼器的要求。

對於一個 mp4 視頻,可以使用 mp4file 這樣的命令行工具:

mp4file --dump xxx.mp4

找到 avcC Box 後,就可以看到這三個值:

mp4file --dump movie.mp4
...
    type avcC (moov.trak.mdia.minf.stbl.stsd.avc1.avcC) // avc1
     configurationVersion = 1 (0x01)
     AVCProfileIndication = 66 (0x42)    // 42
     profile_compatibility = 224 (0xe0)  // E0
     AVCLevelIndication = 30 (0x1e)      // 1E
...

有一處要注意,後面兩個值(profile_compability、AVCLevelIndication)只是瀏覽器用於判斷自身的解碼能力能否滿足需求,所以不需要和視頻完全對應,更高也是可以的。

下面來看 codecs 的第二段 'mp4a.40.2',這一段信息是關於音頻部分的,代表視頻的音頻部分採用了 AAC LC 標準:'mp4a' 代表此視頻的音頻部分採用 MPEG-4 壓縮編碼。隨後是一個分隔點,和一個十六進制數(40),這是 ObjectTypeIndication,40 對應的是 Audio ISO/IEC 14496-3 標準。(不同的值具有不同的含義,詳細可以參考官方文檔

然後又是一個分隔點,和一個十進制數(2),這是 MPEG-4 Audio Object Type,維基百科中的解釋是 "MPEG-4 AAC LC Audio Object Type is based on the MPEG-2 Part 7 Low Complexity profile (LC) combined with Perceptual Noise Substitution (PNS) (defined in MPEG-4 Part 3 Subpart 4)",具體是什麼意思就不翻譯了,其實就是一種 H.264 視頻中常用的音頻編碼規範。

這一整段 codecs 都有完善的官方文檔,可以參考:The 'Codecs' and 'Profiles' Parameters for "Bucket" Media Types

六、請求資源

sourceBuffer對象提供了一系列接口,這裏用到的是 appendBuffer 方法,可以動態地向 MediaSource 中添加視頻/音頻片段(對於一個 MediaSource,可以同時存在多個 SourceBuffer)

如果視頻很長,存在多個chunk 的話,就需要不停地向 SourceBuffer 中加入新的 chunk。這裏就需要注意一個問題了,即 appendBuffer 是異步執行的,在完成前,不能 append 新的 chunk:

sourceBuffer.appendBuffer(buffer1)
sourceBuffer.appendBuffer(buffer2)
// Uncaught DOMException: 
Failed to set the 'timestampOffset' property on 'SourceBuffer': 
This SourceBuffer is still processing 
an 'appendBuffer' or 'remove' operation.

而是應該監聽 SourceBuffer 上的 updateend 事件,確定空閒後,再加入新的 chunk:

sourceBuffer.addEventListener('updateend', () => {
    // 這個時候才能加入新 chunk
    // 先設定新chunk加入的位置,比如第20秒處
    sourceBuffer.timestampOffset = 20
    // 然後加入
    sourceBuffer.append(newBuffer)
}

七、SourceBuffer簡介

SourceBuffer 是由 mediaSource 創建,並直接和 HTMLMediaElement 接觸。簡單來說,它就是一個流的容器,裏面提供的 append(),remove() 來進行流的操作,它可以包含一個或者多個 media segments。

interface SourceBuffer : EventTarget {
             attribute AppendMode          mode;
    readonly attribute boolean             updating;
    readonly attribute TimeRanges          buffered;
             attribute double              timestampOffset;
    readonly attribute AudioTrackList      audioTracks;
    readonly attribute VideoTrackList      videoTracks;
    readonly attribute TextTrackList       textTracks;
             attribute double              appendWindowStart;
             attribute unrestricted double appendWindowEnd;
             attribute EventHandler        onupdatestart;
             attribute EventHandler        onupdate;
             attribute EventHandler        onupdateend;
             attribute EventHandler        onerror;
             attribute EventHandler        onabort;
    void appendBuffer(BufferSource data);
    void abort();
    void remove(double start, unrestricted double end);
};

1.mode
上面說過,SB(SourceBuffer) 裏面存儲的是 media segments(就是你每次通過 append 添加進去的流片段)。SB.mode 有兩種格式:

  • segments: 亂序排放。通過 timestamps 來標識其具體播放的順序。比如:20s的 buffer,30s 的 buffer 等。
  • sequence: 按序排放。通過 appendBuffer 的順序來決定每個 mode 添加的順序。timestamps 根據 sequence 自動產生。

那麼上面兩個哪個是默認值呢?看情況,講真,沒騙你。當 media segments 天生自帶 timestamps,那麼 mode 就爲 segments ,否則爲 sequence。所以,一般情況下,我們是不用管它的值。不過,你可以在後面,將 segments 設置爲 sequence 這個是沒毛病的。反之,將 sequence 設置爲 segments 就有問題了。

var bufferMode = sourceBuffer.mode;
if (bufferMode == 'segments') {
  sourceBuffer.mode = 'sequence';
}

segments 表示 A/V 的播放時根據你視頻播放流中的 pts 來決定,該模式也是最常使用的。因爲音視頻播放中,最重要的就是 pts 的排序。因爲,pts 可以決定播放的時長和順序,如果一旦 A/V 的 pts 錯開,有可能就會造成 A/V sync drift。

sequence 則是根據空間上來進行播放的。每次通過 appendBuffer 來添加指定的 Buffer 的時候,實際上就是添加一段 A/V segment。此時,播放器會根據其添加的位置,來決定播放順序。還需要注意,在播放的同時,你需要告訴 SB,這段 segment 有多長,也就是該段 Buffer 的實際偏移量。而該段偏移量就是由 timestampOffset 決定的。整個過程用代碼描述一下就是:

sb.appendBuffer(media.segment);
sb.timestampOffset += media.duration;

另外,如果你想手動更改 mode 也是可以的,不過需要注意幾個先決條件:

  • 對應的 SB.updating 必須爲 false.
  • 如果該 parent MS 處於 ended 狀態,則會手動將 MS readyState 變爲 open 的狀態。

2.buffered
返回一個 timeRange 對象。用來表示當前被存儲在 SB 中的 buffer。

  1. updating

返回 Boolean,表示當前 SB 是否正在被更新。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 調用時。

  • true:當前 SB 正在處理添加或者移除的 segment
  • false:當前 SB 處於空閒狀態。當且僅當 updating = false 的時候,纔可以對 SB 進行額外的操作。

SB 內部的 buffer 管理主要是通過 appendBuffer(BufferSource data) 和 remote() 兩個方法來實現的。當然,並不是所有的 Buffer 都能隨便添加給指定的 SB,這裏面是需要條件和相關順序的。

  • 該 buffer,必須滿足 MIME 限定的類型
  • 該 buffer,必須包含 initialization segments(IS) 和 media segments(MS)

下圖是相關的支持 MIME

image.png
這裏需要提醒大家一點,MSE 只支持 fmp4 的格式。具體內容可以參考: 學好 MP4,讓直播更給力。上面提到的 IS 和 MS 實際上就是 FMP4 中不同盒子的集合而已。

4.事件
在 SB 中,相關事件觸發包括:

  • updatestart: 當 updating 由 false 變爲 true。
  • update:當 append()/remove() 方法被成功調用完成時,updating 由 true 變爲 false。
  • updateend: append()/remove() 已經結束
  • error: 在 append() 過程中發生錯誤,updating 由 true 變爲 false。
  • abort: 當 append()/remove() 過程中,使用 abort() 方法廢棄時,會觸發。此時,updating 由 true 變爲 false。

注意上面有兩個事件比較類似:update 和 updateend。都是表示處理的結束,不同的是,update 比 updateend 先觸發。

sourceBuffer.addEventListener('updateend', function (e) {
    // 當指定的 buffer 加載完後,就可以開始播放
      mediaSource.endOfStream();
      video.play();
    });

5.添加/移除 buffer
在添加 Buffer 的時候,你需要了解你所採用的 mode 是哪種類型,sequence 或者 segments。這兩種是完全兩種不同的添加方式。

(1)segments
這種方式是直接根據 MP4 文件中的 pts 來決定播放的位置和順序,它的添加方式極其簡單,只需要判斷 updating === false,然後,直接通過 appendBuffer 添加即可。

if (!sb.updating) {
    let MS = this._mergeBuffer(media.tmpBuffer);
           
    sb.appendBuffer(MS); // ****

    media.duration += lib.duration; 
    media.tmpBuffer = [];
}

(2)sequence
如果你是採用這種方式進行添加 Buffer 進行播放的話,那麼你也就沒必要了解 FMP4 格式,而是瞭解 MP4 格式。因爲,該模式下,SB 是根據具體添加的位置來進行播放的。所以,如果你是 FMP4 的話,有可能就有點不適合了。針對 sequence 來說,每段 buffer 都必須有自己本身的指定時長,每段 buffer 不需要參考的 baseDts,即,他們直接可以毫無關聯。那 sequence 具體怎麼操作呢?

簡單來說,在每一次添加過後,都需要根據指定 SB 上的 timestampOffset。該屬性,是用來控制具體 Buffer 的播放時長和位置的。

if (!sb.updating) {
    let MS = this._mergeBuffer(media.tmpBuffer);
           
    sb.appendBuffer(MS); // ****

    sb.timestampOffset += lib.duration; // ****
    media.tmpBuffer = [];
}

上面兩端打 * 號的就是重點內容。該方式比較容易用來直接控制 buffer 片段的添加,而不用過度關注相對 baseDTS 的值。

6.控制播放片段
如果要在 video 標籤中控制指定片段的播放,一般是不可能的。因爲,在加載整個視頻 buffer 的時候,視頻長度就已經固定的,剩下的只是你如果在 video 標籤中控制播放速度和音量大小。而在 MSE 中,如何在已獲得整個視頻流 Buffer 的前提下,完成底層視頻 Buffer 的切割和指定時間段播放呢?

這裏,需要利用 SB 下的 appendWindowStart 和 appendWindowEnd 這兩個屬性。

他們兩個屬性主要是爲了設置,當有視頻 Buffer 添加時,只有符合在 [start,end] 之間的 media frame 才能 append,否則,無法 append。例如:

sourceBuffer.appendWindowStart = 2.0;
sourceBuffer.appendWindowEnd = 5.0;

設置添加 Buffer 的時間戳爲 [2s,5s] 之間。appendWindowStart 和 appendWindowEnd 的基準單位爲 s。該屬性值,通常在添加 Buffer 之前設置。

6.SB 內存釋放
SB 內存釋放其實就和在 JS 中,將一個變量指向 null 一樣的過程。

var a = new ArrayBuffer(1024 * 1000);
a = null; // start garbage collection

在 SB 中,簡單的來說,就是移除指定的 time ranges’ buffer。需要用到的 API 爲:

remove(double start, unrestricted double end);

具體的步驟爲:

  • 找到具體需要移除的 segment。
  • 得到其開始(start)的時間戳(以 s 爲單位)
  • 得到其結束(end)的時間戳(以 s 爲單位)
  • 此時,updating 爲 true,表明正在移除
  • 完成之後,出發 updateend 事件

如果,你想直接清空 Buffer 重新添加的話,可以直接利用 abort() API 來做。它的工作是清空當前 SB 中所有的 segment,使用方法也很簡單,不過就是需要注意不要和 remove 操作一起執行。更保險的做法就是直接,通過 updating===false 來完成:

if(sb.updating===false){
    sb.abort();
}

這時候,abort 的主要流程爲:

  • 確保 MS.readyState===“open”
  • 將 appendWindowStart 設置爲 pts 原始值,比如,0
  • 將 appendWindowEnd 設置爲正無限大,即,Infinity。

abort(): 用來放棄當前 append 流的操作。不過,該方法的業務場景也比較有限。它只能用在當 SB 正在更新流的時候。即,此時通過 fetch,已經接受到新流,並且使用 appendBuffer 添加,此爲開始的時間。然後到 updateend 事件觸發之前,這段時間之內調用 abort()。有一個業務場景是,當用戶移動進度條,而此時 fetch 已經獲取前一次的 media segments,那麼可以使用 abort 放棄該操作,轉而請求新的 media segments。具體可以參考:abort 使用

7.appendBuffer(ArrayBuffer)
用來添加 ArrayBuffer。該 ArrayBuffer 一般是通過 fetch 的 response.arrayBuffer(); 來獲取的。在使用 addSourceBuffer 創建之前,還需要保證當前瀏覽器是否支持該編碼格式。當然,不支持也行,頂多是當前 MS 報錯,斷掉當前 JS 線程。

八、MediaSource簡介

[Constructor]
interface MediaSource : EventTarget {
    readonly attribute SourceBufferList    sourceBuffers;
    readonly attribute SourceBufferList    activeSourceBuffers;
    readonly attribute ReadyState          readyState;
             attribute unrestricted double duration;
             attribute EventHandler        onsourceopen;
             attribute EventHandler        onsourceended;
             attribute EventHandler        onsourceclose;
    SourceBuffer addSourceBuffer(DOMString type);
    void         removeSourceBuffer(SourceBuffer sourceBuffer);
    void         endOfStream(optional EndOfStreamError error);
    void         setLiveSeekableRange(double start, double end); 
    void         clearLiveSeekableRange();
    static boolean isTypeSupported(DOMString type);
};

1.isTypeSupported
isTypeSupported 主要是用來檢測 MS 是否支持某個特定的編碼和容器盒子。例如:

 

MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E, mp4a.40.2"')

這裏有一份具體的 mimeType 參考列表。

2.addSourceBuffer
用來返回一個具體的視頻流 SB,接受一個 mimeType 表示該流的編碼格式。例如:

var mimeType = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
var sourceBuffer = mediaSource.addSourceBuffer(mimeType);

3.removeSourceBuffer
用來移除某個 sourceBuffer。比如當前流已經結束,那麼你就沒必要再保留當前 SB 來佔用空間,可以直接移除。具體格式爲:

mediaSource.removeSourceBuffer(sourceBuffer);

4.endOfStream()
用來表示接受的視頻流的停止,注意,這裏並不是斷開,相當於只是下好了一部分視頻,然後你可以進行播放。此時,MS 的狀態變爲:ended。例如:

  var mediaSource = this;
  var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, function (buf) {
    sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream(); // 結束當前的接受
      video.play(); // 可以播放當前獲得的流
    });
    sourceBuffer.appendBuffer(buf);
  });

5.sourceBuffers
sourceBuffers 是 MS 實例上的一個屬性,它返回的是一個 SourceBufferList 的對象,裏面可以獲取當前 MS 上掛載的所有 SB。不過,只有當 MS 爲 open 狀態的時候,它纔可以訪問。具體使用爲:

let SBs = mediaSource.sourceBuffers;

那我們怎麼獲取到具體的 SB 對象呢?因爲,其返回值是 SourceBufferList 對象,具體格式爲:

interface SourceBufferList : EventTarget {
    readonly attribute unsigned long length;
             attribute EventHandler  onaddsourcebuffer;
             attribute EventHandler  onremovesourcebuffer;
    getter SourceBuffer (unsigned long index);
};

簡單來說,你可以直接通過 index 來訪問具體的某個 SB:

let SBs = mediaSource.sourceBuffers;

let SB1 = SBs[0];

SBL 對象還提供了 addsourcebuffer 和 removesourcebuffer 事件,如果你想監聽 SB 的變化,可以直接通過 SBL 來做。這也是爲什麼 MS 沒有提供監聽事件的一個原因。所以,刪除某一個 SB 就可以通過 SBL 查找,然後,利用 remove 方法移除即可:

 

let SBs = mediaSource.sourceBuffers;

let SB1 = SBs[0];

mediaSource.removeSourceBuffer(SB1);

6.activeSourceBuffers
activeSourceBuffers 實際上是 sourceBuffers 的子集,返回的同樣也是 SBL 對象。爲什麼說也是子集呢?

因爲 ASBs 包含的是當前正在使用的 SB。因爲前面說了,每個 SB 實際上都可以具體代表一個 track,比如,video track,audio track,text track 等等,這些都算。那怎麼標識正在使用的 SB 呢?很簡單,不用標識啊,因爲控制哪一個 SB 正在使用是你來決定的。如果非要標識,就需要使用到 HTML 中的 video 和 audio 節點。通過

audioTrack = media.audioTracks[index]
videoTrack = media.videoTracks[index]

// media 爲具體的 video/audio 的節點
// 返回值就是 video/audio 的底層 tracks

audioTrack = media.audioTracks.getTrackById( id )
videoTrack = media.videoTracks.getTrackById( id )

videoTrack.selected // 返回 boolean 值,標識是否正在被使用

上面的代碼只是告訴你,正在使用 的含義是什麼。對於我們實際編碼的 SB 來說,並沒有太多關係,瞭解就好。上面說了 ASBs 返回值也是一個 SBL。所以,使用方式可以直接參考 SBL 即可。

7.狀態切換
要說道狀態切換,我們得先知道 MS 一共有幾個狀態值。MS 本身狀態並不複雜,一共只有三個狀態值:

enum ReadyState {
    "closed",
    "open",
    "ended"
};
  • closed: 當前的 MS 並沒有和 HTMLMedia 元素連接
  • open: MS 已經和 HTMLMedia 連接,並且等待新的數據被添加到 SB 中去。
  • ended: 當調用 endOfStream 方法時會觸發,並且此時依然和 HTMLMedia 元素連接。

記住,closed 和 ended 到的區別關鍵點在於有沒有和 HTMLMedia 元素連接。

其對應的還有三個監聽事件:

  • sourceopen: 當狀態變爲 open 時觸發。常常在 MS 和 HTMLMedia 綁定時觸發。
  • sourceended: 當狀態變爲 ended 時觸發。
  • sourceclose: 當狀態變爲 closed 時觸發。

那哪種條件下會觸發呢?

(1)sourceopen 觸發
sourceopen 事件相同於是一個總領事件,只有當 sourceopen 時間觸發後,後續對於 MS 來說,纔是一個可操作的對象。通常來說,只有當 MS 和 video 元素成功綁定時,纔會正常觸發:

let mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);

其實這簡單的來說,就是給 MS 添加 HTML media 元素。其整個過程爲:

  • 先延時 media 元素的 load 事件,將 delaying-the-load-event-flag 設置爲 false
  • 將 readyState 設置爲 open。
  • 觸發 MS 的 sourceopen 事件

(2)sourceended 觸發
sourceended 的觸發條件其實很簡單,只有當你調用 endOfStream 的時候,會進行相關的觸發。mediaSource.endOfStream();這個就沒啥需要過多講的了。

(3)sourceclose 的觸發
sourceclose 是在 media 元素和 MS 斷開的時候,纔會觸發。那這個怎麼斷開呢?難道直接將 media 的元素的 src 直接設置爲 null 就 OK 了嗎?要是這樣,我就日了狗了。MS 會這麼簡單麼?實際上並不,如果要手動觸發 sourceclose 事件的話,則需要下列步驟:

  • 將 readyState 設置爲 closed
  • 將 MS.duration 設置爲 NaN
  • 移除 activeSourceBuffers 上的所有 Buffer
  • 觸發 activeSourceBuffers 的 removesourcebuffer 事件
  • 移除 sourceBuffers 上的 SourceBuffer。
  • 觸發 sourceBuffers 的 removesourcebuffer 事件
  • 觸發 MediaSource 的 sourceclose 事件

到這裏,三個狀態事件基本就介紹完了。不過,感覺只有 sourceopen 纔是最有用的一個。

8.track 的切換
track 這個概念其實是音視頻播放的軌道,它和 MS 沒有太大的關係。不過,和 SB 還是有一點關係的。因爲,某個一個 SB 裏面可能會包含一個 track 或者說是幾個 track。所以,推薦某一個 SB 最好包含一個值包含一個 track,這樣,後面的 track 也方便更換。在 track 中的替換裏,有三種類型,audio,video,text 軌道。
(1)video 切換
切換的含義有兩種,一種是移除原有的,一種是添加新的。這裏,我們需要分兩部分來講解。
(a)移除原有不需要 track

  • 從 activeSourceBuffers 移除與當前 track 相關的 SB
  • 觸發 activeSourceBuffers 的 removesourcebuffer 事件

(b)添加指定的 track

  • 從 activeSourceBuffers 添加指定的 SourceBuffer
  • 觸發 activeSourceBuffers 的 addsourcebuffer 事件

(2)audio 切換
audio 的切換和 video 的過程一模一樣。這裏我就不過多贅述了。

9.MS duration 修正機制
MS 的 duration 實際上就是 media 中播放的時延。通常來說,A/V track 實際上是兩個獨立的播放流,這中間必定會存在先關的差異時間。但是,media 播放機制永遠會以最長的 duration 爲準。這種情況對於 live stream 的播放,特別適合。因爲 liveStream 是不斷動態添加 buffer,但是 buffer 內部會有一定的時長的,而 MS 就需要針對這個 buffer 進行動態更新。整個更新機制爲:

  • 當前 MS.duration 更新爲 new duration。
  • 如果 new duration 比 sourceBuffers 中的最大的 pts 小,這時候就會報錯。
  • 讓最後一個的 sample 的 end time 爲所後 timeRanges 的 end time。
  • 將 new duration 設置爲當前 SourceBuffer 中最大的 endTime。
  • 將 video/audio 的播放時長(duration) 設置爲最新的 new duration。

10.如何界定 track
這裏先聲明一下,track 和 SB 並不是一一對應的關係。他們的關係只能是 SB : track = 1: 1 or 2 or 3。即,一個 SB可能包含,一個 A/V track(1),或者,一個 Video track ,一個Audio track(2),或者 再額外加一個 text track(3)。

上面也說過,推薦將 track 和 SB 設置爲一一對應的關係,應該這樣比較好控制,比如,移除或者同步等操作。具體編碼細節我們有空再說,這裏先來說一下,SB 裏面怎麼決定 track 的播放。

track 最重要的特性就是 pts ,duration,access point flag。track 中 最基本的單位叫做 Coded Frame,表示具體能夠播放的音視頻數據。它本身其實就是一些列的 media data,並且這些 media data 裏面必須包含 pts,dts,sampleDuration 的相關信息。在 SB 中,有幾個基本內部屬性是用來標識前面兩個字段的。

  • last decode timestamp: 用來表示最新一個 frame 的編碼時間(pts)。默認爲 null 表示裏面沒有任何數據
  • last frame duration: 表示 coded frame group 裏面最新的 frame 時長。
  • highest end timestamp: 相當於就是最後一個 frame 的 pts + duration
  • need random access point flag: 這個就相當於是同步幀的意思。主要設置是根據音視頻流 裏面具體字段決定的,和前端這邊編碼沒關係。
  • track buffer ranges: 該字段表示的是 coded frame group 裏面,每一幀對應存儲的 pts 範圍。

這裏需要特別說一下 last frame duration 的概念,其實也就是 Coded Frame Duration 的內容。Coded Frame Duration 針對不同的 track 有兩種不同的含義。一種是針對 video/text 的 track,一種是針對 audio 的 track:

  • video/text: 其播放時長(duration)直接是根據 pts 直接的差值來決定,和你具體播放的 samplerate 沒啥關係。雖然,官方也有一個計算 refsampelDuration 的公式:duration = timescale / fps,不過,由於視頻的幀率是動態變化的,沒什麼太大的作用。
  • audio: audio 的播放時長必須是嚴格根據採樣頻率來的,即,其播放時間必須和你自己定製的 timescale 以及 sampleRate 一致纔行。針對於 AAC,因爲其採樣頻率常爲 44100Hz,其固定播放時長則爲:duration = 1024 / sampleRate * timescale

所以,如果你在針對 unstable stream 做同步的話,一定需要注意這個坑。有時候,dts 不同步,有可能纔是真正的同步。

我們再回到上面的子 title 上-- 如果界定 track。一個 SB 裏面是否擁有一個或者多個 track,主要是根據裏面的視頻格式來決定的。打個比方,比如,你是在編碼 MP4 的流文件。它裏面的 track 內容,則是根據 moov box 中的 trak box 來判斷的。即,如果你的 MP4 文件只包含一個,那麼,裏面的 track 也有隻有一個。

九、MSE兼容性 caniuse

1.iOS Safari 不支持 Media Source Extensions 因此無法使用 flv.js

以下摘自HTML5 媒體源擴展(MSE):把影視製作級別的視頻格式帶入 Web

要覆蓋99%的用戶,我們需要做一個視頻流兼容設置,這樣也可以讓那些不支持MSE的瀏覽器也能順利播放,比如一些舊版本的瀏覽器,和iOS上的Safari。老的瀏覽器可以使用Flash播放器來提供服務,Flash播放器是可以直接播放MSE的MPEG-DASH格式內容的,如Bitdash player播放器。爲了支持iOS設備,我們必須要使用Apple的HLS流媒體格式,這是蘋果在HTML5中強推的另一種方式。Apple並不喜歡支持開放標準(如MSE),不過Mac OSX上的Safari還是支持MSE的。

2.以下摘自X5內核視頻之問答彙總--本帖最後由 YongLing 於 2018-07-05 14:45:51 編輯

Q:X5內核支持MSE嗎?
A:X5內核MSE正在支持中,預計TBS44200版本及以後,QQ瀏覽器8.6版本及以後支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章