全面進階 H5 直播

視頻格式?編碼?

如果我們想要理解 HTML5 視頻,首先需要知道,你應該知道,但你不知道的內容?那怎麼去判斷呢? ok,很簡單,我提幾個問題即可,如果某些童鞋知道答案的話,可以直接跳過。

  1. 你知道 ogg,mp4,flv,webm(前面加個點 .)這些叫做什麼嗎?
  2. 那 FLV,MPEG-4,VP8 是啥?
  3. 如果,基友問你要片源,你會說我這是 mp4 的還是 MPEG-4 的呢?

當然,還有一些問題,我這裡就不廢話了。上面主要想說的其實就兩個概念:視頻文件格式(容器格式),視頻編解碼器(視頻編碼格式)。當然,還有另外一種,叫做音頻編解碼器。簡而言之,就是這三個概念比較重要:

  • 視頻文件格式(容器格式)
  • 視頻編解碼器(視頻編碼格式)
  • 音頻編解碼器(音頻編碼格式)

這裡,我們主要講解一下前面兩個。視頻一開始會由兩個端採集,一個是視頻輸入口,是一個音頻輸入口。然後,採集的數據會分別進行相關處理,簡而言之就是,將視頻/音頻流,通過一定的手段轉換為比特流。最終,將這裡比特流以一定順序放到一個盒子里進行存放,從而生成我們最終所看到的,比如,mp4/mp3/flv 等等音視頻格式。

視頻編碼格式

視頻編碼格式就是我們上面提到的第一步,將物理流轉換為比特流,並且進行壓縮。同樣,它的壓縮編碼格式會決定它的視頻文件格式。所以,第一步很重要。針對於 HTML5 中的 video/audio,它實際上是支持多種編碼格式的,但局限於各瀏覽器廠家的普及度,目前視頻格式支持度最高的是 MPEG-4/H.264,音頻則是 MP3/AC3。(下面就主要說下視頻的,音頻就先不談了。)

目前市面上,主流瀏覽器支持的幾個有:

  • H.264
  • MEPG-4 第 2 部分
  • VP8
  • Ogg
  • WebM(免費)

其它格式,我們這裡就不過多贅述,來看一下前兩個比較有趣的。如下圖:

請問,上面箭頭所指的編碼格式是同一個嗎?

答案是:No~

因為,MPEG-4 實際上是於 1999 年提出的一個標準。而 H.264 則是後台作為優化提出的新的標準。簡單來說就是,我們通常說的 MPEG-4 其實就是MPEG-4 Part 2。而,H.264 則是MPEG-4(第十部分,也叫ISO/IEC 14496-10),又可以理解為 MPEG-4 AVC。而兩者,不同的地方,可以參考:latthias 的講解。簡單的區別是:H.264 壓縮率比以前的 MPEG-4(第 2 部分) 高很多。簡單可以參考的就是:

詳細參考: 編碼格式詳解

視頻文件格式

視頻文件格式實際上,我們常常稱作為容器格式,也就是,我們一般生活中最經常談到的格式,flv,mp4,ogg 格式等。**它就可以理解為將比特流按照一定順序放進特定的盒子里。**那選用不同格式來裝視頻有什麼問題嗎? 答案是,沒有任何問題,但是你需要知道如何將該盒子解開,並且能夠找到對應的解碼器進行解碼。那如果按照這樣看的話,對於這些 mp4,ogv,webm等等視頻格式,只要我有這些對應的解碼器以及播放器,那麼就沒有任何問題。那麼針對於,將視頻比特流放進一個盒子裡面,如果其中某一段出現問題,那麼最終生成的文件實際上是不可用的,因為這個盒子本身就是有問題的。 不過,上面有一個誤解的地方在於,我只是將視頻理解為一個靜態的流。試想一下,如果一個視頻需要持續不斷的播放,例如,直播,現場播報等。這裡,我們就拿 TS/PS 流來進行講解。

  • PS(Program Stream): 靜態文件流
  • TS(Transport Stream): 動態文件流

針對於上面兩種容器格式,實際上是對一個視頻比特流做了不一樣的處理。

  • PS: 將完成視頻比特流放到一個盒子里,生成固定的文件
  • TS: 將接受到的視頻,分成不同的盒子里。最終生成帶有多個盒子的文件。

