抖音相信大家都听说过,但是知道有 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] |
该音频数据的类型 2 为 MP3 7 为 G711 A-law 8 为 G711 mu-law 10 为 AAC |
|
音频采样率 |
UB[2] |
0 表示 5.5kHz 1 表示 11kHz 2 表示 22kHz 3 表示 44kHz(对于 AAC 编码将一直是 3 ) |
|
音频位深 |
UB[1] |
0 表示 8Bit 1 表示 16Bit |
|
音频声道 |
UB[1] |
0 表示单声道 1 表示立体声(对于 AAC 编码将总是 1 ) |
|
音频数据 |
DATA |
如果是 AAC 编码为 AACAUDIODATA ,否则音频数据根据音频编码不同而不同 |
对于常用的 AAC 编码的音频数据,FLV 规范还定义了 AACAUDIODATA 数据结构,如下表所示。
|
字段 |
类型 |
描述 |
|
AAC 包类型 |
UI8 |
描述接下来 AAC 数据的类型 0 为 AAC 配置 1 为 AAC 帧数据 |
|
AAC 数据 |
UI8[n] |
如果 AAC 包类型是 0 为 AudioSpecificConfig , 1 为 AAC 帧数据 |
FLV 视频标签
视频 FLV 标签数据字段结构如下表所示。
|
字段 |
类型 |
描述 |
|
帧类型 |
UB[4] |
1 表示 I 帧 2 表示非 I帧 |
|
编码 ID |
UB[4] |
视频编码 ID, 7 表示 AVC 编码 |
|
视频数据 |
DATA |
根据编码 ID 不同而不同, 7 为 AVCVIDEOPACKET |
编码 ID 一般为 7 表示 AVC 编码,官方规范是不支持 HEVC 编码的,但是现在 HEVC 编码越来越流行,所以社区一般把编码 ID 12 定义为 HEVC 编码。
AVCVIDEOPACKET 表示 AVC 视频数据结构,它的结构如下表所示。
|
字段 |
类型 |
描述 |
|
AVC 数据类型 |
UI8 |
0 表示视频配置 AVCDecoderConfigurationRecord 1 表示一个或多个 NAL 2 表示 AVC 序列结束 |
|
CTS |
SI24 |
有符号整数,毫秒,表示该帧 PTS 和 DTS 时间差 |
|
AVC 数据 |
UIB[n] |
AVC 数据类型为 0 表示 AVCDecoderConfigurationRecord 数据 1 表示一个或多个 NAL 数据 |
关于 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