來自公眾號(hào):全棧修仙之路 玩轉(zhuǎn)前端 Video 播放器
Web 開發(fā)者們一直以來想在 Web 中使用音頻和視頻,但早些時(shí)候展融,傳統(tǒng)的 Web 技術(shù)不能夠在 Web 中嵌入音頻和視頻没陡,所以一些像 Flash、Silverlight 的專利技術(shù)在處理這些內(nèi)容上變得很受歡迎。
這些技術(shù)能夠正常的工作,但是卻有著一系列的問題牲蜀,包括無法很好的支持 HTML/CSS 特性、安全問題绅这,以及可行性問題涣达。
幸運(yùn)的是,當(dāng) HTML5 標(biāo)準(zhǔn)公布后证薇,其中包含許多的新特性度苔,包括 <video>
和 <audio>
標(biāo)簽,以及一些 JavaScript APIs 用于對(duì)其進(jìn)行控制浑度。隨著通信技術(shù)和網(wǎng)絡(luò)技術(shù)的不斷發(fā)展寇窑,目前音視頻已經(jīng)成為大家生活中不可或缺的一部分。此外箩张,伴隨著 5G 技術(shù)的慢慢普及甩骏,實(shí)時(shí)音視頻領(lǐng)域還會(huì)有更大的想象空間。
接下來本文將從八個(gè)方面入手先慷,全方位帶你一起探索前端 Video 播放器和主流的流媒體技術(shù)横漏。閱讀完本文后,你將了解以下內(nèi)容:
- 為什么一些網(wǎng)頁中的 Video 元素熟掂,其視頻源地址是采用 Blob URL 的形式缎浇;
- 什么是 HTTP Range 請(qǐng)求及流媒體技術(shù)相關(guān)概念;
- 了解 HLS赴肚、DASH 的概念素跺、自適應(yīng)比特率流技術(shù)及流媒體加密技術(shù);
- 了解 FLV 文件結(jié)構(gòu)誉券、flv.js 的功能特性與使用限制及內(nèi)部的工作原理指厌;
- 了解 MSE(Media Source Extensions)API 及相關(guān)的使用;
- 了解視頻播放器的原理踊跟、多媒體封裝格式及 MP4 與 Fragmented MP4 封裝格式的區(qū)別踩验;
在最后的 「阿寶哥有話說」 環(huán)節(jié),阿寶哥將介紹如何實(shí)現(xiàn)播放器截圖商玫、如何基于截圖生成 GIF箕憾、如何使用 Canvas 播放視頻及如何實(shí)現(xiàn)色度鍵控等功能。
一拳昌、傳統(tǒng)的播放模式
大多數(shù) Web 開發(fā)者對(duì) <video>
都不會(huì)陌生袭异,在以下 HTML 片段中,我們聲明了一個(gè) <video>
元素并設(shè)置相關(guān)的屬性炬藤,然后通過 <source>
標(biāo)簽設(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標(biāo)簽</video>
上述代碼在瀏覽器渲染之后御铃,在頁面中會(huì)顯示一個(gè) Video 視頻播放器碴里,具體如下圖所示:
(圖片來源:https://h5player.bytedance.com/examples/)
通過 Chrome 開發(fā)者工具,我們可以知道當(dāng)播放 「xgplayer-demo-720p.mp4」 視頻文件時(shí)上真,發(fā)了 3 個(gè) HTTP 請(qǐng)求:
此外咬腋,從圖中可以清楚地看到,頭兩個(gè) HTTP 請(qǐng)求響應(yīng)的狀態(tài)碼是 「206」睡互。這里我們來分析第一個(gè) HTTP 請(qǐng)求的請(qǐng)求頭和響應(yīng)頭:
在上面的請(qǐng)求頭中帝火,有一個(gè) range: bytes=0-
首部信息,該信息用于檢測服務(wù)端是否支持 Range 請(qǐng)求湃缎。如果在響應(yīng)中存在 Accept-Ranges
首部(并且它的值不為 “none”)犀填,那么表示該服務(wù)器支持范圍請(qǐng)求。
在上面的響應(yīng)頭中嗓违, Accept-Ranges: bytes
表示界定范圍的單位是 bytes
九巡。這里 Content-Length
也是有效信息,因?yàn)樗峁┝艘螺d的視頻的完整大小蹂季。
1.1 從服務(wù)器端請(qǐng)求特定的范圍
假如服務(wù)器支持范圍請(qǐng)求的話冕广,你可以使用 Range 首部來生成該類請(qǐng)求。該首部指示服務(wù)器應(yīng)該返回文件的哪一或哪幾部分偿洁。
1.1.1 單一范圍
我們可以請(qǐng)求資源的某一部分撒汉。這里我們使用 Visual Studio Code 中的 REST Client 擴(kuò)展來進(jìn)行測試,在這個(gè)例子中涕滋,我們使用 Range 首部來請(qǐng)求 www.example.com 首頁的前 1024 個(gè)字節(jié)睬辐。
對(duì)于使用 REST Client 發(fā)起的 「單一范圍請(qǐng)求」,服務(wù)器端會(huì)返回狀態(tài)碼為 「206 Partial Content」 的響應(yīng)宾肺。而響應(yīng)頭中的 「Content-Length」 首部現(xiàn)在用來表示先前請(qǐng)求范圍的大兴荻(而不是整個(gè)文件的大小)锨用。「Content-Range」 響應(yīng)首部則表示這一部分內(nèi)容在整個(gè)資源中所處的位置丰刊。
1.1.2 多重范圍
Range 頭部也支持一次請(qǐng)求文檔的多個(gè)部分。請(qǐng)求范圍用一個(gè)逗號(hào)分隔開增拥。比如:
$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"
對(duì)于該請(qǐng)求會(huì)返回以下響應(yīng)信息:
因?yàn)槲覀兪钦?qǐng)求文檔的多個(gè)部分啄巧,所以每個(gè)部分都會(huì)擁有獨(dú)立的 「Content-Type」 和 「Content-Range」 信息,并且使用 boundary 參數(shù)對(duì)響應(yīng)體進(jìn)行劃分掌栅。
1.1.3 條件式范圍請(qǐng)求
當(dāng)重新開始請(qǐng)求更多資源片段的時(shí)候秩仆,必須確保自從上一個(gè)片段被接收之后該資源沒有進(jìn)行過修改。
「If-Range」 請(qǐng)求首部可以用來生成條件式范圍請(qǐng)求:假如條件滿足的話渣玲,條件請(qǐng)求就會(huì)生效逗概,服務(wù)器會(huì)返回狀態(tài)碼為 206 Partial 的響應(yīng),以及相應(yīng)的消息主體忘衍。假如條件未能得到滿足逾苫,那么就會(huì)返回狀態(tài)碼為 「200 OK」 的響應(yīng),同時(shí)返回整個(gè)資源枚钓。該首部可以與 「Last-Modified」 驗(yàn)證器或者 「ETag」 一起使用铅搓,但是二者不能同時(shí)使用。
1.1.4 范圍請(qǐng)求的響應(yīng)
與范圍請(qǐng)求相關(guān)的有三種狀態(tài):
- 在請(qǐng)求成功的情況下搀捷,服務(wù)器會(huì)返回 「206 Partial Content」 狀態(tài)碼星掰。
- 在請(qǐng)求的范圍越界的情況下(范圍值超過了資源的大小)嫩舟,服務(wù)器會(huì)返回 「416 Requested Range Not Satisfiable」 (請(qǐng)求的范圍無法滿足) 狀態(tài)碼氢烘。
- 在不支持范圍請(qǐng)求的情況下,服務(wù)器會(huì)返回 「200 OK」 狀態(tài)碼家厌。
剩余的兩個(gè)請(qǐng)求播玖,阿寶哥就不再詳細(xì)分析了。感興趣的小伙伴饭于,可以使用 Chrome 開發(fā)者工具查看一下具體的請(qǐng)求報(bào)文蜀踏。
通過第 3 個(gè)請(qǐng)求,我們可以知道整個(gè)視頻的大小大約為 7.9 MB掰吕。若播放的視頻文件太大或出現(xiàn)網(wǎng)絡(luò)不穩(wěn)定果覆,則會(huì)導(dǎo)致播放時(shí),需要等待較長的時(shí)間殖熟,這嚴(yán)重降低了用戶體驗(yàn)局待。
那么如何解決這個(gè)問題呢?要解決該問題我們可以使用流媒體技術(shù)菱属,接下來我們來介紹流媒體燎猛。
二、流媒體
流媒體是指將一連串的媒體數(shù)據(jù)壓縮后照皆,經(jīng)過網(wǎng)上分段發(fā)送數(shù)據(jù)重绷,在網(wǎng)上即時(shí)傳輸影音以供觀賞的一種技術(shù)與過程,此技術(shù)使得數(shù)據(jù)包得以像流水一樣發(fā)送膜毁;如果不使用此技術(shù)昭卓,就必須在使用前下載整個(gè)媒體文件。
流媒體實(shí)際指的是一種新的媒體傳送方式瘟滨,有聲音流候醒、視頻流、文本流杂瘸、圖像流倒淫、動(dòng)畫流等,而非一種新的媒體败玉。流媒體最主要的技術(shù)特征就是流式傳輸敌土,它使得數(shù)據(jù)可以像流水一樣傳輸镜硕。流式傳輸是指通過網(wǎng)絡(luò)傳送媒體技術(shù)的總稱。實(shí)現(xiàn)流式傳輸主要有兩種方式:順序流式傳輸(Progressive Streaming)和實(shí)時(shí)流式傳輸(Real Time Streaming)返干。
目前網(wǎng)絡(luò)上常見的流媒體協(xié)議:
通過上表可知兴枯,不同的協(xié)議有著不同的優(yōu)缺點(diǎn)。在實(shí)際使用過程中矩欠,我們通常會(huì)在平臺(tái)兼容的條件下選用最優(yōu)的流媒體傳輸協(xié)議财剖。比如,在瀏覽器里做直播癌淮,選用 HTTP-FLV 協(xié)議是不錯(cuò)的躺坟,性能優(yōu)于 RTMP+Flash,延遲可以做到和 RTMP+Flash 一樣甚至更好乳蓄。
而由于 HLS 延遲較大咪橙,一般只適合視頻點(diǎn)播的場景,但由于它在移動(dòng)端擁有較好的兼容性栓袖,所以在接受高延遲的條件下匣摘,也是可以應(yīng)用在直播場景。
講到這里相信有些小伙伴會(huì)好奇裹刮,對(duì)于 Video 元素來說使用流媒體技術(shù)之后與傳統(tǒng)的播放模式有什么直觀的區(qū)別音榜。下面阿寶哥以常見的 HLS 流媒體協(xié)議為例,來簡單對(duì)比一下它們之間的區(qū)別捧弃。
通過觀察上圖赠叼,我們可以很明顯地看到,當(dāng)使用 HLS 流媒體網(wǎng)絡(luò)傳輸協(xié)議時(shí)违霞,<video>
元素 src
屬性使用的是 blob://
協(xié)議嘴办。講到該協(xié)議,我們就不得不聊一下 Blob 與 Blob URL买鸽。
2.1 Blob
Blob(Binary Large Object)表示二進(jìn)制類型的大對(duì)象涧郊。在數(shù)據(jù)庫管理系統(tǒng)中,將二進(jìn)制數(shù)據(jù)存儲(chǔ)為一個(gè)單一個(gè)體的集合眼五。Blob 通常是影像妆艘、聲音或多媒體文件。「在 JavaScript 中 Blob 類型的對(duì)象表示不可變的類似文件對(duì)象的原始數(shù)據(jù)看幼∨」
Blob
由一個(gè)可選的字符串 type
(通常是 MIME 類型)和 blobParts
組成:
?
MIME(Multipurpose Internet Mail Extensions)多用途互聯(lián)網(wǎng)郵件擴(kuò)展類型,是設(shè)定某種擴(kuò)展名的文件用一種應(yīng)用程序來打開的方式類型诵姜,當(dāng)該擴(kuò)展名文件被訪問的時(shí)候汽煮,瀏覽器會(huì)自動(dòng)使用指定應(yīng)用程序來打開。多用于指定一些客戶端自定義的文件名,以及一些媒體文件打開方式暇赤。
常見的 MIME 類型有:超文本標(biāo)記語言文本 .html text/html心例、PNG圖像 .png image/png、普通文本 .txt text/plain 等翎卓。
?
為了更直觀的感受 Blob 對(duì)象契邀,我們先來使用 Blob 構(gòu)造函數(shù)摆寄,創(chuàng)建一個(gè) myBlob 對(duì)象失暴,具體如下圖所示:
如你所見,myBlob 對(duì)象含有兩個(gè)屬性:size 和 type微饥。其中 size
屬性用于表示數(shù)據(jù)的大卸喊恰(以字節(jié)為單位),type
是 MIME 類型的字符串欠橘。Blob 表示的不一定是 JavaScript 原生格式的數(shù)據(jù)矩肩。比如 File
接口基于 Blob
,繼承了 blob 的功能并將其擴(kuò)展使其支持用戶系統(tǒng)上的文件肃续。
2.2 Blob URL/Object URL
Blob URL/Object URL 是一種偽協(xié)議黍檩,允許 Blob 和 File 對(duì)象用作圖像,下載二進(jìn)制數(shù)據(jù)鏈接等的 URL 源始锚。在瀏覽器中刽酱,我們使用 URL.createObjectURL
方法來創(chuàng)建 Blob URL,該方法接收一個(gè) Blob
對(duì)象瞧捌,并為其創(chuàng)建一個(gè)唯一的 URL棵里,其形式為 blob:<origin>/<uuid>
,對(duì)應(yīng)的示例如下:
blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641
瀏覽器內(nèi)部為每個(gè)通過 URL.createObjectURL
生成的 URL 存儲(chǔ)了一個(gè) URL → Blob 映射姐呐。因此殿怜,此類 URL 較短,但可以訪問 Blob
曙砂。生成的 URL 僅在當(dāng)前文檔打開的狀態(tài)下才有效头谜。但如果你訪問的 Blob URL 不再存在,則會(huì)從瀏覽器中收到 404 錯(cuò)誤鸠澈。
上述的 Blob URL 看似很不錯(cuò)柱告,但實(shí)際上它也有副作用。雖然存儲(chǔ)了 URL → Blob 的映射款侵,但 Blob 本身仍駐留在內(nèi)存中末荐,瀏覽器無法釋放它。映射在文檔卸載時(shí)自動(dòng)清除新锈,因此 Blob 對(duì)象隨后被釋放甲脏。但是,如果應(yīng)用程序壽命很長,那不會(huì)很快發(fā)生块请。因此娜氏,如果我們創(chuàng)建一個(gè) Blob URL,即使不再需要該 Blob墩新,它也會(huì)存在內(nèi)存中贸弥。
針對(duì)這個(gè)問題,我們可以調(diào)用 URL.revokeObjectURL(url)
方法海渊,從內(nèi)部映射中刪除引用绵疲,從而允許刪除 Blob(如果沒有其他引用),并釋放內(nèi)存臣疑。
2.3 Blob vs ArrayBuffer
其實(shí)在前端除了 「Blob 對(duì)象」 之外盔憨,你還可能會(huì)遇到 「ArrayBuffer 對(duì)象」。它用于表示通用的讯沈,固定長度的原始二進(jìn)制數(shù)據(jù)緩沖區(qū)郁岩。你不能直接操縱 ArrayBuffer 的內(nèi)容,而是需要?jiǎng)?chuàng)建一個(gè) TypedArray 對(duì)象或 DataView 對(duì)象缺狠,該對(duì)象以特定格式表示緩沖區(qū)问慎,并使用該對(duì)象讀取和寫入緩沖區(qū)的內(nèi)容。
Blob 對(duì)象與 ArrayBuffer 對(duì)象擁有各自的特點(diǎn)挤茄,它們之間的區(qū)別如下:
除非你需要使用 ArrayBuffer 提供的寫入/編輯的能力如叼,否則 Blob 格式可能是最好的。
Blob 對(duì)象是不可變的驮樊,而 ArrayBuffer 是可以通過 TypedArrays 或 DataView 來操作薇正。
ArrayBuffer 是存在內(nèi)存中的,可以直接操作囚衔。而 Blob 可以位于磁盤挖腰、高速緩存內(nèi)存和其他不可用的位置。
雖然 Blob 可以直接作為參數(shù)傳遞給其他函數(shù)练湿,比如
window.URL.createObjectURL()
猴仑。但是,你可能仍需要 FileReader 之類的 File API 才能與 Blob 一起使用肥哎。Blob 與 ArrayBuffer 對(duì)象之間是可以相互轉(zhuǎn)化的:
使用 FileReader 的
readAsArrayBuffer()
方法辽俗,可以把 Blob 對(duì)象轉(zhuǎn)換為 ArrayBuffer 對(duì)象;使用 Blob 構(gòu)造函數(shù)篡诽,如
new Blob([new Uint8Array(data]);
崖飘,可以把 ArrayBuffer 對(duì)象轉(zhuǎn)換為 Blob 對(duì)象。
在前端 AJAX 場景下杈女,除了常見的 JSON 格式之外朱浴,我們也可能會(huì)用到 Blob 或 ArrayBuffer 對(duì)象:
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í)際需要獲取對(duì)應(yīng)類型的數(shù)據(jù)了翰蠢。介紹完上述內(nèi)容项乒,下面我們先來介紹目前應(yīng)用比較廣泛的 HLS 流媒體傳輸協(xié)議。
三梁沧、HLS
3.1 HLS 簡介
HTTP Live Streaming(縮寫是 HLS)是由蘋果公司提出基于 HTTP 的流媒體網(wǎng)絡(luò)傳輸協(xié)議檀何,它是蘋果公司 QuickTime X 和 iPhone 軟件系統(tǒng)的一部分。它的工作原理是把整個(gè)流分成一個(gè)個(gè)小的基于 HTTP 的文件來下載廷支,每次只下載一些频鉴。當(dāng)媒體流正在播放時(shí),客戶端可以選擇從許多不同的備用源中以不同的速率下載同樣的資源酥泞,允許流媒體會(huì)話適應(yīng)不同的數(shù)據(jù)速率砚殿。
此外啃憎,當(dāng)用戶的信號(hào)強(qiáng)度發(fā)生抖動(dòng)時(shí)芝囤,視頻流會(huì)動(dòng)態(tài)調(diào)整以提供出色的再現(xiàn)效果。
(圖片來源:https://www.wowza.com/blog/hls-streaming-protocol)
最初辛萍, 僅 iOS 支持 HLS悯姊。但現(xiàn)在 HLS 已成為專有格式,幾乎所有設(shè)備都支持它贩毕。顧名思義悯许,HLS(HTTP Live Streaming)協(xié)議通過標(biāo)準(zhǔn)的 HTTP Web 服務(wù)器傳送視頻內(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 支持各種廣告標(biāo)準(zhǔn)规辱,例如 VAST 和 VPAID谆棺。
為什么蘋果要提出 HLS 這個(gè)協(xié)議,其實(shí)它的主要是為了解決 RTMP 協(xié)議存在的一些問題罕袋。比如 RTMP 協(xié)議不使用標(biāo)準(zhǔn)的 HTTP 接口傳輸數(shù)據(jù)改淑,所以在一些特殊的網(wǎng)絡(luò)環(huán)境下可能被防火墻屏蔽掉。但是 HLS 由于使用的 HTTP 協(xié)議傳輸數(shù)據(jù)浴讯,通常情況下不會(huì)遇到被防火墻屏蔽的情況朵夏。除此之外,它也很容易通過 CDN(內(nèi)容分發(fā)網(wǎng)絡(luò))來傳輸媒體流榆纽。
3.2 HLS 自適應(yīng)比特流
HLS 是一種自適應(yīng)比特率流協(xié)議仰猖。因此询吴,HLS 流可以動(dòng)態(tài)地使視頻分辨率自適應(yīng)每個(gè)人的網(wǎng)絡(luò)狀況。如果你正在使用高速 WiFi亮元,則可以在手機(jī)上流式傳輸高清視頻猛计。但是,如果你在有限數(shù)據(jù)連接的公共汽車或地鐵上爆捞,則可以以較低的分辨率觀看相同的視頻奉瘤。
在開始一個(gè)流媒體會(huì)話時(shí),客戶端會(huì)下載一個(gè)包含元數(shù)據(jù)的 Extended M3U(m3u8)Playlist 文件煮甥,用于尋找可用的媒體流盗温。
(圖片來源:https://www.wowza.com/blog/hls-streaming-protocol)
為了便于大家的理解,我們使用 hls.js 這個(gè) JavaScript 實(shí)現(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 對(duì)應(yīng)的 m3u8 文件双霍,我們可以知道該視頻支持以下 5 種不同清晰度的視頻:
- 1920x1080(1080P)
- 1280x720(720P)
- 848x480(480P)
- 512x288
- 320x184
而不同清晰度視頻對(duì)應(yīng)的媒體播放列表砚偶,會(huì)定義在各自的 m3u8 文件中。這里我們以 720P 的視頻為例洒闸,來查看其對(duì)應(yīng)的 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
當(dāng)用戶選定某種清晰度的視頻之后染坯,將會(huì)下載該清晰度對(duì)應(yīng)的媒體播放列表(m3u8 文件),該列表中就會(huì)列出每個(gè)片段的信息丘逸。HLS 的傳輸/封裝格式是 MPEG-2 TS(MPEG-2 Transport Stream)单鹿,是一種傳輸和存儲(chǔ)包含視頻、音頻與通信協(xié)議各種數(shù)據(jù)的標(biāo)準(zhǔn)格式深纲,用于數(shù)字電視廣播系統(tǒng)仲锄,如 DVB、ATSC儒喊、IPTV 等等。
「需要注意的是利用一些現(xiàn)成的工具蛤奢,我們是可以把多個(gè) TS 文件合并為 mp4 格式的視頻文件。」 如果要做視頻版權(quán)保護(hù)培廓,那我們可以考慮使用對(duì)稱加密算法爷速,比如 AES-128 對(duì)切片進(jìn)行對(duì)稱加密颓遏。當(dāng)客戶端進(jìn)行播放時(shí)曼玩,先根據(jù) m3u8 文件中配置的密鑰服務(wù)器地址,獲取對(duì)稱加密的密鑰渤滞,然后再下載分片趴腋,當(dāng)分片下載完成后再使用匹配的對(duì)稱加密算法進(jìn)行解密播放。
對(duì)上述過程感興趣的小伙伴可以參考 Github 上 video-hls-encrypt 這個(gè)項(xiàng)目贯吓,該項(xiàng)目深入淺出介紹了基于 HLS 流媒體協(xié)議視頻加密的解決方案并提供了完整的示例代碼们陆。
(圖片來源:https://github.com/hauk0101/video-hls-encrypt)
介紹完蘋果公司推出的 HLS (HTTP Live Streaming)技術(shù),接下來我們來介紹另一種基于 HTTP 的動(dòng)態(tài)自適應(yīng)流 —— DASH。
四望伦、DASH
4.1 DASH 簡介
「基于 HTTP 的動(dòng)態(tài)自適應(yīng)流(英語:Dynamic Adaptive Streaming over HTTP林说,縮寫 DASH煎殷,也稱 MPEG-DASH)是一種自適應(yīng)比特率流技術(shù),使高質(zhì)量流媒體可以通過傳統(tǒng)的 HTTP 網(wǎng)絡(luò)服務(wù)器以互聯(lián)網(wǎng)傳遞腿箩『乐保」 類似蘋果公司的 HTTP Live Streaming(HLS)方案,MPEG-DASH 會(huì)將內(nèi)容分解成一系列小型的基于 HTTP 的文件片段珠移,每個(gè)片段包含很短長度的可播放內(nèi)容弓乙,而內(nèi)容總長度可能長達(dá)數(shù)小時(shí)。
內(nèi)容將被制成多種比特率的備選片段钧惧,以提供多種比特率的版本供選用暇韧。當(dāng)內(nèi)容被 MPEG-DASH 客戶端回放時(shí),客戶端將根據(jù)當(dāng)前網(wǎng)絡(luò)條件自動(dòng)選擇下載和播放哪一個(gè)備選方案浓瞪⌒覆#客戶端將選擇可及時(shí)下載的最高比特率片段進(jìn)行播放,從而避免播放卡頓或重新緩沖事件乾颁。也因如此涂乌,MPEG-DASH 客戶端可以無縫適應(yīng)不斷變化的網(wǎng)絡(luò)條件并提供高質(zhì)量的播放體驗(yàn)膘茎,擁有更少的卡頓與重新緩沖發(fā)生率物遇。
MPEG-DASH 是首個(gè)基于 HTTP 的自適應(yīng)比特率流解決方案驶俊,它也是一項(xiàng)國際標(biāo)準(zhǔn)裸燎。MPEG-DASH 不應(yīng)該與傳輸協(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 實(shí)現(xiàn)允許在網(wǎng)頁瀏覽器中通過 HTML5 Media Source Extensions(MSE)使用 MPEG-DASH般此。另有其他 JavaScript 實(shí)現(xiàn),如 bitdash 播放器支持使用 HTML5 加密媒體擴(kuò)展播放有 DRM 的MPEG-DASH牵现。當(dāng)與 WebGL 結(jié)合使用铐懊,MPEG-DASH 基于 HTML5 的自適應(yīng)比特率流還可實(shí)現(xiàn) 360° 視頻的實(shí)時(shí)和按需的高效流式傳輸。
4.2 DASH 重要概念
- MPD:媒體文件的描述文件(manifest)瞎疼,作用類似 HLS 的 m3u8 文件科乎。
- Representation:對(duì)應(yīng)一個(gè)可選擇的輸出(alternative)。如 480p 視頻贼急,720p 視頻茅茂,44100 采樣音頻等都使用 Representation 描述捏萍。
- Segment(分片):每個(gè) Representation 會(huì)劃分為多個(gè) Segment。Segment 分為 4 類空闲,其中令杈,最重要的是:Initialization Segment(每個(gè) Representation 都包含 1 個(gè) Init Segment),Media Segment(每個(gè) Representation 的媒體內(nèi)容包含若干 Media Segment)碴倾。
(圖片來源:https://blog.csdn.net/yue_huang/article/details/78466537)
在國內(nèi) Bilibili 于 2018 年開始使用 DASH 技術(shù)逗噩,至于為什么選擇 DASH 技術(shù)。感興趣的小伙伴可以閱讀 我們?yōu)槭裁词褂肈ASH 這篇文章跌榔。
講了那么多异雁,相信有些小伙伴會(huì)好奇 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>
(文件來源:https://h5player.bytedance.com/examples/)
在播放視頻時(shí)僧须,西瓜視頻播放器會(huì)根據(jù) MPD 文件纲刀,自動(dòng)請(qǐng)求對(duì)應(yīng)的分片進(jìn)行播放。
前面我們已經(jīng)提到了 Bilibili皆辽,接下來不得不提其開源的一個(gè)著名的開源項(xiàng)目 —— flv.js柑蛇,不過在介紹它之前我們需要來了解一下 FLV 流媒體格式。
五驱闷、FLV
5.1 FLV 文件結(jié)構(gòu)
FLV 是 FLASH Video 的簡稱耻台,F(xiàn)LV 流媒體格式是隨著 Flash MX 的推出發(fā)展而來的視頻格式。由于它形成的文件極小空另、加載速度極快盆耽,使得網(wǎng)絡(luò)觀看視頻文件成為可能,它的出現(xiàn)有效地解決了視頻文件導(dǎo)入 Flash 后扼菠,使導(dǎo)出的 SWF 文件體積龐大摄杂,不能在網(wǎng)絡(luò)上很好的使用等問題。
FLV 文件由 FLV Header 和 FLV Body 兩部分構(gòu)成循榆,而 FLV Body 由一系列的 Tag 構(gòu)成:
5.1.1 FLV 頭文件
FLV 頭文件:(9 字節(jié))
1-3:前 3 個(gè)字節(jié)是文件格式標(biāo)識(shí)(FLV 0x46 0x4C 0x56)析恢。
4-4:第 4 個(gè)字節(jié)是版本(0x01)。
5-5:第 5 個(gè)字節(jié)的前 5 個(gè) bit 是保留的必須是 0秧饮。
第 5 個(gè)字節(jié)的第 6 個(gè) bit 音頻類型標(biāo)志(TypeFlagsAudio)映挂。
第 5 個(gè)字節(jié)的第 7 個(gè) bit 也是保留的必須是 0。
第5個(gè)字節(jié)的第8個(gè)bit視頻類型標(biāo)志(TypeFlagsVideo)盗尸。
6-9: 第 6-9 的四個(gè)字節(jié)還是保留的柑船,其數(shù)據(jù)為 00000009。
整個(gè)文件頭的長度泼各,一般是 9(3+1+1+4)鞍时。
5.1.2 tag 基本格式
tag 類型信息,固定長度為 15 字節(jié):
- 1-4:前一個(gè) tag 長度(4字節(jié)),第一個(gè) tag 就是 0逆巍。
- 5-5:tag 類型(1 字節(jié))及塘;0x8 音頻;0x9 視頻蒸苇;0x12 腳本數(shù)據(jù)磷蛹。
- 6-8:tag 內(nèi)容大小(3 字節(jié))溪烤。
- 9-11:時(shí)間戳(3 字節(jié)味咳,毫秒)(第 1 個(gè) tag 的時(shí)候總是為 0,如果是腳本 tag 就是 0)檬嘀。
- 12-12:時(shí)間戳擴(kuò)展(1 字節(jié))讓時(shí)間戳變成 4 字節(jié)(以存儲(chǔ)更長時(shí)間的 flv 時(shí)間信息)槽驶,本字節(jié)作為時(shí)間戳的最高位。
在 flv 回放過程中鸳兽,播放順序是按照 tag 的時(shí)間戳順序播放掂铐。任何加入到文件中時(shí)間設(shè)置數(shù)據(jù)格式都將被忽略。
- 13-15:streamID(3 字節(jié))總是 0揍异。
FLV 格式詳細(xì)的結(jié)構(gòu)圖如下圖所示:
在瀏覽器中 HTML5 的 <video>
是不支持直接播放 FLV 視頻格式全陨,需要借助 flv.js 這個(gè)開源庫來實(shí)現(xiàn)播放 FLV 視頻格式的功能。
5.2 flv.js 簡介
flv.js 是用純 JavaScript 編寫的 HTML5 Flash Video(FLV)播放器衷掷,它底層依賴于 Media Source Extensions辱姨。在實(shí)際運(yùn)行過程中,它會(huì)自動(dòng)解析 FLV 格式文件并喂給原生 HTML5 Video 標(biāo)簽播放音視頻數(shù)據(jù)戚嗅,使瀏覽器在不借助 Flash 的情況下播放 FLV 成為可能雨涛。
5.2.1 flv.js 的特性
- 支持播放 H.264 + AAC / MP3 編碼的 FLV 文件;
- 支持播放多段分段視頻懦胞;
- 支持播放 HTTP FLV 低延遲實(shí)時(shí)流替久;
- 支持播放基于 WebSocket 傳輸?shù)?FLV 實(shí)時(shí)流;
- 兼容 Chrome躏尉,F(xiàn)ireFox蚯根,Safari 10,IE11 和 Edge胀糜;
- 極低的開銷稼锅,支持瀏覽器的硬件加速。
5.2.2 flv.js 的限制
- MP3 音頻編解碼器無法在 IE11/Edge 上運(yùn)行僚纷;
- 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è)計(jì)架構(gòu)圖如下圖所示:
(圖片來源:https://github.com/bilibili/flv.js/blob/master/docs/design.md)
有關(guān) flv.js 工作原理更詳細(xì)的介紹,感興趣的小伙們可以閱讀 花椒開源項(xiàng)目實(shí)時(shí)互動(dòng)流媒體播放器 這篇文章《赣現(xiàn)在我們已經(jīng)介紹了 hls.js 和 flv.js 這兩個(gè)主流的流媒體解決方案痊臭,其實(shí)它們的成功離不開 Media Source Extensions 這個(gè)幕后英雄默默地支持哮肚。因此,接下來阿寶哥將帶大家一起認(rèn)識(shí)一下 MSE(Media Source Extensions)广匙。
六允趟、MSE
6.1 MSE API
媒體源擴(kuò)展 API(Media Source Extensions) 提供了實(shí)現(xiàn)無插件且基于 Web 的流媒體的功能。使用 MSE鸦致,媒體串流能夠通過 JavaScript 創(chuàng)建潮剪,并且能通過使用 audio
和 video
元素進(jìn)行播放。
近幾年來分唾,我們已經(jīng)可以在 Web 應(yīng)用程序上無插件地播放視頻和音頻了抗碰。但是,現(xiàn)有架構(gòu)過于簡單绽乔,只能滿足一次播放整個(gè)曲目的需要弧蝇,無法實(shí)現(xiàn)拆分/合并數(shù)個(gè)緩沖文件。早期的流媒體主要使用 Flash 進(jìn)行服務(wù)折砸,以及通過 RTMP 協(xié)議進(jìn)行視頻串流的 Flash 媒體服務(wù)器看疗。
媒體源擴(kuò)展(MSE)實(shí)現(xiàn)后,情況就不一樣了睦授。MSE 使我們可以把通常的單個(gè)媒體文件的 src
值替換成引用 MediaSource
對(duì)象(一個(gè)包含即將播放的媒體文件的準(zhǔn)備狀態(tài)等信息的容器)两芳,以及引用多個(gè) SourceBuffer
對(duì)象(代表多個(gè)組成整個(gè)串流的不同媒體塊)的元素。
為了便于大家理解睹逃,我們來看一下基礎(chǔ)的 MSE 數(shù)據(jù)流:
MSE 讓我們能夠根據(jù)內(nèi)容獲取的大小和頻率盗扇,或是內(nèi)存占用詳情(例如什么時(shí)候緩存被回收),進(jìn)行更加精準(zhǔn)地控制沉填。它是基于它可擴(kuò)展的 API 建立自適應(yīng)比特率流客戶端(例如 DASH 或 HLS 的客戶端)的基礎(chǔ)疗隶。
在現(xiàn)代瀏覽器中創(chuàng)造能兼容 MSE 的媒體非常費(fèi)時(shí)費(fèi)力,還要消耗大量計(jì)算機(jī)資源和能源翼闹。此外斑鼻,還須使用外部應(yīng)用程序?qū)?nèi)容轉(zhuǎn)換成合適的格式。雖然瀏覽器支持兼容 MSE 的各種媒體容器猎荠,但采用 H.264 視頻編碼坚弱、AAC 音頻編碼和 MP4 容器的格式是非常常見的,所以 MSE 需要兼容這些主流的格式关摇。此外 MSE 還為開發(fā)者提供了一個(gè) API荒叶,用于運(yùn)行時(shí)檢測容器和編解碼是否受支持。
6.2 MediaSource 接口
MediaSource 是 Media Source Extensions API 表示媒體資源 HTMLMediaElement 對(duì)象的接口输虱。MediaSource 對(duì)象可以附著在 HTMLMediaElement 在客戶端進(jìn)行播放些楣。在介紹 MediaSource 接口前,我們先來看一下它的結(jié)構(gòu)圖:
(圖片來源 —— https://www.w3.org/TR/media-source/)
要理解 MediaSource 的結(jié)構(gòu)圖,我們得先來介紹一下客戶端音視頻播放器播放一個(gè)視頻流的主要流程:
獲取流媒體 -> 解協(xié)議 -> 解封裝 -> 音愁茁、視頻解碼 -> 音頻播放及視頻渲染(需處理音視頻同步)蚕钦。
由于采集的原始音視頻數(shù)據(jù)比較大,為了方便網(wǎng)絡(luò)傳輸鹅很,我們通常會(huì)使用編碼器嘶居,如常見的 H.264 或 AAC 來壓縮原始媒體信號(hào)。最常見的媒體信號(hào)是視頻促煮,音頻和字幕邮屁。比如,日常生活中的電影污茵,就是由不同的媒體信號(hào)組成樱报,除運(yùn)動(dòng)圖片外,大多數(shù)電影還含有音頻和字幕泞当。
常見的視頻編解碼器有:H.264迹蛤,HEVC,VP9 和 AV1襟士。而音頻編解碼器有:AAC盗飒,MP3 或 Opus。每個(gè)媒體信號(hào)都有許多不同的編解碼器陋桂。下面我們以西瓜視頻播放器的 Demo 為例逆趣,來直觀感受一下音頻軌、視頻軌和字幕軌:
現(xiàn)在我們來開始介紹 MediaSource 接口的相關(guān)內(nèi)容嗜历。
6.2.1 狀態(tài)
enum ReadyState { "closed", // 指示當(dāng)前源未附加到媒體元素宣渗。 "open", // 源已經(jīng)被媒體元素打開,數(shù)據(jù)即將被添加到SourceBuffer對(duì)象中 "ended" // 源仍附加到媒體元素梨州,但endOfStream()已被調(diào)用痕囱。};
6.2.2 流終止異常
enum EndOfStreamError { "network", // 終止播放并發(fā)出網(wǎng)絡(luò)錯(cuò)誤信號(hào)。 "decode" // 終止播放并發(fā)出解碼錯(cuò)誤信號(hào)暴匠。};
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
—— 只讀:返回一個(gè) SourceBufferList 對(duì)象鞍恢,包含了這個(gè) MediaSource 的SourceBuffer 的對(duì)象列表。 -
MediaSource.activeSourceBuffers
—— 只讀:返回一個(gè) SourceBufferList 對(duì)象每窖,包含了這個(gè)MediaSource.sourceBuffers 中的 SourceBuffer 子集的對(duì)象—即提供當(dāng)前被選中的視頻軌(video track)帮掉,啟用的音頻軌(audio tracks)以及顯示/隱藏的字幕軌(text tracks)的對(duì)象列表 -
MediaSource.readyState
—— 只讀:返回一個(gè)包含當(dāng)前 MediaSource 狀態(tài)的集合,即使它當(dāng)前沒有附著到一個(gè) media 元素(closed)窒典,或者已附著并準(zhǔn)備接收 SourceBuffer 對(duì)象(open)蟆炊,亦或者已附著但這個(gè)流已被 MediaSource.endOfStream() 關(guān)閉。 -
MediaSource.duration
:獲取和設(shè)置當(dāng)前正在推流媒體的持續(xù)時(shí)間瀑志。 -
onsourceopen
:設(shè)置 sourceopen 事件對(duì)應(yīng)的事件處理程序盅称。 -
onsourceended
:設(shè)置 sourceended 事件對(duì)應(yīng)的事件處理程序肩祥。 -
onsourceclose
:設(shè)置 sourceclose 事件對(duì)應(yīng)的事件處理程序。
6.2.5 方法
-
MediaSource.addSourceBuffer()
:創(chuàng)建一個(gè)帶有給定 MIME 類型的新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表缩膝。 -
MediaSource.removeSourceBuffer()
:刪除指定的 SourceBuffer 從這個(gè) MediaSource 對(duì)象中的 SourceBuffers 列表。 -
MediaSource.endOfStream()
:表示流的結(jié)束岸霹。
6.2.6 靜態(tài)方法
-
MediaSource.isTypeSupported()
:返回一個(gè) Boolean 值表明給定的 MIME 類型是否被當(dāng)前的瀏覽器支持—— 這意味著是否可以成功的創(chuàng)建這個(gè) MIME 類型的 SourceBuffer 對(duì)象疾层。
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) 判斷當(dāng)前平臺(tái)是否支持 Media Source Extensions API贡避,若支持的話痛黎,則創(chuàng)建 MediaSource 對(duì)象,且綁定 sourceopen 事件處理函數(shù)刮吧。
- (2) 創(chuàng)建一個(gè)帶有給定 MIME 類型的新的 SourceBuffer 并添加到 MediaSource 的 SourceBuffers 列表湖饱。
- (3) 從遠(yuǎn)程流服務(wù)器下載視頻流,并轉(zhuǎn)換成 ArrayBuffer 對(duì)象杀捻。
- (4) 為 sourceBuffer 對(duì)象添加 updateend 事件處理函數(shù)井厌,在視頻流傳輸完成后關(guān)閉流。
- (5) 往 sourceBuffer 對(duì)象中添加已轉(zhuǎn)換的 ArrayBuffer 格式的視頻流數(shù)據(jù)致讥。
上面阿寶哥只是簡單介紹了一下 MSE API仅仆,想深入了解它實(shí)際應(yīng)用的小伙伴,可以進(jìn)一步了解一下 「hls.js」 或 「flv.js」 項(xiàng)目垢袱。接下來阿寶哥將介紹音視頻基礎(chǔ)之多媒體容器格式墓拜。
七、多媒體封裝格式
一般情況下请契,一個(gè)完整的視頻文件是由音頻和視頻兩部分組成的咳榜。常見的 AVI、RMVB爽锥、MKV涌韩、ASF、WMV救恨、MP4贸辈、3GP、FLV 等文件只能算是一種封裝格式肠槽。H.264擎淤,HEVC,VP9 和 AV1 等就是視頻編碼格式秸仙,MP3嘴拢、AAC 和 Opus 等就是音頻編碼格式。「比如:將一個(gè) H.264 視頻編碼文件和一個(gè) AAC 音頻編碼文件按 MP4 封裝標(biāo)準(zhǔn)封裝以后寂纪,就得到一個(gè) MP4 后綴的視頻文件席吴,也就是我們常見的 MP4 視頻文件了赌结。」
音視頻編碼的主要目的是壓縮原始數(shù)據(jù)的體積孝冒,而封裝格式(也稱為多媒體容器)柬姚,比如 MP4,MKV庄涡,是用來存儲(chǔ)/傳輸編碼數(shù)據(jù)量承,并按一定規(guī)則把音視頻、字幕等數(shù)據(jù)組織起來穴店,同時(shí)還會(huì)包含一些元信息撕捍,比如當(dāng)前流中包含哪些編碼類型、時(shí)間戳等泣洞,播放器可以按照這些信息來匹配解碼器忧风、同步音視頻。
為了能更好地理解多媒體封裝格式球凰,我們再來回顧一下視頻播放器的原理狮腿。
7.1 視頻播放器原理
視頻播放器是指能播放以數(shù)字信號(hào)形式存儲(chǔ)的視頻的軟件,也指具有播放視頻功能的電子器件產(chǎn)品弟蚀。大多數(shù)視頻播放器(除了少數(shù)波形文件外)攜帶解碼器以還原經(jīng)過壓縮的媒體文件蚤霞,視頻播放器還要內(nèi)置一整套轉(zhuǎn)換頻率以及緩沖的算法。大多數(shù)的視頻播放器還能支持播放音頻文件义钉。
視頻播放基本處理流程大致包括以下幾個(gè)階段:
(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ù),音頻的壓縮編碼標(biāo)準(zhǔn)包括 AAC收毫,MP3攻走,AC-3 等殷勘,視頻壓縮編碼標(biāo)準(zhǔn)包含 H.264,MPEG2昔搂,VC-1 等經(jīng)過解碼得到非壓縮的視頻顏色數(shù)據(jù)如 YUV420P玲销,RGB 和非壓縮的音頻數(shù)據(jù)如 PCM 等。
(4)音視頻同步
將同步解碼出來的音頻和視頻數(shù)據(jù)分別送至系統(tǒng)聲卡和顯卡播放巩趁。
了解完視頻播放器的原理痒玩,下一步我們來介紹多媒體封裝格式。
7.2 多媒體封裝格式
對(duì)于數(shù)字媒體數(shù)據(jù)來說议慰,容器就是一個(gè)可以將多媒體數(shù)據(jù)混在一起存放的東西,就像是一個(gè)包裝箱奴曙,它可以對(duì)音别凹、視頻數(shù)據(jù)進(jìn)行打包裝箱,將原來的兩塊獨(dú)立的媒體數(shù)據(jù)整合到一起洽糟,當(dāng)然也可以單單只存放一種類型的媒體數(shù)據(jù)炉菲。
「有時(shí)候,多媒體容器也稱封裝格式坤溃,它只是為編碼后的多媒體數(shù)據(jù)提供了一個(gè) “外殼”拍霜,也就是將所有的處理好的音頻、視頻或字幕都包裝到一個(gè)文件容器內(nèi)呈現(xiàn)給觀眾薪介,這個(gè)包裝的過程就叫封裝祠饺。」 常用的封裝格式有:MP4汁政,MOV道偷,TS,F(xiàn)LV记劈,MKV 等勺鸦。這里我們來介紹大家比較熟悉的 MP4 封裝格式。
7.2.1 MP4 封裝格式
MPEG-4 Part 14(MP4)是最常用的容器格式之一目木,通常以 .mp4 文件結(jié)尾换途。它用于 HTTP(DASH)上的動(dòng)態(tài)自適應(yīng)流,也可以用于 Apple 的 HLS 流刽射。MP4 基于 ISO 基本媒體文件格式(MPEG-4 Part 12)军拟,該格式基于 QuickTime 文件格式。MPEG 代表動(dòng)態(tài)圖像專家組柄冲,是國際標(biāo)準(zhǔn)化組織(ISO)和國際電工委員會(huì)(IEC)的合作吻谋。MPEG 的成立是為了設(shè)置音頻和視頻壓縮與傳輸?shù)臉?biāo)準(zhǔn)。
MP4 支持多種編解碼器现横,常用的視頻編解碼器是 H.264 和 HEVC漓拾,而常用的音頻編解碼器是 AAC阁最,AAC 是著名的 MP3 音頻編解碼器的后繼產(chǎn)品。
MP4 是由一些列的 box 組成骇两,它的最小組成單元是 box速种。MP4 文件中的所有數(shù)據(jù)都裝在 box 中,即 MP4 文件由若干個(gè) box 組成低千,每個(gè) box 有類型和長度配阵,可以將 box 理解為一個(gè)數(shù)據(jù)對(duì)象塊。box 中可以包含另一個(gè) box示血,這種 box 稱為 container box棋傍。
一個(gè) MP4 文件首先會(huì)有且僅有 一個(gè) ftype
類型的 box,作為 MP4 格式的標(biāo)志并包含關(guān)于文件的一些信息难审,之后會(huì)有且只有一個(gè) moov
類型的 box(movie box)瘫拣,它是一種 container box,可以有多個(gè)告喊,也可以沒有麸拄,媒體數(shù)據(jù)的結(jié)構(gòu)由 metadata 進(jìn)行描述。
相信有些讀者會(huì)有疑問 —— 實(shí)際的 MP4 文件結(jié)構(gòu)是怎么樣的黔姜?通過使用 mp4box.js 提供的在線服務(wù)拢切,我們可以方便的查看本地或在線 MP4 文件內(nèi)部的結(jié)構(gòu):
?
mp4box.js 在線地址:https://gpac.github.io/mp4box.js/test/filereader.html
?
由于 MP4 文件結(jié)構(gòu)比較復(fù)雜(不信請(qǐng)看下圖),這里我們就不繼續(xù)展開秆吵,有興趣的讀者淮椰,可以自行閱讀相關(guān)文章。
接下來帮毁,我們來介紹 Fragmented MP4 容器格式实苞。
7.2.2 Fragmented MP4 封裝格式
MP4 ISO Base Media 文件格式標(biāo)準(zhǔn)允許以 fragmented 方式組織 box,這也就意味著 MP4 文件可以組織成這樣的結(jié)構(gòu)烈疚,由一系列的短的 metadata/data box 對(duì)組成黔牵,而不是一個(gè)長的 metadata/data 對(duì)。Fragmented MP4 文件結(jié)構(gòu)如下圖所示爷肝,圖中只包含了兩個(gè) fragments:
(圖片來源 —— https://alexzambelli.com/blog/2009/02/10/smooth-streaming-architecture/)
在 Fragmented MP4 文件中含有三個(gè)非常關(guān)鍵的 boxes:moov
猾浦、moof
和 mdat
。
- moov(movie metadata box):用于存放多媒體 file-level 的元信息灯抛。
- mdat(media data box):和普通 MP4 文件的
mdat
一樣金赦,用于存放媒體數(shù)據(jù),不同的是普通 MP4 文件只有一個(gè)mdat
box对嚼,而 Fragmented MP4 文件中夹抗,每個(gè) fragment 都會(huì)有一個(gè)mdat
類型的 box。 - moof(movie fragment box):用于存放 fragment-level 的元信息纵竖。該類型的 box 在普通的 MP4 文件中是不存在的漠烧,而在 Fragmented MP4 文件中杏愤,每個(gè) fragment 都會(huì)有一個(gè)
moof
類型的 box。
Fragmented MP4 文件中的 fragment 由 moof
和 mdat
兩部分組成已脓,每個(gè) fragment 可以包含一個(gè)音頻軌或視頻軌珊楼,并且也會(huì)包含足夠的元信息,以保證這部分?jǐn)?shù)據(jù)可以單獨(dú)解碼度液。Fragment 的結(jié)構(gòu)如下圖所示:
(圖片來源 —— https://alexzambelli.com/blog/2009/02/10/smooth-streaming-architecture/)
同樣厕宗,利用 mp4box.js 提供的在線服務(wù),我們也可以清晰的查看 Fragmented MP4 文件的內(nèi)部結(jié)構(gòu):
我們已經(jīng)介紹了 MP4 和 Fragmented MP4 這兩種容器格式堕担,我們用一張圖來總結(jié)一下它們之間的主要區(qū)別:
八已慢、阿寶哥有話說
8.1 如何實(shí)現(xiàn)視頻本地預(yù)覽
視頻本地預(yù)覽的功能主要利用 URL.createObjectURL()
方法來實(shí)現(xiàn)。URL.createObjectURL() 靜態(tài)方法會(huì)創(chuàng)建一個(gè) DOMString霹购,其中包含一個(gè)表示參數(shù)中給出的對(duì)象的 URL蛇受。這個(gè) URL 的生命周期和創(chuàng)建它的窗口中的 document 綁定。這個(gè)新的 URL 對(duì)象表示指定的 File 對(duì)象或 Blob 對(duì)象厕鹃。
<!DOCTYPE html><html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>視頻本地預(yù)覽示例</title> </head> <body> <h3>阿寶哥:視頻本地預(yù)覽示例</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 如何實(shí)現(xiàn)播放器截圖
播放器截圖功能主要利用 CanvasRenderingContext2D.drawImage()
API 來實(shí)現(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"> <!-- 請(qǐng)?zhí)鎿Q為實(shí)際視頻地址 --> <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)知道如何獲取視頻的每一幀宗收,其實(shí)在結(jié)合 gif.js 這個(gè)庫提供的 GIF 編碼功能漫拭,我們就可以快速地實(shí)現(xiàn)截取視頻幀生成 GIF 動(dòng)畫的功能。這里阿寶哥不繼續(xù)展開介紹混稽,有興趣的小伙伴可以閱讀 ”使用 JS 直接截取 視頻片段 生成 gif 動(dòng)畫“ 這篇文章采驻。
8.3 如何實(shí)現(xiàn) Canvas 播放視頻
使用 Canvas 播放視頻主要是利用 ctx.drawImage(video, x, y, width, height)
來對(duì)視頻當(dāng)前幀的圖像進(jìn)行繪制,其中 video 參數(shù)就是頁面中的 video 對(duì)象匈勋。所以如果我們按照特定的頻率不斷獲取 video 當(dāng)前畫面礼旅,并渲染到 Canvas 畫布上,就可以實(shí)現(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;"> <!-- 請(qǐng)?zhí)鎿Q為實(shí)際視頻地址 --> <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 如何實(shí)現(xiàn)色度鍵控(綠屏效果)
上一個(gè)示例我們介紹了使用 Canvas 播放視頻痘系,那么可能有一些小伙伴會(huì)有疑問,為什么要通過 Canvas 繪制視頻饿自,Video 標(biāo)簽不 “香” 么汰翠?這是因?yàn)?Canvas 提供了 getImageData
和 putImageData
方法使得開發(fā)者可以動(dòng)態(tài)地更改每一幀圖像的顯示內(nèi)容龄坪。這樣的話,我們就可以實(shí)時(shí)地操縱視頻數(shù)據(jù)來合成各種視覺特效到正在呈現(xiàn)的視頻畫面中奴璃。
比如 MDN 上的 ”使用 canvas 處理視頻“ 的教程中就演示了如何使用 JavaScript 代碼執(zhí)行色度鍵控(綠屏或藍(lán)屏效果)悉默。
所謂的色度鍵控,又稱色彩嵌空苟穆,是一種去背合成技術(shù)抄课。Chroma 為純色之意,Key 則是抽離顏色之意雳旅。把被拍攝的人物或物體放置于綠幕的前面跟磨,并進(jìn)行去背后,將其替換成其他的背景攒盈。此技術(shù)在電影抵拘、電視劇及游戲制作中被大量使用,色鍵也是虛擬攝影棚(Virtual studio)與視覺效果(Visual effects)當(dāng)中的一個(gè)重要環(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()
方法負(fù)責(zé)獲取一幀數(shù)據(jù)并執(zhí)行色度鍵控效果僵蛛。利用色度鍵控技術(shù),我們還可以實(shí)現(xiàn)純客戶端實(shí)時(shí)蒙版彈幕迎变。這里阿寶哥就不詳細(xì)介紹了充尉,感興趣的小伙伴可以閱讀一下創(chuàng)宇前端 ”彈幕不擋人!基于色鍵技術(shù)的純客戶端實(shí)時(shí)蒙版彈幕“ 這篇文章衣形。
九驼侠、參考資源
- Baike - 流媒體
- MDN - Video_and_audio_content
- MDN - Range_requests
- MDN - Media_Source_Extensions_API
- Wiki - MPEG-DASH
- w3.org - Media Source Extensions