那麼結果就是,如果一個或多個盒子出現損壞,PS 格式無法觀看,而 TS 只是會出現跳幀或者馬賽克效應。兩者具體的區別就是:對於視頻的容錯率越高,則會選用 TS,對視頻容錯率越低,則會選用 PS。

常用為:

  • AVI:MPEG-2,DIVX,XVID,AC-1,H.264;
  • WMV:WMV,AC-1;
  • RM、RMVB:RV, RM;
  • MOV:MPEG-2,XVID,H.264;
  • TS/PS:MPEG-2,H.264,MPEG-4;
  • MKV:可以封裝所有的視頻編碼格式。

詳細參考:視頻文件格式

直播協議

2016 年是直播元年,一是由於各大寬頻提供商順應民意增寬降價,二是大量資本流進了直播板塊,促進了技術的更新迭代。市面上,最常用的是 Apple 推出的 HLS 直播協議(原始支持 H5 播放),當然,還有 RTMP、HTTP-FLV、RTP等。 這裡,再問一個問題:

  1. HLS 和 MPEG-4/H.264 以及容器格式 TS/PS 是啥關係?

簡單來說,沒關係。

HLS 根本就不會涉及到視頻本身的解碼問題。它的存在只是為了確保你的視頻能夠及時,快速,正確的播放。

現在,直播行業依舊很火,而 HTML5 直播,一直以來都是一個比較蛋疼的內容。一是,瀏覽器廠商更新速度比較慢,二是,這並不是我們前端專攻的一塊,所以,有時候的確很雞肋。當然,進了前端,你就別想著休息。接下來,我們來詳細的看一下市面上主流的幾個協議。

HLS

HLS 全稱是 HTTP Live Streaming。這是 Apple 提出的直播流協議。目前,IOS 和 高版本 Android 都支持 HLS。那什麼是 HLS 呢? HLS 主要的兩塊內容是 .m3u8 文件和 .ts 播放文件。接受伺服器會將接受到的視頻流進行緩存,然後緩存到一定程度後,會將這些視頻流進行編碼格式化,同時會生成一份 .m3u8 文件和其它很多的 .ts 文件。根據 wiki 闡述,HLS 的基本架構為:

  • 伺服器:後台伺服器接受視頻流,然後進行編碼和片段化。
    • 編碼:視頻格式編碼採用 H.264。音頻編碼為 AAC, MP3, AC-3,EC-3。然後使用 MPEG-2 Transport Stream 作為容器格式。
    • 分片:將 TS 文件分成若干個相等大小的 .ts 文件。並且生成一個 .m3u8 作為索引文件(確保包的順序)
  • 分發:由於 HLS 是基於 HTTP 的,所以,作為分發,最常用的就是 CDN 了。
  • 客戶端:使用一個 URL 去下載 m3u8 文件,然後,開始下載 ts 文件,下載完成後,使用 playback software(即時播放器) 進行播放。

這裡,我們著重介紹一下客戶端的過程。首先,直播之所以是直播,在於它的內容是實時更新的。那 HLS 是怎麼完成呢? 我們使用 HLS 直接就用一個 video 進行包括即可:

<video controls autoplay> <source src="http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" type="application/vnd.apple.mpegurl" /> <p class="warning">Your browser does not support HTML5 video.</p> </video>

根據上面的描述,它實際上就是去請求一個 .m3u8 的索引文件。該文件包含了對 .ts 文件的相關描述,例如:

#EXT-X-VERSION:3 PlayList 的版本,可帶可不帶。下面有說明#EXTM3U m3u文件頭#EXT-X-TARGETDURATION:10 分片最大時長,單位為 s#EXT-X-MEDIA-SEQUENCE:1 第一個TS分片的序列號,如果沒有,默認為 0#EXT-X-ALLOW-CACHE 是否允許cache#EXT-X-ENDLIST m3u8文件結束符#EXTINF 指定每個媒體段(ts)的持續時間(秒),僅對其後面的URI有效

不過,這只是一個非常簡單,不涉及任何功能的直播流。實際上,HLS 的整個架構,可以分為:

當然,如果你使用的是 masterplaylist 作為鏈接,如:

