抖音相信大家都聽說過,但是知道有 Web 版抖音 的人可能要少一些,和 TikTok 一樣抖音也有 Web 版本,可以讓我們在瀏覽器中就可以刷短視頻和觀看抖音直播。抖音是如何實現在瀏覽器中直播的呢?本篇文章來解析抖音直播的技術原理。
調試
首先點擊 https://live.douyin.com 進入抖音直播頁面。

然后隨便進入一個直播間并打開開發者工具,查看播放器相關 DOM 結構,如下圖所示。

首先可以發現原來抖音也是使用的 xgplayer。另外還可以發現 video 元素的 src 屬性是 blob: 開頭的視頻地址,和我們平時用 video 元素播放的視頻有點不一樣,要了解為什么視頻地址是 blob: 開頭的,就需要了解接下來介紹的 MSE API。
Media Source Extensions 介紹
Media Source Extensions API(MSE)媒體源擴展 API 提供了實現無插件且基于 Web 的流媒體的功能,不同于簡單的使用 video 元素,video 元素對于開發者來說完全是一個黑盒,瀏覽器自己去加載數據,加載完了自己解析,解碼再播放,這個過程中開發者無法進行任何操作。利用 MSE API 開發者可以自定義獲取流媒體數據并且還可以對數據做一些操作。
MSE 的兼容性如下圖所示。

可以發現 MSE 的兼容性還算可以,IE 11 都支持。但是號稱現代 IE 的 Safari 瀏覽器的 iphone 版,到現在都還不支持 MSE API,應該是蘋果想推廣自家的 HLS 協議吧,讓你在 iphone 設備上播放流媒體只能用他家的協議。
MSE API 主要有 MediaSource 和 SourceBuffer 兩個對象,MediaSource 表示是一個視頻源,它下有一個或多個 SourceBuffer,SourceBuffer 表示一個源數據,比如一個視頻分為視頻和音頻,我們可以創建兩個 SourceBuffer 一個用于播放視頻,一個播放音頻,MSE 架構圖如下所示。

通過上圖還可以發現 SourceBuffer 下面還細分了 TrackBuffer,因為你還可以不創建兩個 SourceBuffer,只用一個 SourceBuffer 來播放視頻和音頻,讓它內部自己分離音視頻,用不同的解碼器進行解碼播放。
使用 MSE 播放視頻的流程如下圖所示。
首先我們使用 fetch 或 XHR 去下載數據,然后做些處理過后,將數據交給 MediaSource,最后通過 video 元素進行播放,
如何將 MediaSource 和 video 元素連接呢?這就需要用到 URL.createObjectURL 它會創建一個 DOMString 表示指定的 File 對象或 Blob(二進制大對象) 對象。這個 URL 的生命周期和創建它的窗口中的 document 綁定。這就是為什么上面調試中的 video 元素的 src 是一個 blob 開頭的字符串。
下面來看看使用 MSE 播放視頻的最小代碼。
const video = document.querySelector('video')const mediaSource = new MediaSource()
mediaSource.addEventListener('sourceopen', ({ target }) => { URL.revokeObjectURL(video.src) const mime = 'video/webm; codecs="vorbis, vp8"'
const sourceBuffer = target.addSourceBuffer(mime) // target 就是 mediaSource
fetch('/static/media/flower.webm')
.then(response => response.arrayBuffer())
.then(arrayBuffer => {
sourceBuffer.addEventListener('updateend', () => { if (!sourceBuffer.updating && target.readyState === 'open') {
target.endOfStream()
video.play()
}
})
sourceBuffer.AppendBuffer(arrayBuffer)
})
})
video.src = URL.createObjectURL(mediaSource)
addSourceBuffer 方法會根據給定的 MIME 類型創建一個新的 SourceBuffer 對象,然后會將它追加到 MediaSource 的 SourceBuffers 列表中。
我們需要傳入相關具體的編解碼器(codecs)字符串,這里第一個是音頻(vorbis),第二個是視頻(vp8),兩個位置也可以互換,知道了具體的編解碼器瀏覽器就無需下載具體數據就知道當前類型是否支持,如果不支持該方法就會拋出 NotSupportedError 錯誤。更多關于媒體類型 MIME 編解碼器可以參考 RFC 4281。
這里還在一開始就調用了 revokeObjectURL。這并不會破壞任何對象,可以在 MediaSource 連接到 video 后隨時調用。它允許瀏覽器在適當的時候進行垃圾回收。
視頻并沒有直接推送到 MediaSource 中,而是 SourceBuffer,一個 MeidaSource 中有一個或多個 SourceBuffer。每個都與一種內容類型關聯,可能是視頻、音頻、視頻和音頻等。
HTTP-FLV 介紹
了解了 Web 環境是如何播放流媒體,現在來看看抖音直播是使用的什么流媒體協議吧。打開開發者工具的網絡面板,如下圖所示。

