Web 開發(fā)者們一直以來想在 Web 中使用音頻和視頻以故,但早些時候蜗细,傳統(tǒng)的 Web 技術(shù)不能夠在 Web 中嵌入音頻和視頻,所以一些像 Flash怒详、Silverlight 的專利技術(shù)在處理這些內(nèi)容上變得很受歡迎炉媒。
這些技術(shù)能夠正常的工作,但是卻有著一系列的問題昆烁,包括無法很好的支持 HTML/CSS 特性吊骤、安全問題,以及可行性問題静尼。
幸運的是白粉,當 HTML5 標準公布后,其中包含許多的新特性鼠渺,包括 <video>
和 <audio>
標簽鸭巴,以及一些 JavaScript APIs 用于對其進行控制。隨著通信技術(shù)和網(wǎng)絡技術(shù)的不斷發(fā)展拦盹,目前音視頻已經(jīng)成為大家生活中不可或缺的一部分鹃祖。此外,伴隨著 5G 技術(shù)的慢慢普及普舆,實時音視頻領(lǐng)域還會有更大的想象空間恬口。
接下來本文將從八個方面入手,全方位帶你一起探索前端 Video 播放器和主流的流媒體技術(shù)沼侣。閱讀完本文后祖能,你將了解以下內(nèi)容:
- 為什么一些網(wǎng)頁中的 Video 元素,其視頻源地址是采用 Blob URL 的形式蛾洛;
- 什么是 HTTP Range 請求及流媒體技術(shù)相關(guān)概念养铸;
- 了解 HLS、DASH 的概念、自適應比特率流技術(shù)及流媒體加密技術(shù)揭厚;
- 了解 FLV 文件結(jié)構(gòu)却特、flv.js 的功能特性與使用限制及內(nèi)部的工作原理;
- 了解 MSE(Media Source Extensions)API 及相關(guān)的使用筛圆;
- 了解視頻播放器的原理裂明、多媒體封裝格式及 MP4 與 Fragmented MP4 封裝格式的區(qū)別;
在最后的 「阿寶哥有話說」 環(huán)節(jié)太援,阿寶哥將介紹如何實現(xiàn)播放器截圖闽晦、如何基于截圖生成 GIF、如何使用 Canvas 播放視頻及如何實現(xiàn)色度鍵控等功能提岔。
一仙蛉、傳統(tǒng)的播放模式
大多數(shù) Web 開發(fā)者對 <video>
都不會陌生,在以下 HTML 片段中碱蒙,我們聲明了一個 <video>
元素并設(shè)置相關(guān)的屬性荠瘪,然后通過 <source>
標簽設(shè)置視頻源和視頻格式:
<video id="mse" autoplay=true playsinline controls="controls">
<source src="https://h5player.bytedance.com/video/mp4/xgplayer-demo-720p.mp4" type="video/mp4">
你的瀏覽器不支持Video標簽
</video>
上述代碼在瀏覽器渲染之后,在頁面中會顯示一個 Video 視頻播放器赛惩,具體如下圖所示:
通過 Chrome 開發(fā)者工具哀墓,我們可以知道當播放 「xgplayer-demo-720p.mp4」 視頻文件時,發(fā)了 3 個 HTTP 請求:
此外喷兼,從圖中可以清楚地看到篮绰,頭兩個 HTTP 請求響應的狀態(tài)碼是 「206」。這里我們來分析第一個 HTTP 請求的請求頭和響應頭:
在上面的請求頭中季惯,有一個 range: bytes=0-
首部信息吠各,該信息用于檢測服務端是否支持 Range 請求。如果在響應中存在 Accept-Ranges
首部(并且它的值不為 “none”)勉抓,那么表示該服務器支持范圍請求贾漏。
在上面的響應頭中, Accept-Ranges: bytes
表示界定范圍的單位是 bytes
藕筋。這里 Content-Length
也是有效信息磕瓷,因為它提供了要下載的視頻的完整大小。
1.1 從服務器端請求特定的范圍
假如服務器支持范圍請求的話念逞,你可以使用 Range 首部來生成該類請求。該首部指示服務器應該返回文件的哪一或哪幾部分边翁。
1.1.1 單一范圍
我們可以請求資源的某一部分翎承。這里我們使用 Visual Studio Code 中的 REST Client 擴展來進行測試,在這個例子中符匾,我們使用 Range 首部來請求 www.example.com 首頁的前 1024 個字節(jié)叨咖。
對于使用 REST Client 發(fā)起的 「單一范圍請求」,服務器端會返回狀態(tài)碼為 「206 Partial Content」 的響應。而響應頭中的 「Content-Length」 首部現(xiàn)在用來表示先前請求范圍的大械楦鳌(而不是整個文件的大卸庀汀)。「Content-Range」 響應首部則表示這一部分內(nèi)容在整個資源中所處的位置趣倾。
1.1.2 多重范圍
Range 頭部也支持一次請求文檔的多個部分聘惦。請求范圍用一個逗號分隔開。比如:
$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"
對于該請求會返回以下響應信息:
因為我們是請求文檔的多個部分儒恋,所以每個部分都會擁有獨立的 「Content-Type」 和 「Content-Range」 信息善绎,并且使用 boundary 參數(shù)對響應體進行劃分。
1.1.3 條件式范圍請求
當重新開始請求更多資源片段的時候诫尽,必須確保自從上一個片段被接收之后該資源沒有進行過修改禀酱。
「If-Range」 請求首部可以用來生成條件式范圍請求:假如條件滿足的話,條件請求就會生效牧嫉,服務器會返回狀態(tài)碼為 206 Partial 的響應剂跟,以及相應的消息主體。假如條件未能得到滿足酣藻,那么就會返回狀態(tài)碼為 「200 OK」 的響應曹洽,同時返回整個資源。該首部可以與 「Last-Modified」 驗證器或者 「ETag」 一起使用臊恋,但是二者不能同時使用衣洁。
1.1.4 范圍請求的響應
與范圍請求相關(guān)的有三種狀態(tài):
- 在請求成功的情況下,服務器會返回 「206 Partial Content」 狀態(tài)碼抖仅。
- 在請求的范圍越界的情況下(范圍值超過了資源的大蟹环颉),服務器會返回 「416 Requested Range Not Satisfiable」 (請求的范圍無法滿足) 狀態(tài)碼撤卢。
- 在不支持范圍請求的情況下环凿,服務器會返回 「200 OK」 狀態(tài)碼。
剩余的兩個請求放吩,阿寶哥就不再詳細分析了智听。感興趣的小伙伴,可以使用 Chrome 開發(fā)者工具查看一下具體的請求報文渡紫。
通過第 3 個請求到推,我們可以知道整個視頻的大小大約為 7.9 MB。若播放的視頻文件太大或出現(xiàn)網(wǎng)絡不穩(wěn)定惕澎,則會導致播放時莉测,需要等待較長的時間,這嚴重降低了用戶體驗唧喉。
那么如何解決這個問題呢捣卤?要解決該問題我們可以使用流媒體技術(shù)忍抽,接下來我們來介紹流媒體。
二董朝、流媒體
流媒體是指將一連串的媒體數(shù)據(jù)壓縮后鸠项,經(jīng)過網(wǎng)上分段發(fā)送數(shù)據(jù),在網(wǎng)上即時傳輸影音以供觀賞的一種技術(shù)與過程子姜,此技術(shù)使得數(shù)據(jù)包得以像流水一樣發(fā)送祟绊;如果不使用此技術(shù),就必須在使用前下載整個媒體文件闲询。
流媒體實際指的是一種新的媒體傳送方式久免,有聲音流、視頻流扭弧、文本流阎姥、圖像流、動畫流等鸽捻,而非一種新的媒體呼巴。流媒體最主要的技術(shù)特征就是流式傳輸,它使得數(shù)據(jù)可以像流水一樣傳輸御蒲。流式傳輸是指通過網(wǎng)絡傳送媒體技術(shù)的總稱衣赶。實現(xiàn)流式傳輸主要有兩種方式:順序流式傳輸(Progressive Streaming)和實時流式傳輸(Real Time Streaming)。
目前網(wǎng)絡上常見的流媒體協(xié)議:
通過上表可知厚满,不同的協(xié)議有著不同的優(yōu)缺點府瞄。在實際使用過程中,我們通常會在平臺兼容的條件下選用最優(yōu)的流媒體傳輸協(xié)議碘箍。比如遵馆,在瀏覽器里做直播,選用 HTTP-FLV 協(xié)議是不錯的丰榴,性能優(yōu)于 RTMP+Flash货邓,延遲可以做到和 RTMP+Flash 一樣甚至更好。
而由于 HLS 延遲較大四濒,一般只適合視頻點播的場景换况,但由于它在移動端擁有較好的兼容性,所以在接受高延遲的條件下盗蟆,也是可以應用在直播場景戈二。
講到這里相信有些小伙伴會好奇,對于 Video 元素來說使用流媒體技術(shù)之后與傳統(tǒng)的播放模式有什么直觀的區(qū)別。下面阿寶哥以常見的 HLS 流媒體協(xié)議為例,來簡單對比一下它們之間的區(qū)別佑吝。
通過觀察上圖围段,我們可以很明顯地看到亏栈,當使用 HLS 流媒體網(wǎng)絡傳輸協(xié)議時,<video>
元素 src
屬性使用的是 blob://
協(xié)議宏赘。講到該協(xié)議绒北,我們就不得不聊一下 Blob 與 Blob URL。
2.1 Blob
Blob(Binary Large Object)表示二進制類型的大對象察署。在數(shù)據(jù)庫管理系統(tǒng)中闷游,將二進制數(shù)據(jù)存儲為一個單一個體的集合。Blob 通常是影像贴汪、聲音或多媒體文件脐往。「在 JavaScript 中 Blob 類型的對象表示不可變的類似文件對象的原始數(shù)據(jù)“夤。」
Blob
由一個可選的字符串 type
(通常是 MIME 類型)和 blobParts
組成:
?
MIME(Multipurpose Internet Mail Extensions)多用途互聯(lián)網(wǎng)郵件擴展類型业簿,是設(shè)定某種擴展名的文件用一種應用程序來打開的方式類型,當該擴展名文件被訪問的時候阳懂,瀏覽器會自動使用指定應用程序來打開梅尤。多用于指定一些客戶端自定義的文件名,以及一些媒體文件打開方式岩调。
常見的 MIME 類型有:超文本標記語言文本 .html text/html巷燥、PNG圖像 .png image/png、普通文本 .txt text/plain 等号枕。
?
為了更直觀的感受 Blob 對象缰揪,我們先來使用 Blob 構(gòu)造函數(shù),創(chuàng)建一個 myBlob 對象葱淳,具體如下圖所示:
如你所見钝腺,myBlob 對象含有兩個屬性:size 和 type。其中 size
屬性用于表示數(shù)據(jù)的大型茏稀(以字節(jié)為單位)拍屑,type
是 MIME 類型的字符串。Blob 表示的不一定是 JavaScript 原生格式的數(shù)據(jù)坑傅。比如 File
接口基于 Blob
僵驰,繼承了 blob 的功能并將其擴展使其支持用戶系統(tǒng)上的文件。
2.2 Blob URL/Object URL
Blob URL/Object URL 是一種偽協(xié)議唁毒,允許 Blob 和 File 對象用作圖像蒜茴,下載二進制數(shù)據(jù)鏈接等的 URL 源。在瀏覽器中浆西,我們使用 URL.createObjectURL
方法來創(chuàng)建 Blob URL粉私,該方法接收一個 Blob
對象,并為其創(chuàng)建一個唯一的 URL近零,其形式為 blob:<origin>/<uuid>
诺核,對應的示例如下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
瀏覽器內(nèi)部為每個通過 URL.createObjectURL
生成的 URL 存儲了一個 URL → Blob 映射抄肖。因此,此類 URL 較短窖杀,但可以訪問 Blob
漓摩。生成的 URL 僅在當前文檔打開的狀態(tài)下才有效。但如果你訪問的 Blob URL 不再存在入客,則會從瀏覽器中收到 404 錯誤管毙。
上述的 Blob URL 看似很不錯,但實際上它也有副作用桌硫。雖然存儲了 URL → Blob 的映射夭咬,但 Blob 本身仍駐留在內(nèi)存中,瀏覽器無法釋放它铆隘。映射在文檔卸載時自動清除卓舵,因此 Blob 對象隨后被釋放。但是咖驮,如果應用程序壽命很長边器,那不會很快發(fā)生。因此托修,如果我們創(chuàng)建一個 Blob URL忘巧,即使不再需要該 Blob,它也會存在內(nèi)存中睦刃。
針對這個問題砚嘴,我們可以調(diào)用 URL.revokeObjectURL(url)
方法,從內(nèi)部映射中刪除引用涩拙,從而允許刪除 Blob(如果沒有其他引用)际长,并釋放內(nèi)存。
2.3 Blob vs ArrayBuffer
其實在前端除了 「Blob 對象」 之外兴泥,你還可能會遇到 「ArrayBuffer 對象」工育。它用于表示通用的,固定長度的原始二進制數(shù)據(jù)緩沖區(qū)搓彻。你不能直接操縱 ArrayBuffer 的內(nèi)容如绸,而是需要創(chuàng)建一個 TypedArray 對象或 DataView 對象,該對象以特定格式表示緩沖區(qū)旭贬,并使用該對象讀取和寫入緩沖區(qū)的內(nèi)容怔接。
Blob 對象與 ArrayBuffer 對象擁有各自的特點,它們之間的區(qū)別如下:
除非你需要使用 ArrayBuffer 提供的寫入/編輯的能力稀轨,否則 Blob 格式可能是最好的扼脐。
Blob 對象是不可變的,而 ArrayBuffer 是可以通過 TypedArrays 或 DataView 來操作奋刽。
ArrayBuffer 是存在內(nèi)存中的瓦侮,可以直接操作艰赞。而 Blob 可以位于磁盤、高速緩存內(nèi)存和其他不可用的位置脏榆。
雖然 Blob 可以直接作為參數(shù)傳遞給其他函數(shù)猖毫,比如
window.URL.createObjectURL()
。但是须喂,你可能仍需要 FileReader 之類的 File API 才能與 Blob 一起使用。Blob 與 ArrayBuffer 對象之間是可以相互轉(zhuǎn)化的:
使用 FileReader 的
readAsArrayBuffer()
方法趁蕊,可以把 Blob 對象轉(zhuǎn)換為 ArrayBuffer 對象坞生;使用 Blob 構(gòu)造函數(shù),如
new Blob([new Uint8Array(data]);
掷伙,可以把 ArrayBuffer 對象轉(zhuǎn)換為 Blob 對象是己。
在前端 AJAX 場景下,除了常見的 JSON 格式之外任柜,我們也可能會用到 Blob 或 ArrayBuffer 對象:
function GET(url, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer'; // or xhr.responseType = "blob";
xhr.send();
xhr.onload = function(e) {
if (xhr.status != 200) {
alert("Unexpected status code " + xhr.status + " for " + url);
return false;
}
callback(new Uint8Array(xhr.response)); // or new Blob([xhr.response]);
};
}
在以上示例中卒废,通過為 xhr.responseType 設(shè)置不同的數(shù)據(jù)類型,我們就可以根據(jù)實際需要獲取對應類型的數(shù)據(jù)了宙地。介紹完上述內(nèi)容摔认,下面我們先來介紹目前應用比較廣泛的 HLS 流媒體傳輸協(xié)議。
三宅粥、HLS
3.1 HLS 簡介
HTTP Live Streaming(縮寫是 HLS)是由蘋果公司提出基于 HTTP 的流媒體網(wǎng)絡傳輸協(xié)議参袱,它是蘋果公司 QuickTime X 和 iPhone 軟件系統(tǒng)的一部分。它的工作原理是把整個流分成一個個小的基于 HTTP 的文件來下載秽梅,每次只下載一些抹蚀。當媒體流正在播放時,客戶端可以選擇從許多不同的備用源中以不同的速率下載同樣的資源企垦,允許流媒體會話適應不同的數(shù)據(jù)速率环壤。
此外,當用戶的信號強度發(fā)生抖動時钞诡,視頻流會動態(tài)調(diào)整以提供出色的再現(xiàn)效果郑现。
最初, 僅 iOS 支持 HLS臭增。但現(xiàn)在 HLS 已成為專有格式懂酱,幾乎所有設(shè)備都支持它。顧名思義誊抛,HLS(HTTP Live Streaming)協(xié)議通過標準的 HTTP Web 服務器傳送視頻內(nèi)容列牺。這意味著你無需集成任何特殊的基礎(chǔ)架構(gòu)即可分發(fā) HLS 內(nèi)容。
HLS 擁有以下特性:
- HLS 將播放使用 H.264 或 HEVC / H.265 編解碼器編碼的視頻拗窃。
- HLS 將播放使用 AAC 或 MP3 編解碼器編碼的音頻瞎领。
- HLS 視頻流一般被切成 10 秒的片段泌辫。
- HLS 的傳輸/封裝格式是 MPEG-2 TS。
- HLS 支持 DRM(數(shù)字版權(quán)管理)九默。
- HLS 支持各種廣告標準震放,例如 VAST 和 VPAID。
為什么蘋果要提出 HLS 這個協(xié)議驼修,其實它的主要是為了解決 RTMP 協(xié)議存在的一些問題殿遂。比如 RTMP 協(xié)議不使用標準的 HTTP 接口傳輸數(shù)據(jù),所以在一些特殊的網(wǎng)絡環(huán)境下可能被防火墻屏蔽掉乙各。但是 HLS 由于使用的 HTTP 協(xié)議傳輸數(shù)據(jù)墨礁,通常情況下不會遇到被防火墻屏蔽的情況。除此之外耳峦,它也很容易通過 CDN(內(nèi)容分發(fā)網(wǎng)絡)來傳輸媒體流恩静。
3.2 HLS 自適應比特流
HLS 是一種自適應比特率流協(xié)議。因此蹲坷,HLS 流可以動態(tài)地使視頻分辨率自適應每個人的網(wǎng)絡狀況驶乾。如果你正在使用高速 WiFi,則可以在手機上流式傳輸高清視頻循签。但是级乐,如果你在有限數(shù)據(jù)連接的公共汽車或地鐵上,則可以以較低的分辨率觀看相同的視頻懦底。
在開始一個流媒體會話時唇牧,客戶端會下載一個包含元數(shù)據(jù)的 Extended M3U(m3u8)Playlist 文件,用于尋找可用的媒體流聚唐。
為了便于大家的理解丐重,我們使用 hls.js 這個 JavaScript 實現(xiàn)的 HLS 客戶端,所提供的 在線示例杆查,來看一下具體的 m3u8 文件扮惦。
「x36xhzz.m3u8」
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
url_0/193039199_mp4_h264_aac_hd_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=246440,CODECS="mp4a.40.5,avc1.42000d",RESOLUTION=320x184,NAME="240"
url_2/193039199_mp4_h264_aac_ld_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=460560,CODECS="mp4a.40.5,avc1.420016",RESOLUTION=512x288,NAME="380"
url_4/193039199_mp4_h264_aac_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=836280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=848x480,NAME="480"
url_6/193039199_mp4_h264_aac_hq_7.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6221600,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,NAME="1080"
url_8/193039199_mp4_h264_aac_fhd_7.m3u8
通過觀察 Master Playlist 對應的 m3u8 文件,我們可以知道該視頻支持以下 5 種不同清晰度的視頻:
- 1920x1080(1080P)
- 1280x720(720P)
- 848x480(480P)
- 512x288
- 320x184
而不同清晰度視頻對應的媒體播放列表亲桦,會定義在各自的 m3u8 文件中崖蜜。這里我們以 720P 的視頻為例,來查看其對應的 m3u8 文件:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:11
#EXTINF:10.000,
url_462/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
url_463/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
url_464/193039199_mp4_h264_aac_hd_7.ts
#EXTINF:10.000,
...
url_525/193039199_mp4_h264_aac_hd_7.ts
#EXT-X-ENDLIST
當用戶選定某種清晰度的視頻之后客峭,將會下載該清晰度對應的媒體播放列表(m3u8 文件)豫领,該列表中就會列出每個片段的信息。HLS 的傳輸/封裝格式是 MPEG-2 TS(MPEG-2 Transport Stream)舔琅,是一種傳輸和存儲包含視頻等恐、音頻與通信協(xié)議各種數(shù)據(jù)的標準格式,用于數(shù)字電視廣播系統(tǒng),如 DVB课蔬、ATSC囱稽、IPTV 等等。
「需要注意的是利用一些現(xiàn)成的工具二跋,我們是可以把多個 TS 文件合并為 mp4 格式的視頻文件战惊。」 如果要做視頻版權(quán)保護扎即,那我們可以考慮使用對稱加密算法吞获,比如 AES-128 對切片進行對稱加密。當客戶端進行播放時谚鄙,先根據(jù) m3u8 文件中配置的密鑰服務器地址衫哥,獲取對稱加密的密鑰,然后再下載分片襟锐,當分片下載完成后再使用匹配的對稱加密算法進行解密播放。
對上述過程感興趣的小伙伴可以參考 Github 上 video-hls-encrypt 這個項目膛锭,該項目深入淺出介紹了基于 HLS 流媒體協(xié)議視頻加密的解決方案并提供了完整的示例代碼粮坞。
介紹完蘋果公司推出的 HLS (HTTP Live Streaming)技術(shù),接下來我們來介紹另一種基于 HTTP 的動態(tài)自適應流 —— DASH初狰。
四莫杈、DASH
4.1 DASH 簡介
「基于 HTTP 的動態(tài)自適應流(英語:Dynamic Adaptive Streaming over HTTP,縮寫 DASH奢入,也稱 MPEG-DASH)是一種自適應比特率流技術(shù)筝闹,使高質(zhì)量流媒體可以通過傳統(tǒng)的 HTTP 網(wǎng)絡服務器以互聯(lián)網(wǎng)傳遞⌒裙猓」 類似蘋果公司的 HTTP Live Streaming(HLS)方案关顷,MPEG-DASH 會將內(nèi)容分解成一系列小型的基于 HTTP 的文件片段,每個片段包含很短長度的可播放內(nèi)容武福,而內(nèi)容總長度可能長達數(shù)小時议双。
內(nèi)容將被制成多種比特率的備選片段,以提供多種比特率的版本供選用捉片。當內(nèi)容被 MPEG-DASH 客戶端回放時平痰,客戶端將根據(jù)當前網(wǎng)絡條件自動選擇下載和播放哪一個備選方案∥槿遥客戶端將選擇可及時下載的最高比特率片段進行播放宗雇,從而避免播放卡頓或重新緩沖事件。也因如此莹规,MPEG-DASH 客戶端可以無縫適應不斷變化的網(wǎng)絡條件并提供高質(zhì)量的播放體驗赔蒲,擁有更少的卡頓與重新緩沖發(fā)生率。
MPEG-DASH 是首個基于 HTTP 的自適應比特率流解決方案,它也是一項國際標準嘹履。MPEG-DASH 不應該與傳輸協(xié)議混淆 —— MPEG-DASH 使用 TCP 傳輸協(xié)議腻扇。「不同于 HLS、HDS 和 Smooth Streaming砾嫉,DASH 不關(guān)心編解碼器幼苛,因此它可以接受任何編碼格式編碼的內(nèi)容,如 H.265焕刮、H.264舶沿、VP9 等∨洳ⅲ」
雖然 HTML5 不直接支持 MPEG-DASH括荡,但是已有一些 MPEG-DASH 的 JavaScript 實現(xiàn)允許在網(wǎng)頁瀏覽器中通過 HTML5 Media Source Extensions(MSE)使用 MPEG-DASH。另有其他 JavaScript 實現(xiàn)溉旋,如 bitdash 播放器支持使用 HTML5 加密媒體擴展播放有 DRM 的MPEG-DASH畸冲。當與 WebGL 結(jié)合使用,MPEG-DASH 基于 HTML5 的自適應比特率流還可實現(xiàn) 360° 視頻的實時和按需的高效流式傳輸观腊。
4.2 DASH 重要概念
- MPD:媒體文件的描述文件(manifest)邑闲,作用類似 HLS 的 m3u8 文件。
- Representation:對應一個可選擇的輸出(alternative)梧油。如 480p 視頻苫耸,720p 視頻,44100 采樣音頻等都使用 Representation 描述褪子。
- Segment(分片):每個 Representation 會劃分為多個 Segment。Segment 分為 4 類骗村,其中嫌褪,最重要的是:Initialization Segment(每個 Representation 都包含 1 個 Init Segment),Media Segment(每個 Representation 的媒體內(nèi)容包含若干 Media Segment)叙身。
在國內(nèi) Bilibili 于 2018 年開始使用 DASH 技術(shù)渔扎,至于為什么選擇 DASH 技術(shù)。感興趣的小伙伴可以閱讀 我們?yōu)槭裁词褂肈ASH 這篇文章信轿。
講了那么多晃痴,相信有些小伙伴會好奇 MPD 文件長什么樣?這里我們來看一下西瓜視頻播放器 DASH 示例中的 MPD 文件:
<?xml version="1.0"?>
<!-- MPD file Generated with GPAC version 0.7.2-DEV-rev559-g61a50f45-master at 2018-06-11T11:40:23.972Z-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H1M30.080S" maxSegmentDuration="PT0H0M1.000S" profiles="urn:mpeg:dash:profile:full:2011">
<ProgramInformation moreInformationURL="http://gpac.io">
<Title>xgplayer-demo_dash.mpd generated by GPAC</Title>
</ProgramInformation>
<Period duration="PT0H1M30.080S">
<AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720" maxFrameRate="25" par="16:9" lang="eng">
<ContentComponent id="1" contentType="audio" />
<ContentComponent id="2" contentType="video" />
<Representation id="1" mimeType="video/mp4" codecs="mp4a.40.2,avc3.4D4020" width="1280" height="720" frameRate="25" sar="1:1" startWithSAP="0" bandwidth="6046495">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>xgplayer-demo_dashinit.mp4</BaseURL>
<SegmentList timescale="1000" duration="1000">
<Initialization range="0-1256"/>
<SegmentURL mediaRange="1257-1006330" indexRange="1257-1300"/>
<SegmentURL mediaRange="1006331-1909476" indexRange="1006331-1006374"/>
...
<SegmentURL mediaRange="68082016-68083543" indexRange="68082016-68082059"/>
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>
在播放視頻時财忽,西瓜視頻播放器會根據(jù) MPD 文件倘核,自動請求對應的分片進行播放。
前面我們已經(jīng)提到了 Bilibili即彪,接下來不得不提其開源的一個著名的開源項目 —— flv.js紧唱,不過在介紹它之前我們需要來了解一下 FLV 流媒體格式活尊。
五、FLV
5.1 FLV 文件結(jié)構(gòu)
FLV 是 FLASH Video 的簡稱漏益,F(xiàn)LV 流媒體格式是隨著 Flash MX 的推出發(fā)展而來的視頻格式蛹锰。由于它形成的文件極小、加載速度極快绰疤,使得網(wǎng)絡觀看視頻文件成為可能铜犬,它的出現(xiàn)有效地解決了視頻文件導入 Flash 后,使導出的 SWF 文件體積龐大轻庆,不能在網(wǎng)絡上很好的使用等問題癣猾。
FLV 文件由 FLV Header 和 FLV Body 兩部分構(gòu)成,而 FLV Body 由一系列的 Tag 構(gòu)成:
5.1.1 FLV 頭文件
FLV 頭文件:(9 字節(jié))
1-3:前 3 個字節(jié)是文件格式標識(FLV 0x46 0x4C 0x56)余爆。
4-4:第 4 個字節(jié)是版本(0x01)纷宇。
5-5:第 5 個字節(jié)的前 5 個 bit 是保留的必須是 0。
第 5 個字節(jié)的第 6 個 bit 音頻類型標志(TypeFlagsAudio)蛾方。
第 5 個字節(jié)的第 7 個 bit 也是保留的必須是 0像捶。
第5個字節(jié)的第8個bit視頻類型標志(TypeFlagsVideo)。
6-9: 第 6-9 的四個字節(jié)還是保留的桩砰,其數(shù)據(jù)為 00000009作岖。
整個文件頭的長度,一般是 9(3+1+1+4)五芝。
5.1.2 tag 基本格式
tag 類型信息,固定長度為 15 字節(jié):
- 1-4:前一個 tag 長度(4字節(jié))辕万,第一個 tag 就是 0枢步。
- 5-5:tag 類型(1 字節(jié));0x8 音頻渐尿;0x9 視頻醉途;0x12 腳本數(shù)據(jù)。
- 6-8:tag 內(nèi)容大凶┤住(3 字節(jié))隘擎。
- 9-11:時間戳(3 字節(jié),毫秒)(第 1 個 tag 的時候總是為 0凉夯,如果是腳本 tag 就是 0)货葬。
- 12-12:時間戳擴展(1 字節(jié))讓時間戳變成 4 字節(jié)(以存儲更長時間的 flv 時間信息),本字節(jié)作為時間戳的最高位劲够。
在 flv 回放過程中震桶,播放順序是按照 tag 的時間戳順序播放。任何加入到文件中時間設(shè)置數(shù)據(jù)格式都將被忽略征绎。
- 13-15:streamID(3 字節(jié))總是 0蹲姐。
FLV 格式詳細的結(jié)構(gòu)圖如下圖所示:
在瀏覽器中 HTML5 的 <video>
是不支持直接播放 FLV 視頻格式,需要借助 flv.js 這個開源庫來實現(xiàn)播放 FLV 視頻格式的功能。
5.2 flv.js 簡介
flv.js 是用純 JavaScript 編寫的 HTML5 Flash Video(FLV)播放器柴墩,它底層依賴于 Media Source Extensions忙厌。在實際運行過程中,它會自動解析 FLV 格式文件并喂給原生 HTML5 Video 標簽播放音視頻數(shù)據(jù)江咳,使瀏覽器在不借助 Flash 的情況下播放 FLV 成為可能逢净。
5.2.1 flv.js 的特性
- 支持播放 H.264 + AAC / MP3 編碼的 FLV 文件;
- 支持播放多段分段視頻扎阶;
- 支持播放 HTTP FLV 低延遲實時流汹胃;
- 支持播放基于 WebSocket 傳輸?shù)?FLV 實時流;
- 兼容 Chrome东臀,F(xiàn)ireFox着饥,Safari 10,IE11 和 Edge惰赋;
- 極低的開銷宰掉,支持瀏覽器的硬件加速。
5.2.2 flv.js 的限制
- MP3 音頻編解碼器無法在 IE11/Edge 上運行赁濒;
- HTTP FLV 直播流不支持所有的瀏覽器轨奄。
5.2.3 flv.js 的使用
<script src="flv.min.js"></script>
<video id="videoElement"></video>
<script> if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: 'http://example.com/flv/video.flv'
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}</script>
5.3 flv.js 工作原理
flv.js 的工作原理是將 FLV 文件流轉(zhuǎn)換為 ISO BMFF(Fragmented MP4)片段,然后通過 Media Source Extensions API 將 mp4 段喂給 HTML5 <video>
元素拒炎。flv.js 的設(shè)計架構(gòu)圖如下圖所示:
有關(guān) flv.js 工作原理更詳細的介紹挪拟,感興趣的小伙們可以閱讀 花椒開源項目實時互動流媒體播放器 這篇文章。現(xiàn)在我們已經(jīng)介紹了 hls.js 和 flv.js 這兩個主流的流媒體解決方案击你,其實它們的成功離不開 Media Source Extensions 這個幕后英雄默默地支持玉组。
六、MSE
6.1 MSE API
媒體源擴展 API(Media Source Extensions) 提供了實現(xiàn)無插件且基于 Web 的流媒體的功能丁侄。使用 MSE惯雳,媒體串流能夠通過 JavaScript 創(chuàng)建,并且能通過使用 audio
和 video
元素進行播放鸿摇。
近幾年來石景,我們已經(jīng)可以在 Web 應用程序上無插件地播放視頻和音頻了。但是拙吉,現(xiàn)有架構(gòu)過于簡單潮孽,只能滿足一次播放整個曲目的需要,無法實現(xiàn)拆分/合并數(shù)個緩沖文件筷黔。早期的流媒體主要使用 Flash 進行服務恩商,以及通過 RTMP 協(xié)議進行視頻串流的 Flash 媒體服務器。
媒體源擴展(MSE)實現(xiàn)后必逆,情況就不一樣了怠堪。MSE 使我們可以把通常的單個媒體文件的 src
值替換成引用 MediaSource
對象(一個包含即將播放的媒體文件的準備狀態(tài)等信息的容器)揽乱,以及引用多個 SourceBuffer
對象(代表多個組成整個串流的不同媒體塊)的元素。
為了便于大家理解粟矿,我們來看一下基礎(chǔ)的 MSE 數(shù)據(jù)流:
MSE 讓我們能夠根據(jù)內(nèi)容獲取的大小和頻率凰棉,或是內(nèi)存占用詳情(例如什么時候緩存被回收),進行更加精準地控制陌粹。它是基于它可擴展的 API 建立自適應比特率流客戶端(例如 DASH 或 HLS 的客戶端)的基礎(chǔ)撒犀。
在現(xiàn)代瀏覽器中創(chuàng)造能兼容 MSE 的媒體非常費時費力,還要消耗大量計算機資源和能源掏秩。此外或舞,還須使用外部應用程序?qū)?nèi)容轉(zhuǎn)換成合適的格式。雖然瀏覽器支持兼容 MSE 的各種媒體容器蒙幻,但采用 H.264 視頻編碼映凳、AAC 音頻編碼和 MP4 容器的格式是非常常見的,所以 MSE 需要兼容這些主流的格式邮破。此外 MSE 還為開發(fā)者提供了一個 API诈豌,用于運行時檢測容器和編解碼是否受支持。
6.2 MediaSource 接口
MediaSource 是 Media Source Extensions API 表示媒體資源 HTMLMediaElement 對象的接口抒和。MediaSource 對象可以附著在 HTMLMediaElement 在客戶端進行播放矫渔。在介紹 MediaSource 接口前,我們先來看一下它的結(jié)構(gòu)圖:
要理解 MediaSource 的結(jié)構(gòu)圖摧莽,我們得先來介紹一下客戶端音視頻播放器播放一個視頻流的主要流程:
獲取流媒體 -> 解協(xié)議 -> 解封裝 -> 音混槐、視頻解碼 -> 音頻播放及視頻渲染(需處理音視頻同步)鹰霍。
由于采集的原始音視頻數(shù)據(jù)比較大格带,為了方便網(wǎng)絡傳輸肛宋,我們通常會使用編碼器质帅,如常見的 H.264 或 AAC 來壓縮原始媒體信號誓沸。最常見的媒體信號是視頻跷敬,音頻和字幕麻车。比如撕阎,日常生活中的電影受裹,就是由不同的媒體信號組成,除運動圖片外虏束,大多數(shù)電影還含有音頻和字幕棉饶。
常見的視頻編解碼器有:H.264,HEVC镇匀,VP9 和 AV1照藻。而音頻編解碼器有:AAC,MP3 或 Opus汗侵。每個媒體信號都有許多不同的編解碼器幸缕。下面我們以西瓜視頻播放器的 Demo 為例群发,來直觀感受一下音頻軌、視頻軌和字幕軌:
現(xiàn)在我們來開始介紹 MediaSource 接口的相關(guān)內(nèi)容发乔。
6.2.1 狀態(tài)
enum ReadyState {
"closed", // 指示當前源未附加到媒體元素熟妓。
"open", // 源已經(jīng)被媒體元素打開,數(shù)據(jù)即將被添加到SourceBuffer對象中
"ended" // 源仍附加到媒體元素栏尚,但endOfStream()已被調(diào)用起愈。
};
6.2.2 流終止異常
enum EndOfStreamError {
"network", // 終止播放并發(fā)出網(wǎng)絡錯誤信號。
"decode" // 終止播放并發(fā)出解碼錯誤信號译仗。
};
6.2.3 構(gòu)造器
[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);
};
6.2.4 屬性
-
MediaSource.sourceBuffers
—— 只讀:返回一個 SourceBufferList 對象抬虽,包含了這個 MediaSource 的SourceBuffer 的對象列表。 -
MediaSource.activeSourceBuffers
—— 只讀:返回一個 SourceBufferList 對象纵菌,包含了這個MediaSource.sourceBuffers 中的 SourceBuffer 子集的對象—即提供當前被選中的視頻軌(video track)阐污,啟用的音頻軌(audio tracks)以及顯示/隱藏的字幕軌(text tracks)的對象列表 -
MediaSource.readyState
—— 只讀:返回一個包含當前 MediaSource 狀態(tài)的集合,即使它當前沒有附著到一個 media 元素(closed)产艾,或者已附著并準備接收 SourceBuffer 對象(open)疤剑,亦或者已附著但這個流已被 MediaSource.endOfStream() 關(guān)閉。 -
MediaSource.duration
:獲取和設(shè)置當前正在推流媒體的持續(xù)時間闷堡。 -
onsourceopen
:設(shè)置 sourceopen 事件對應的事件處理程序隘膘。 -
onsourceended
:設(shè)置 sourceended 事件對應的事件處理程序。 -
onsourceclose
:設(shè)置 sourceclose 事件對應的事件處理程序杠览。
6.2.5 方法
-
MediaSource.addSourceBuffer()
:創(chuàng)建一個帶有給定 MIME 類型的新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表弯菊。 -
MediaSource.removeSourceBuffer()
:刪除指定的 SourceBuffer 從這個 MediaSource 對象中的 SourceBuffers 列表。 -
MediaSource.endOfStream()
:表示流的結(jié)束踱阿。
6.2.6 靜態(tài)方法
-
MediaSource.isTypeSupported()
:返回一個 Boolean 值表明給定的 MIME 類型是否被當前的瀏覽器支持—— 這意味著是否可以成功的創(chuàng)建這個 MIME 類型的 SourceBuffer 對象管钳。
6.2.7 使用示例
var vidElement = document.querySelector('video');
if (window.MediaSource) { // (1)
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/mp4; codecs="avc1.42E01E, mp4a.40.2"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime); // (2)
var videoUrl = 'hello-mse.mp4';
fetch(videoUrl) // (3)
.then(function(response) {
return response.arrayBuffer();
})
.then(function(arrayBuffer) {
sourceBuffer.addEventListener('updateend', function(e) { (4)
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});
sourceBuffer.appendBuffer(arrayBuffer); // (5)
});
}
以上示例介紹了如何使用 MSE API,接下來我們來分析一下主要的工作流程:
- (1) 判斷當前平臺是否支持 Media Source Extensions API软舌,若支持的話才漆,則創(chuàng)建 MediaSource 對象,且綁定 sourceopen 事件處理函數(shù)佛点。
- (2) 創(chuàng)建一個帶有給定 MIME 類型的新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表醇滥。
- (3) 從遠程流服務器下載視頻流,并轉(zhuǎn)換成 ArrayBuffer 對象超营。
- (4) 為 sourceBuffer 對象添加 updateend 事件處理函數(shù)鸳玩,在視頻流傳輸完成后關(guān)閉流。
- (5) 往 sourceBuffer 對象中添加已轉(zhuǎn)換的 ArrayBuffer 格式的視頻流數(shù)據(jù)演闭。
七不跟、多媒體封裝格式
一般情況下,一個完整的視頻文件是由音頻和視頻兩部分組成的米碰。常見的 AVI窝革、RMVB购城、MKV、ASF聊闯、WMV工猜、MP4、3GP菱蔬、FLV 等文件只能算是一種封裝格式篷帅。H.264,HEVC拴泌,VP9 和 AV1 等就是視頻編碼格式魏身,MP3、AAC 和 Opus 等就是音頻編碼格式蚪腐。「比如:將一個 H.264 視頻編碼文件和一個 AAC 音頻編碼文件按 MP4 封裝標準封裝以后箭昵,就得到一個 MP4 后綴的視頻文件,也就是我們常見的 MP4 視頻文件了回季〖抑疲」
音視頻編碼的主要目的是壓縮原始數(shù)據(jù)的體積,而封裝格式(也稱為多媒體容器)泡一,比如 MP4颤殴,MKV,是用來存儲/傳輸編碼數(shù)據(jù)鼻忠,并按一定規(guī)則把音視頻涵但、字幕等數(shù)據(jù)組織起來,同時還會包含一些元信息帖蔓,比如當前流中包含哪些編碼類型矮瘟、時間戳等,播放器可以按照這些信息來匹配解碼器塑娇、同步音視頻澈侠。
為了能更好地理解多媒體封裝格式,我們再來回顧一下視頻播放器的原理埋酬。
7.1 視頻播放器原理
視頻播放器是指能播放以數(shù)字信號形式存儲的視頻的軟件哨啃,也指具有播放視頻功能的電子器件產(chǎn)品。大多數(shù)視頻播放器(除了少數(shù)波形文件外)攜帶解碼器以還原經(jīng)過壓縮的媒體文件奇瘦,視頻播放器還要內(nèi)置一整套轉(zhuǎn)換頻率以及緩沖的算法。大多數(shù)的視頻播放器還能支持播放音頻文件劲弦。
視頻播放基本處理流程大致包括以下幾個階段:
(1)解協(xié)議
從原始的流媒體協(xié)議數(shù)據(jù)中刪除信令數(shù)據(jù)耳标,只保留音視頻數(shù)據(jù),如采用 RTMP 協(xié)議傳輸?shù)臄?shù)據(jù)邑跪,經(jīng)過解協(xié)議后輸出 flv 格式的數(shù)據(jù)次坡。
(2)解封裝
分離音頻和視頻壓縮編碼數(shù)據(jù)呼猪,常見的封裝格式 MP4,MKV砸琅,RMVB宋距,F(xiàn)LV,AVI 這些格式症脂。從而將已經(jīng)壓縮編碼的視頻谚赎、音頻數(shù)據(jù)放到一起。例如 FLV 格式的數(shù)據(jù)經(jīng)過解封裝后輸出 H.264 編碼的視頻碼流和 AAC 編碼的音頻碼流诱篷。
(3)解碼
視頻壶唤,音頻壓縮編碼數(shù)據(jù),還原成非壓縮的視頻棕所,音頻原始數(shù)據(jù)闸盔,音頻的壓縮編碼標準包括 AAC,MP3琳省,AC-3 等迎吵,視頻壓縮編碼標準包含 H.264,MPEG2针贬,VC-1 等經(jīng)過解碼得到非壓縮的視頻顏色數(shù)據(jù)如 YUV420P击费,RGB 和非壓縮的音頻數(shù)據(jù)如 PCM 等。
(4)音視頻同步
將同步解碼出來的音頻和視頻數(shù)據(jù)分別送至系統(tǒng)聲卡和顯卡播放坚踩。
了解完視頻播放器的原理荡灾,下一步我們來介紹多媒體封裝格式。
7.2 多媒體封裝格式
對于數(shù)字媒體數(shù)據(jù)來說瞬铸,容器就是一個可以將多媒體數(shù)據(jù)混在一起存放的東西批幌,就像是一個包裝箱,它可以對音嗓节、視頻數(shù)據(jù)進行打包裝箱荧缘,將原來的兩塊獨立的媒體數(shù)據(jù)整合到一起,當然也可以單單只存放一種類型的媒體數(shù)據(jù)拦宣。
「有時候截粗,多媒體容器也稱封裝格式,它只是為編碼后的多媒體數(shù)據(jù)提供了一個 “外殼”鸵隧,也就是將所有的處理好的音頻绸罗、視頻或字幕都包裝到一個文件容器內(nèi)呈現(xiàn)給觀眾,這個包裝的過程就叫封裝豆瘫∩后埃」 常用的封裝格式有:MP4,MOV外驱,TS育灸,F(xiàn)LV腻窒,MKV 等。這里我們來介紹大家比較熟悉的 MP4 封裝格式磅崭。
7.2.1 MP4 封裝格式
MPEG-4 Part 14(MP4)是最常用的容器格式之一儿子,通常以 .mp4 文件結(jié)尾。它用于 HTTP(DASH)上的動態(tài)自適應流砸喻,也可以用于 Apple 的 HLS 流柔逼。MP4 基于 ISO 基本媒體文件格式(MPEG-4 Part 12),該格式基于 QuickTime 文件格式恩够。MPEG 代表動態(tài)圖像專家組卒落,是國際標準化組織(ISO)和國際電工委員會(IEC)的合作。MPEG 的成立是為了設(shè)置音頻和視頻壓縮與傳輸?shù)臉藴省?/p>
MP4 支持多種編解碼器蜂桶,常用的視頻編解碼器是 H.264 和 HEVC儡毕,而常用的音頻編解碼器是 AAC,AAC 是著名的 MP3 音頻編解碼器的后繼產(chǎn)品扑媚。
MP4 是由一些列的 box 組成腰湾,它的最小組成單元是 box。MP4 文件中的所有數(shù)據(jù)都裝在 box 中疆股,即 MP4 文件由若干個 box 組成费坊,每個 box 有類型和長度,可以將 box 理解為一個數(shù)據(jù)對象塊旬痹。box 中可以包含另一個 box附井,這種 box 稱為 container box。
一個 MP4 文件首先會有且僅有 一個 ftype
類型的 box两残,作為 MP4 格式的標志并包含關(guān)于文件的一些信息永毅,之后會有且只有一個 moov
類型的 box(movie box),它是一種 container box人弓,可以有多個沼死,也可以沒有,媒體數(shù)據(jù)的結(jié)構(gòu)由 metadata 進行描述崔赌。
相信有些讀者會有疑問 —— 實際的 MP4 文件結(jié)構(gòu)是怎么樣的意蛀?通過使用 mp4box.js 提供的在線服務,我們可以方便的查看本地或在線 MP4 文件內(nèi)部的結(jié)構(gòu):
?
mp4box.js 在線地址:https://gpac.github.io/mp4box.js/test/filereader.html
?
由于 MP4 文件結(jié)構(gòu)比較復雜(不信請看下圖)健芭,這里我們就不繼續(xù)展開县钥,有興趣的讀者,可以自行閱讀相關(guān)文章慈迈。
接下來若贮,我們來介紹 Fragmented MP4 容器格式。
7.2.2 Fragmented MP4 封裝格式
MP4 ISO Base Media 文件格式標準允許以 fragmented 方式組織 box,這也就意味著 MP4 文件可以組織成這樣的結(jié)構(gòu)兜看,由一系列的短的 metadata/data box 對組成,而不是一個長的 metadata/data 對狭瞎。Fragmented MP4 文件結(jié)構(gòu)如下圖所示细移,圖中只包含了兩個 fragments:
在 Fragmented MP4 文件中含有三個非常關(guān)鍵的 boxes:moov
、moof
和 mdat
熊锭。
- moov(movie metadata box):用于存放多媒體 file-level 的元信息弧轧。
- mdat(media data box):和普通 MP4 文件的
mdat
一樣,用于存放媒體數(shù)據(jù)碗殷,不同的是普通 MP4 文件只有一個mdat
box精绎,而 Fragmented MP4 文件中,每個 fragment 都會有一個mdat
類型的 box锌妻。 - moof(movie fragment box):用于存放 fragment-level 的元信息代乃。該類型的 box 在普通的 MP4 文件中是不存在的,而在 Fragmented MP4 文件中仿粹,每個 fragment 都會有一個
moof
類型的 box搁吓。
Fragmented MP4 文件中的 fragment 由 moof
和 mdat
兩部分組成,每個 fragment 可以包含一個音頻軌或視頻軌吭历,并且也會包含足夠的元信息堕仔,以保證這部分數(shù)據(jù)可以單獨解碼。Fragment 的結(jié)構(gòu)如下圖所示:
同樣晌区,利用 mp4box.js 提供的在線服務摩骨,我們也可以清晰的查看 Fragmented MP4 文件的內(nèi)部結(jié)構(gòu):
我們已經(jīng)介紹了 MP4 和 Fragmented MP4 這兩種容器格式,我們用一張圖來總結(jié)一下它們之間的主要區(qū)別:
八朗若、一些視頻相關(guān)的功能實現(xiàn)
8.1 如何實現(xiàn)視頻本地預覽
視頻本地預覽的功能主要利用 URL.createObjectURL()
方法來實現(xiàn)恼五。URL.createObjectURL() 靜態(tài)方法會創(chuàng)建一個 DOMString,其中包含一個表示參數(shù)中給出的對象的 URL捡偏。這個 URL 的生命周期和創(chuàng)建它的窗口中的 document 綁定唤冈。這個新的 URL 對象表示指定的 File 對象或 Blob 對象。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>視頻本地預覽示例</title>
</head>
<body>
<h3>阿寶哥:視頻本地預覽示例</h3>
<input type="file" accept="video/*" onchange="loadFile(event)" />
<video
id="previewContainer"
controls
width="480"
height="270"
style="display: none;"
></video>
<script> const loadFile = function (event) {
const reader = new FileReader();
reader.onload = function () {
const output = document.querySelector("#previewContainer");
output.style.display = "block";
output.src = URL.createObjectURL(new Blob([reader.result]));
};
reader.readAsArrayBuffer(event.target.files[0]);
}; </script>
</body>
</html>
8.2 如何實現(xiàn)播放器截圖
播放器截圖功能主要利用 CanvasRenderingContext2D.drawImage()
API 來實現(xiàn)银伟。Canvas 2D API 中的 CanvasRenderingContext2D.drawImage() 方法提供了多種方式在 Canvas 上繪制圖像你虹。
drawImage API 的語法如下:
?
void ctx.drawImage(image, dx, dy);
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
?
其中 image 參數(shù)表示繪制到上下文的元素。允許任何的 canvas 圖像源(CanvasImageSource)彤避,例如:CSSImageValue傅物,HTMLImageElement,SVGImageElement琉预,HTMLVideoElement董饰,HTMLCanvasElement,ImageBitmap 或者 OffscreenCanvas。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>播放器截圖示例</title>
</head>
<body>
<h3>阿寶哥:播放器截圖示例</h3>
<video id="video" controls="controls" width="460" height="270" crossorigin="anonymous">
<!-- 請?zhí)鎿Q為實際視頻地址 -->
<source src="https://xxx.com/vid_159411468092581" />
</video>
<button onclick="captureVideo()">截圖</button>
<script> let video = document.querySelector("#video");
let canvas = document.createElement("canvas");
let img = document.createElement("img");
img.crossOrigin = "";
let ctx = canvas.getContext("2d");
function captureVideo() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
img.src = canvas.toDataURL();
document.body.append(img);
} </script>
</body>
</html>
現(xiàn)在我們已經(jīng)知道如何獲取視頻的每一幀卒暂,其實在結(jié)合 gif.js 這個庫提供的 GIF 編碼功能啄栓,我們就可以快速地實現(xiàn)截取視頻幀生成 GIF 動畫的功能。這里阿寶哥不繼續(xù)展開介紹也祠,有興趣的小伙伴可以閱讀 ”使用 JS 直接截取 視頻片段 生成 gif 動畫“ 這篇文章昙楚。
8.3 如何實現(xiàn) Canvas 播放視頻
使用 Canvas 播放視頻主要是利用 ctx.drawImage(video, x, y, width, height)
來對視頻當前幀的圖像進行繪制,其中 video 參數(shù)就是頁面中的 video 對象诈嘿。所以如果我們按照特定的頻率不斷獲取 video 當前畫面堪旧,并渲染到 Canvas 畫布上,就可以實現(xiàn)使用 Canvas 播放視頻的功能奖亚。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>使用 Canvas 播放視頻</title>
</head>
<body>
<h3>阿寶哥:使用 Canvas 播放視頻</h3>
<video id="video" controls="controls" style="display: none;">
<!-- 請?zhí)鎿Q為實際視頻地址 -->
<source src="https://xxx.com/vid_159411468092581" />
</video>
<canvas
id="myCanvas"
width="460"
height="270"
style="border: 1px solid blue;"
></canvas>
<div>
<button id="playBtn">播放</button>
<button id="pauseBtn">暫停</button>
</div>
<script> const video = document.querySelector("#video");
const canvas = document.querySelector("#myCanvas");
const playBtn = document.querySelector("#playBtn");
const pauseBtn = document.querySelector("#pauseBtn");
const context = canvas.getContext("2d");
let timerId = null;
function draw() {
if (video.paused || video.ended) return;
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(video, 0, 0, canvas.width, canvas.height);
timerId = setTimeout(draw, 0);
}
playBtn.addEventListener("click", () => {
if (!video.paused) return;
video.play();
draw();
});
pauseBtn.addEventListener("click", () => {
if (video.paused) return;
video.pause();
clearTimeout(timerId);
}); </script>
</body>
</html>
8.4 如何實現(xiàn)色度鍵控(綠屏效果)
上一個示例我們介紹了使用 Canvas 播放視頻淳梦,那么可能有一些小伙伴會有疑問,為什么要通過 Canvas 繪制視頻昔字,Video 標簽不 “香” 么爆袍?這是因為 Canvas 提供了 getImageData
和 putImageData
方法使得開發(fā)者可以動態(tài)地更改每一幀圖像的顯示內(nèi)容。這樣的話作郭,我們就可以實時地操縱視頻數(shù)據(jù)來合成各種視覺特效到正在呈現(xiàn)的視頻畫面中螃宙。
比如 MDN 上的 ”使用 canvas 處理視頻“ 的教程中就演示了如何使用 JavaScript 代碼執(zhí)行色度鍵控(綠屏或藍屏效果)。
所謂的色度鍵控所坯,又稱色彩嵌空谆扎,是一種去背合成技術(shù)。Chroma 為純色之意芹助,Key 則是抽離顏色之意堂湖。把被拍攝的人物或物體放置于綠幕的前面,并進行去背后状土,將其替換成其他的背景无蜂。此技術(shù)在電影、電視劇及游戲制作中被大量使用蒙谓,色鍵也是虛擬攝影棚(Virtual studio)與視覺效果(Visual effects)當中的一個重要環(huán)節(jié)斥季。
下面我們來看一下關(guān)鍵代碼:
processor.computeFrame = function computeFrame() {
this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
let l = frame.data.length / 4;
for (let i = 0; i < l; i++) {
let r = frame.data[i * 4 + 0];
let g = frame.data[i * 4 + 1];
let b = frame.data[i * 4 + 2];
if (g > 100 && r > 100 && b < 43)
frame.data[i * 4 + 3] = 0;
}
this.ctx2.putImageData(frame, 0, 0);
return;
}
以上的 computeFrame()
方法負責獲取一幀數(shù)據(jù)并執(zhí)行色度鍵控效果。利用色度鍵控技術(shù)累驮,我們還可以實現(xiàn)純客戶端實時蒙版彈幕酣倾。