<video controls autoplay> <source src="http://devimages.apple.com/iphone/samples/bipbop/masterplaylist.m3u8" type="application/vnd.apple.mpegurl" /> <p class="warning">Your browser does not support HTML5 video.</p> </video>

我們看一下,masterplaylist 裡面具體的內容是啥:

#EXTM3U#EXT-X-VERSION:6#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540live/medium.m3u8#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720live/high.m3u8#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=640x360live/low.m3u8#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=416x234live/cellular.m3u8

EXT-X-STREAM-INF 這個標籤頭代表:當前用戶的播放環境。masterplaylist 主要乾的事就是根據, 當前用戶的帶寬,解析度,解碼器等條件決定使用哪一個流。所以,master playlist 是為了更好的用戶體驗而存在的。不過,弊端就是後台儲備流的量會成倍增加。 現在,我們來主要看一下,如果你使用 master playlist,那麼整個流程是啥? 當填寫了 master playlist URL,那麼用戶只會下載一次該 master playlist。接著,播放器根據當前的環境決定使用哪一個 media playlist(就是 子 m3u8 文件)。如果,在播放當中,用戶的播放條件發生變化時,播放器也會切換對應的 media playlist。關於 master playlist 內容,我們就先介紹到這裡。 關於 HLS,感覺主要內容還在 media playlist 上。當然,media playlist 還分為三種 list:

  • live playlist: 動態列表。顧名思義,該列表是動態變化的,裡面的 ts 文件會實時更新,並且過期的 ts 索引會被刪除。默認,情況下都是使用動態列表。
  • event playlist: 靜態列表。它和動態列表主要區別就是,原來的 ts 文件索引不會被刪除,該列表是不斷更新,而且文件大小會逐漸增大。它會在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作為標識。
  • VOD playlist: 全量列表。它就是將所有的 ts 文件都列在 list 當中。如果,使用該列表,就和播放一整個視頻沒有啥區別了。它是使用 #EXT-X-ENDLIST 表示文件結尾。

live playlist DEMO:

#EXTM3U#EXT-X-VERSION:6#EXT-X-TARGETDURATION:10#EXT-X-MEDIA-SEQUENCE:26#EXTINF:9.901,http://media.example.com/wifi/segment26.ts#EXTINF:9.901,http://media.example.com/wifi/segment27.ts#EXTINF:9.501,http://media.example.com/wifi/segment28.ts

evet playlist DEMO:

#EXTM3U#EXT-X-VERSION:6#EXT-X-TARGETDURATION:10#EXT-X-MEDIA-SEQUENCE:0#EXT-X-PLAYLIST-TYPE:EVENT#EXTINF:9.9001,http://media.example.com/wifi/segment0.ts#EXTINF:9.9001,http://media.example.com/wifi/segment1.ts#EXTINF:9.9001,http://media.example.com/wifi/segment2.ts

VOD playlist DEMO:

#EXTM3U#EXT-X-VERSION:6#EXT-X-TARGETDURATION:10#EXT-X-MEDIA-SEQUENCE:0#EXT-X-PLAYLIST-TYPE:VOD#EXTINF:9.9001,http://media.example.com/wifi/segment0.ts#EXTINF:9.9001,http://media.example.com/wifi/segment1.ts#EXTINF:9.9001,http://media.example.com/wifi/segment2.ts#EXT-X-ENDLIST

上面提到過一個 EXT-X-VERSION 這樣的標籤,這是用來表示當前 HLS 的版本。那 HLS 有哪些版本呢? 根據 apple 官方文檔 的說明,我們可以了解到,不同版本的區別:

當然,HLS 支持的功能,並不只是分片播放(專門適用於直播),它還包括其他應有的功能。

  • 使用 HTTPS 加密 ts 文件
  • 快/倒放
  • 廣告插入
  • 不同解析度視頻切換

HLS 的弊端

由於 HLS 是基於 HTTP 的,所以,它關於 HTTP 的好處,我們大部分都了解,比如,高兼容性,高可擴展性等。不過正由於是 HTTP 協議,所以會在握手協議上造成一定的延遲性。HLS 首次連接時,總共的延時包括:

  1. TCP 握手,2. m3u8 文件下載,3. m3u8 下的 ts 文件下載。