可以發現抖音直播使用的是 HTTP-FLV 協議,其實不看也知道抖音使用的是 HTTP-FLV,因為國內直播平臺全部都使用 HTTP-FLV!所以國內直播基礎建設對 HTTP-FLV 支持比較好。但是在國外 HTTP-FLV 幾乎沒有人用,國外用的最多的是 HLS 和 DASH 協議。
FLV(全稱 Flash Video)是一種流媒體格式,由 Adobe 公司開發,并在 2003 年發布。它的出現有效的解決了視頻文件在網絡上傳播放的問題,在當時它是實際意義的 Web 流媒體標準,非常多的流媒體平臺都使用它來播放視頻。
但是隨著技術的進步, html5 的 Video 元素,已經替換 Flash 視頻播放,目前 Flash 技術已經被棄用,各大流媒體平臺也切換到了 HLS 或 DASH 技術來實現 Web 流媒體播放。雖然 Flash 被棄用,在國外 FLV 也幾乎沒人使用,但是在國內它并沒有被棄用,反而被廣泛用于國內直播場景,所以了解 FLV 格式還是很有必要的。
要在 Web 環境拉取 flv 直播流,不能使用 XHR,需要使用 fetch API 去拉流,因為 HTTP-FLV 會用到 HTTP/1.1 的 chunked transfer encoding 功能流式去加載數據,是客戶端和服務器建立起一個 HTTP 連接后保持連接不斷開,服務器不斷發送直播流數據給客戶端,類似于 IM 中的長輪詢。
下面是使用 fetch 拉流的實例代碼。
fetch('./a.flv')
.then((res) => { const reader = res.body.getReader() const pump = async () => { const data = await reader.read(); if (!data.done) pump();
} pump()
})
可能大家還聽過 WS-FLV,這是使用 WebSocket 去拉 FLV 流,相比 HTTP-FLV 沒啥優勢,所以開始盡可能使用 HTTP-FLV。在我看來 WS-FLV 唯一的作用是兼容 IE 11 瀏覽器,因為 IE 11 是不支持 fetch 的,并且 IE 自帶的 MSStream 又有很多問題,這時候只有用 WebSocket 去拉流。
FLV 格式
接下來讓我們再更深入了解下 FLV 文件格式,FLV 格式的文件構成是比較簡單的,整個文件是由一個文件頭和一個文件體組成,文件體是由一個個標簽組成。
FLV 文件頭
FLV 文件由 9 個字節的文件頭開始,FLV 文件頭結構如下表所示。
字段 |
類型 |
描述 |
簽名 |
UI8 |
字節 0x46 表示字符 F |
簽名 |
UI8 |
字節 0x4C 表示字符 L |
簽名 |
UI8 |
字節 0x56 表示字符 V |
版本 |
UI8 |
該 FLV 文件版本 |
保留 |
UB[5] |
5 個比特的保留段,必須為 0 |
音頻標識 |
UB[1] |
1 比特,表示該文件是否存在音頻 |
保留 |
UB[1] |
1 比特的保留段,必須為 0 |
視頻標識 |
UB[1] |
1 比特,表示該文件是否存在視頻 |
數據偏移 |
UI32 |
表示文件體在整個文件的偏移,一般為 9,也就是文件頭的大小 |
FLV 文件體
FLV 文件頭之后就是文件體,文件體是由上一個 FLV 標簽大小和 FLV 標簽循環組成,如下表所示。
字段 |
類型 |
描述 |
前標簽大小 |
UI32 |
總是為 0,因為它之前沒有 FLV 標簽 |
FLV 標簽 |
FLVTAG |
第一個 FLV 標簽 |
前標簽大小 |
UI32 |
第一個 FLV 標簽大小 |
... |
... |
... |
最后一個 FLV 標簽 |
FLVTAG |
最后一個 FLV 標簽 |
前標簽大小 |
UI32 |
最后一個 FLV 標簽大小 |
需要注意的是,FLV 標簽大小是標簽它之前的 FLV 標簽大小,所以第一個標簽大小總是為 0。
一共有 3 種類型的 FLV 標簽,FLV 標簽如下表所示。
字段 |
類型 |
描述 |
標簽類型 |
UI8 |
8 表示音頻, 9 表示視頻, 18 表示腳本數據 |
數據大小 |
UI24 |
數據字段的大小 |
時間戳 |
UI24 |
該標簽數據表示的毫秒單位時間戳,如果是第一個標簽則為 0 |
高位時間戳 |
UI8 |
表示高位字節 |
流 ID |
UI24 |
總是為 0 |
數據字段 |
DATA |
該標簽中的數據 |
FLV 標簽中的數據字段的結構會因為標簽的類型不同而不同,音頻標簽數據字段為 AUDIODATA,視頻標簽為 VIDEODATA,腳本數據標簽為 SCRIPTDATAOBJECT。
FLV 音頻標簽
音頻 FLV 標簽數據字段結構如下表所示。
字段 |
類型 |
描述 |
音頻類型 |
UB[4] |
該音頻數據的類型 |
音頻采樣率 |
UB[2] |
0 表示 5.5kHz |
音頻位深 |
UB[1] |
0 表示 8Bit |
音頻聲道 |
UB[1] |
0 表示單聲道 |
音頻數據 |
DATA |
如果是 AAC 編碼為 AACAUDIODATA,否則音頻數據根據音頻編碼不同而不同 |
對于常用的 AAC 編碼的音頻數據,FLV 規范還定義了 AACAUDIODATA 數據結構,如下表所示。
字段 |
類型 |
描述 |
AAC 包類型 |
UI8 |
描述接下來 AAC 數據的類型 |
AAC 數據 |
UI8[n] |
如果 AAC 包類型是 0 為 AudIOSpecificConfig,1 為 AAC 幀數據 |
FLV 視頻標簽
視頻 FLV 標簽數據字段結構如下表所示。
字段 |
類型 |
描述 |
幀類型 |
UB[4] |
1 表示 I 幀 |
編碼 ID |
UB[4] |
視頻編碼 ID,7 表示 AVC 編碼 |
視頻數據 |
DATA |
根據編碼 ID 不同而不同,7 為 AVCVIDEOPACKET |
編碼 ID 一般為 7 表示 AVC 編碼,官方規范是不支持 HEVC 編碼的,但是現在 HEVC 編碼越來越流行,所以社區一般把編碼 ID 12 定義為 HEVC 編碼。
AVCVIDEOPACKET 表示 AVC 視頻數據結構,它的結構如下表所示。
字段 |
類型 |
描述 |
AVC 數據類型 |
UI8 |
0 表示視頻配置 |
CTS |
SI24 |
有符號整數,毫秒,表示該幀 PTS 和 DTS 時間差 |
AVC 數據 |
UIB[n] |
AVC 數據類型為 0 表示 |
關于
AVCDecoderConfigurationRecord 數據結構,請查看 ISO 14496-15 的第 5.2.4.1 章節。
FLV 數據標簽
FLV 視頻元數據存放在 FLV 數據標簽里面,它的結構如下表所示。
字段 |
類型 |
描述 |
對象 |
SCRIPTDATAOBJECT[] |
多個腳本數據對象 |
結束 |
UI24 |
總是為 9,表示結束 |
SCRIPTDATAOBJECT 描述的是一個對象,它由一個鍵值對組成,結構如下表所示。
字段 |
類型 |
描述 |
鍵 |
SCRIPTDATASTRING |
對象鍵 |
值 |
SCRIPTDATAVALUE |
對象值 |
鍵和值的數據結構如下表所示。
字段 |
類型 |
描述 |
類型 |
UI8 |
該鍵或值的類型是什么 |
數組長度 |
UI32 |
如果是數組類型,這里是數組長度 |
具體數據 |
TYPE |
具體的數據,根據類型不同而不同 |
數據終止符 |
TYPE |
如果類型是 3 或 8,表示對象和數組的終止 |
FLV 文件的元信息一般放在 onMetaData 字段中,解析完成 FLV 數據標簽后將返回下面這個對象。
interface FLVScriptData {
onMetaData?: {
duration?: number;
width?: number;
height?: number;
videodatarate?: number;
framerate?: number;
videocodecid?: number;
audiosamplerate?: number;
audiosamplesize?: number;
stereo?: boolean;
audiocodecid?: number;
filesize?: number;
}
}
onMetaData 對象的字段含義如下。
- duration 是視頻的總時長,單位是秒。
- width 是視頻的寬度,單位是像素。
- height 是視頻的高度,單位是像素。
- videodatarate 是視頻的碼率,單位是 kb 每秒。
- framerate 是視頻的幀率。
- videocodecid 是視頻的編碼 ID,同 FLV 視頻標簽中的編碼 ID。
- audiosamplerate 是音頻的采樣率。
- audiosamplesize 是音頻的位深。
- stereo 表示是否為立體聲。
- audiocodecid 是音頻的編碼 ID,同 FLV 音頻標簽中的編碼 ID。
- filesize 是文件的大小,單位是字節
FMP4 格式
MP4 格式相信大家都聽說過,MP4 或稱 MPEG-4 第 14 部分是一種標準的數字多媒體容器格式,它被定義在 ISO 14496-14 中,是由蘋果的 QuickTime 視頻格式演化而來(也就是我們常見的 .mov 視頻格式)。
FMP4 是 fragmented MP4 的縮寫,FMP4 更適合流媒體傳輸,它們的區別如下所示。