其中,每個 ts 文件,大概會存放 5s~10s 的時長,並且每個 m3u8 文件會存放 3~8 個 ts 文件。我們折中算一下,5 個 ts 文件,每個時長大約 8s 那麼,總的下來,一共延時 40s。當然,這還不算上 TCP 握手,m3u8 文件下載等問題。那優化辦法有嗎?有的,那就是減少每個 m3u8 文件中的 ts 數量和 ts 文件時長,不過,這樣也會成倍的增加後台承受流量請求的壓力。所以,這還是需要到業務中去探索最優的配置(打個廣告:騰訊雲的直播視頻流業務,做的確實挺棒。) 關於 HLS 的詳細內容,可以參考:HLS 詳解 關於 m3u8 文件的標籤內容,可以參考:HLS 標籤頭詳解 總而言之,HLS 之所以能這麼流行,關鍵在於它的支持度是真的廣,所以,對於一般 H5 直播來說,應該是非常友好的。不過,既然是直播,關鍵在於它的實時性,而 HLS 天生就存在一定的延時,所以,就可以考慮其他低延時的方案,比如 RTMP,HTTP-FLV。下面,我們來看一下 RTMP 內容。

RTMP

RTMP 全稱為:Real-Time Messaging Protocol 。它是專門應對實時交流場景而開發出來的一個協議。它爹是 Macromedia,後來賣身給了 Adobe。RTMP 根據不同的業務場景,有很多變種:

  • 純 RTMP 使用 TCP 連接,默認埠為 1935(有可能被封)。
  • RTMPS: 就是 RTMP + TLS/SSL
  • RTMPE: RTMP + encryption。在 RTMP 原始協議上使用,Adobe 自身的加密方法
  • RTMPT: RTMP + HTTP。使用 HTTP 的方式來包裹 RTMP 流,這樣能直接通過防火牆。
  • RTMFP: RMPT + UDP。該協議常常用於 P2P 的場景中,針對延時有變態的要求。

既然是 Adobe 公司開發的(算吧),那麼,該協議針對的就是 Flash Video,即,FLV。不過,在移動端上,Flash Player 已經被殺絕了,那為啥還會出現這個呢?簡單來說,它主要是針對 PC 端的。RTMP 出現的時候,還是 零幾 年的時候,IE 還在大行其道,Flash Player 也並未被各大瀏覽器所排斥。那時候 RTMP 毋庸置疑的可以在視頻界有自己的一席之地。

RTMP 由於藉由 TCP 長連接協議,所以,客戶端向服務端推流這些操作而言,延時性很低。它會將上傳的流分成不同的分片,這些分片的大小,有時候變,有時候不會變。默認情況下就是,64B 的音頻數據 + 128B 的視頻數據 + 其它數據(比如 頭,協議標籤等)。但 RTMP 具體傳輸的時候,會將分片進一步劃分為包,即,視頻包,音頻包,協議包等。因為,RTMP 在進行傳輸的時候,會建立不同的通道,來進行數據的傳輸,這樣對於不同的資源,對不同的通道設置相關的帶寬上限。

RTMP 處理的格式是 MP3/ACC + FLV1。 不過,由於支持性的原因,RTMP 並未在 H5 直播中,展示出優勢。下列是簡單的對比:

HTTP-FLV

HTTP-FLV 和 RTMPT 類似,都是針對於 FLV 視頻格式做的直播分發流。但,兩者有著很大的區別。

  • 相同點
    • 兩者都是針對 FLV 格式
    • 兩者延時都很低
    • 兩者都走的 HTTP 通道
  • 不同點
    • HTTP-FLv
      • 直接發起長連接,下載對應的 FLV 文件
      • 頭部信息簡單
    • RTMPT
      • 握手協議過於複雜
      • 分包,組包過程耗費精力大

通過上面來看,HTTP-FLV 和 RTMPT 確實不是一回事,但,如果了解 SRS(simple rtmp server),那麼 對 HTTP-FLV 應該清楚不少。SRS 本質上,就是 RTMP + FLV 進行傳輸。因為 RTMP 發的包很容易處理,通常 RTMP 協議會作為視頻上傳端來處理,然後經由伺服器轉換為 FLV 文件,通過 HTTP-FLV 下發給用戶。