這是一個普通的 MP4 文件,可以看到它有一個很大的 mdat (實際電影數據)box,所有視頻元信息都存放在 moov 盒子,所有音視頻數據都存放在 mdat 盒子,所以 mp4 格式并不適合流媒體傳輸。

這是 fragmented MP4 的截圖,它是由 ISO BMFF 初始化分片(ftyp 后跟單個電影標題盒子 moov),加上一個個 moof 和 mdat 盒子組成的視頻分片組成,它的元信息和音視頻數據分散到一個個的 moof 和 mdat 盒子中,一次性只加載需要展示的部分,有點類似于前端的瀑布流分頁的數據加載。
因為 MP4 格式比 FLV 復雜的多,這里篇幅有限就不再詳細介紹了,感興趣的同學可以去看看 ISO 14496-12。
視頻格式
上面之所以介紹 FMP4 格式是因為 MSE API 并不是所有視頻格式都支持(比如上面介紹的 flv,或者普通的 mp4 格式就不會支持)根據瀏覽器的不同,可能支持的視頻格式也不同,但是 FMP4 格式所有的瀏覽器都支持,更多信息可以查看 ISO BMFF Byte Stream Format。
上面介紹的 FLV、MP4、FMP4、MOV 這些全都是視頻封裝格式,他們就像一個盒子來存放真正的音視頻流數據。
所以要在瀏覽器中播放 flv 直播流,還需要將 flv 視頻格式轉換成 fmp4 視頻格式。根據上面介紹的 flv 文件格式對 flv 進行解析,這個操作一般稱為解封裝(demux),解析出來音視頻等信息數據后,再封裝(remux)成 fmp4 視頻格式,最后交給 MSE API 來播放。

如上圖所示,我們需要將 FLV 格式轉換成 FMP4 格式,其中的音視頻流是不變的,這個操作也稱為轉封裝。
整體播放流程
那么在 Web 中播放 HTTP-FLV 直播流的整體流程如下所示。
- 首先使用 fetch 去拉 flv 直播流。
- 使用 HTTP/1.1 的 chunked transfer encoding 功能,流式下載視頻 chunk 片段。
- 使用 FlvDemuxer 流式解封裝 flv 視頻流。
- 對視頻流進行修復做音視頻同步。(一些音視頻流可能會有問題)
- 使用 FMP4Remuxer 將視頻流封裝成 FMP4 格式。
- 最后將封裝好的 FMP4 片段數據交給 MSE 播放。
上面 FlvDemuxer 和 FMP4Remuxer 的代碼需要自己根據 flv 和 fmp4 文件格式編寫,將 flv 中的每一幀的音頻、視頻和元信息都解出來,然后再將它們封裝成 fmp4 格式。
總結
本篇文章講解抖音直播的技術原理,它是使用 HTTP-FLV 來播放直播流,不光是抖音在使用 HTTP-FLV 直播方案,國內幾乎所有的直播平臺都在使用 HTTP-FLV 方案,所以看完這篇文章相當于了解了國內所有平臺的直播技術直播原理。不過各個平臺會在 HTTP-FLV 基礎上加點自己的東西,例如斗魚直播還使用了 P2P 技術來節省服務器流量。相比和其他平臺用一樣直播方案的抖音直播,抖音短視頻播放原理其實更有意思,下次將分享抖音短視頻技術原理。
作者:羽月
來源:微信公眾號:羽月技術
出處
:https://mp.weixin.qq.com/s/6qDBhjHk0ejzAg_kCkDEWw