現在市面上,比較常用的就是 HTTP-FLV 進行播放。但,由於手機端上不支持,所以,H5 的 HTTP-FLV 也是一個痛點。不過,現在 flv.js 可以幫助高版本的瀏覽器,通過 mediaSource 來進行解析。HTTP-FLV 的使用方式也很簡單。和 HLS 一樣,只需要添加一個連接即可:

<object type="application/x-shockwave-flash" src="http://s6.pdim.gs/static/a2a36bc596148316.flv"></object>

不過,並不是末尾是 .flv 的都是 HTTP-FLV 協議,因為,涉及 FLV 的流有三種,它們三種的使用方式都是一模一樣的。

  • FLV 文件:相當於就是一整個文件,官方稱為 漸進 HTTP 流。它的特點是只能漸進下載,不能進行點播。
  • FLV 偽流:該方式,可以通過在末尾添加 ?start=xxx 的參數,指定返回的對應開始時間視頻數據。該方式比上面那種就多了一個點播的功能。本質上還是 FLV 直播。
  • FLV 直播流:這就是 HTTP-FLV 真正所支持的流。SRS 在內部使用的是 RTMP 進行分發,然後在傳給用戶的使用,經過一層轉換,變為 HTTP 流,最終傳遞給用戶。

上面說到,HTTP-FLV 就是長連接,簡而言之只需要加上一個 Connection:keep-alive 即可。關鍵是它的響應頭,由於,HTTP-FLV 傳遞的是視頻格式,所有,它的 Content-Type 和 Transfer-Encoding 需要設置其它值。

Content-Type:video/x-flvExpires:Fri, 10 Feb 2017 05:24:03 GMTPragma:no-cacheTransfer-Encoding:chunked

不過,一般而言,直播伺服器一般和業務服務是不會放在一塊的,所以這裡,可能會額外需要支持跨域直播的相關技術。在 XHR2 裡面,解決辦法也很簡單,直接使用 CORS 即可:

// 那麼整個響應頭,可以為:Access-Control-Allow-credentials:trueAccess-Control-Allow-max-age:86400Access-Control-Allow-methods:GET,POST,OPTIONSAccess-Control-Allow-Origin:*Cache-Control:no-cacheContent-Type:video/x-flvExpires:Fri, 10 Feb 2017 05:24:03 GMTPragma:no-cacheTransfer-Encoding:chunked

對於 HTTP-FLV 來說,關鍵難點在於 RTMP 和 HTTP 協議的轉換,這裡我就不多說了。因為,我們主要針對的是前端開發,講一下和前端相關的內容。

接下來,我們在主要來介紹一下 FLV 格式的。因為,後面我們需要通過 mediaSource 來解碼 FLV。

FLV 格式淺析

FLV 原始格式,Adobe 可以直接看 flv格式詳解。我這裡就抽主要的內容講講。FLV 也是與時俱進,以前 FLV 的格式叫做 FLV,新版的可以叫做 F4V。兩者的區別,簡單的區分方法就是:

  • FLV 是專門針對 Flash 播放器的
  • F4V 是有點像 MEPG 格式的 Flash 播放,主要為了兼容 H.264/ACC。F4V 不支持 FLV(兩者本來都不是同一個格式)

這裡我們主要針對 FLV 進行相關了解。因為,一般情況下,後台發送視頻流時,為了簡潔快速,就是發送 FLV 視頻。FLV 由於年限比較久,它所支持的內容是 H.263,VP6 codec。FLV 一般可以嵌套在 .swf 文件當中,不過,對於 HTTP-FLV 等 FLV 直播流來說,一般直接使用 .flv 文件即可。在 07 年的時候,提出了 F4V 這個視頻格式,當然,FLV 等也會向前兼容。

這裡,我們來正式介紹一下 FLV 的格式。一個完整的 FLV 流包括 FLV Header + FLV Packets。

FLV Header

FLV 格式頭不難,就幾個欄位:

|Field|Data Type|Default|Details| |:—|:—|:—| |Signature|byte[3]|「FLV」|有三個B的大小,算是一種身份的象徵| |Version|uint8|1|只有 0x01 是有效的。其實就是默認值| |Flags|uint8 bitmask|0x05|表示該流的特徵。0x04 是 audio,0x01 是 video,0x05 是 audio+video| |Header Size|uint32_be|9|用來跳過多餘的頭|

FLV Packets

在 FLV 的頭部之後,就正式開始發送 FLV 文件。文件會被拆解為數個包(FLV tags)進行傳輸。每個包都帶有 15B 的頭。前 4 個位元組是用來代表前一個包的頭部內容,用來完成倒放的功能。整個包的結構為:

具體解釋如下:

欄位欄位大小默認值詳解Size of previous packetuint32_be0關於前一個包的信息,如果是第一個包,則該部分為 NULLPacket Typeuint818設置包的內容,如果是第一個包,則該部分為 AMF 元數據Payload Sizeuint24_bevaries該包的大小Timestamp Loweruint24_be0起始時間戳Timestamp Upperuint80持續時間戳,通常加上 Lower 實際上戳,代表整個時間。Stream IDuint24_be0流的類型,第一個流設為 NULLPayload Datafreeformvaries傳輸數據

其中,由於 Packet Type 的值可以取多個, 需要額外說明一下。

  • Packet Type
    • 1: RTMP 包的大小
    • 3: RTMP 位元組讀包反饋,RTMP ping,RTMP 伺服器帶寬,RTMP 客戶端帶寬
    • 8: 音頻和視頻的數據
    • 15: RTMP flex 流
    • 24: 經過封裝的 flash video。

上面是關於 FLV 簡單的介紹。不過,如果沒有 Media Source Extensions 的幫助,那麼上面說的基本上全是廢話。由於,Flash Player 已經被時代所遺棄,所以,我們不能在瀏覽器上,順利的播放 FLV 視頻。接下來,我們先來詳細了解一下 MSE 的相關內容。

Media Source Extensions

在沒有 MSE 出現之前,前端對 video 的操作,僅僅局限在對視頻文件的操作,而並不能對視頻流做任何相關的操作。現在 MSE 提供了一系列的介面,使開發者可以直接提供 media stream。

那 MSE 是如何完成視頻流的載入和播放呢?

入門實例

這可以參考 google 的 MSE 簡介

var vidElement = document.querySelector(video);if (window.MediaSource) { var mediaSource = new MediaSource(); vidElement.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener(sourceopen, sourceOpen);} else { console.log("The Media Source Extensions API is not supported.")}function sourceOpen(e) { URL.revokeObjectURL(vidElement.src); var mime = video/webm; codecs="opus, vp9"; var mediaSource = e.target; var sourceBuffer = mediaSource.addSourceBuffer(mime); var videoUrl = droid.webm; fetch(videoUrl) .then(function(response) { return response.arrayBuffer(); }) .then(function(arrayBuffer) { sourceBuffer.addEventListener(updateend, function(e) { if (!sourceBuffer.updating && mediaSource.readyState === open) { mediaSource.endOfStream(); } }); sourceBuffer.appendBuffer(arrayBuffer); });}

可以從上面的代碼看出,一套完整的執行代碼,不僅需要使用 MSE 而且,還有一下這些相關的 API。

  • HTMLVideoElement.getVideoPlaybackQuality()
  • SourceBuffer
  • SourceBufferList
  • TextTrack.sourceBuffer
  • TrackDefault
  • TrackDefaultList
  • URL.createObjectURL()
  • VideoPlaybackQuality
  • VideoTrack.sourceBuffer

我們簡單講解一下上面的流程。根據 google 的闡述,整個過程可以為:

  • 第一步,通過非同步拉取數據。
  • 第二步,通過 MediaSource 處理數據。
  • 第三步,將數據流交給 audio/video 標籤進行播放。

而中間傳遞的數據都是通過 Buffer 的形式來進行傳遞的。

中間有個需要注意的點,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)}

MS 對流的解析

MS 提供了我們對底層音視頻流的處理,那一開始我們怎麼決定以何種格式進行編解碼呢?

這裡,可以使用 addSourceBuffer(mime) 來設置相關的編碼器:

var mime = video/webm; codecs="opus, vp9"; var sourceBuffer = mediaSource.addSourceBuffer(mime);

然後通過,非同步拉取相關的音視頻流:

fetch(url).then(res=>{ return res.arrayBuffer();}).then(buffer=>{ sourceBuffer.appendBuffer(buffer);})

如果視頻已經傳完了,而相關的 Buffer 還在佔用內存,這時候,就需要我們顯示的中斷當前的 Buffer 內容。那麼最終我們的非同步處理結果變為:

fetch(url).then(res=>{ return res.arrayBuffer();}).then(function(arrayBuffer) { sourceBuffer.addEventListener(updateend, function(e) { // 是否有持續更新的流 if (!sourceBuffer.updating && mediaSource.readyState === open) { // 沒有,則中斷連接 mediaSource.endOfStream(); } }); sourceBuffer.appendBuffer(arrayBuffer); });

上面我們大致了解了一下關於 Media Source Extensions 的大致流程,但裡面的細節我們還沒有細講。接下來,我們來具體看一下 MSE 一籃子的生態技術包含哪些內容。首先是,MediaSource

MediaSource

MS(MediaSource) 可以理解為多個視頻流的管理工具。以前,我們只能下載一個清晰度的流,並且不能平滑切換低畫質或者高畫質的流,而現在我們可以利用 MS 實現這裡特性。我們先來簡單了解一下他的 API。

MS 的創建

創建一個 MS:

var mediaSource = new MediaSource();

相關方法

addSourceBuffer()

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

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

sourceBuffer 是直接和視頻流有交集的 API。例如:

function sourceOpen (_) { var mediaSource = this; var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); fetchAB(assetURL, function (buf) { sourceBuffer.addEventListener(updateend, function (_) { mediaSource.endOfStream(); video.play(); }); // 通過 fetch 添加視頻 Buffer sourceBuffer.appendBuffer(buf); });};

它通過 appendBuffer 直接添加視頻流,實現播放。不過,在使用 addSourceBuffer 創建之前,還需要保證當前瀏覽器是否支持該編碼格式。

removeSourceBuffer()

用來移除某個 sourceBuffer。移除也主要是考慮性能原因,將不需要的流移除以節省相應的空間,格式為:

mediaSource.removeSourceBuffer(sourceBuffer);

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); });

isTypeSupported()

該是用來檢測當前瀏覽器是否支持指定視頻格式的解碼。格式為:

var isItSupported = mediaSource.isTypeSupported(mimeType); // 返回值為 Boolean

mimeType 可以為 type 或者 type + codec。

例如:

// 不同的瀏覽器支持不一樣,不過基本的類型都支持。MediaSource.isTypeSupported(audio/mp3); // false,這裡應該為 audio/mpeg MediaSource.isTypeSupported(video/mp4); // trueMediaSource.isTypeSupported(video/mp4; codecs="avc1.4D4028, mp4a.40.2"); // true

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

MS 的狀態

當 MS 從創建開始,都會自帶一個 readyState 屬性,用來表示其當前打開的狀態。MS 有三個狀態:

  • closed: 當前 MS 沒有和 media element(比如:video.src) 相關聯。創建時,MS 就是該狀態。
  • open: source 打開,並且準備接受通過 sourceBuffer.appendBuffer 添加的數據。
  • ended: 當 endOfStream() 執行完成,會變為該狀態,此時,source 依然和 media element 連接。

var mediaSource = new MediaSource;mediaSource.readyState; // 默認為 closed

當由 closed 變為 open 狀態時,需要監聽 sourceopen 事件。

video.src = URL.createObjectURL(mediaSource);mediaSource.addEventListener(sourceopen, sourceOpen);

MS 針對這幾個狀態變化,提供了相關的事件:sourceopen,sourceended,sourceclose。

  • sourceopen: 當 「closed」 to 「open」 或者 「ended」 to 「open」 時觸發。
  • sourceended: 當 「open」 to 「ended」 時觸發。
  • sourceclose: 當 「open」 to 「closed」 或者 「ended」 to 「closed」 時觸發。

MS 還提供了其他的監聽事件 sourceopen,sourceended,sourceclose,updatestart,update,updateend,error,abort,addsourcebuffer,removesourcebuffer. 這裡主要選了比較重要的,其他的可以參考官方文檔。

MS 屬性

比較常用的屬性有: duration,readyState。

  • duration: 獲得當前媒體播放的時間,既可以設置(get),也可以獲取(set)。單位為 s(秒)

mediaSource.duration = 5.5; // 設置媒體流播放的時間var myDuration = mediaSource.duration; // 獲得媒體流開始播放的時間

在實際應用中為:

sourceBuffer.addEventListener(updateend, function (_) { mediaSource.endOfStream(); mediaSource.duration = 120; // 設置當前流播放的時間 video.play(); });

  • readyState: 獲得當前 MS 的狀態。取值上面已經講過了: closed,open,ended。

var mediaSource = new MediaSource; //此時的 mediaSource.readyState 狀態為 closed

以及:

sourceBuffer.addEventListener(updateend, function (_) { mediaSource.endOfStream(); // 調用該方法後結果為:ended video.play(); });

除了上面兩個屬性外,還有 sourceBuffers,activeSourceBuffers 這兩個屬性。用來返回通過 addSourceBuffer() 創建的 SourceBuffer 數組。這沒啥過多的難度。

接下來我們就來看一下靠底層的 sourceBuffer。

SourceBuffer

SourceBuffer 是由 mediaSource 創建,並直接和 HTMLMediaElement 接觸。簡單來說,它就是一個流的容器,裡面提供的 append(),remove() 來進行流的操作,它可以包含一個或者多個 media segments。同樣,接下來,我們再來看一下該構造函數上的基本屬性和內容。

基礎內容

前面說過 sourceBuffer 主要是一個用來存放流的容器,那麼,它是怎麼存放的,它存放的內容是啥,有沒有順序等等。這些都是 sourceBuffer 最最根本的問題。OK,接下來,我們來看一下的它的基本架構有些啥。

參考 W3C,可以基本了解到裡面的內容為:

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);};

上面這些屬性決定了其 sourceBuffer 整個基礎。

首先是 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;}

然後另外兩個就是 buffered 和 updating。

  • buffered:返回一個 timeRange 對象。用來表示當前被存儲在 SB 中的 buffer。
  • updating: 返回 Boolean,表示當前 SB 是否正在被更新。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 調用時。

另外還有一些其他的相關屬性,比如 textTracks,timestampOffset,trackDefaults,這裡就不多說了。實際上,SB 是一個事件驅動的對象,一些常見的處理,都是在具體的事件中完成的。那麼它又有哪些事件呢?

事件觸發

在 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(); });

相關方法

SB 處理流的方法就是 +/- : appendBuffer, remove。另外還有一個中斷處理函數 abort()。

  • appendBuffer(ArrayBuffer):用來添加 ArrayBuffer。該 ArrayBuffer 一般是通過 fetch 的 response.arrayBuffer(); 來獲取的。
  • remove(start, end): 用來移除具體某段的 media segments。
    • @param start/end: 都是時間單位(s)。用來表示具體某段的 media segments 的範圍。
  • abort(): 用來放棄當前 append 流的操作。不過,該方法的業務場景也比較有限。它只能用在當 SB 正在更新流的時候。即,此時通過 fetch,已經接受到新流,並且使用 appendBuffer 添加,此為開始的時間。然後到 updateend 事件觸發之前,這段時間之內調用 abort()。有一個業務場景是,當用戶移動進度條,而,此時 fetch 已經獲取前一次的 media segments,那麼可以使用 abort 放棄該操作,轉而請求新的 media segments。具體可以參考:abort 使用

上面主要介紹了處理音視頻流需要用的 Web 技術,後面章節,我們接入實戰,具體來講一下,如何做到使用 MSE 進行 remux 和 demux。


推薦閱讀:

你不知道的 JS 錯誤和調用棧常識
相比 React 全家桶,選擇 Vue2 有何優劣?
為什麼 TypeScript 成功了,更先進的 ActionScript 卻失敗了?
從前端開發看面向未來的敏捷學習法
每個頁面都新建了一個css,這樣會不會帶來麻煩?後端的也總說這樣不利於修改,一改就要一路改過去好麻煩的說。

TAG:直播 | HTML5 | 前端开发 |