# MP4 & FLV# FLVFLV 是一种非常常见的音视频封装,尤其是在流媒体场景中经常用到。FLV 封装也是比较简单的封装格式,它是由一个个 Tag 组成的。Tag 又分为 视频 Tag 、音频 Tag 和 Script Tag ,分别用来存放视频数据、音频数据和 MetaData 数据。
FLV Header(9 字节) Signature - 前 3 个字节是文件的标识,固定是 FLV Version - 第 4 个字节表示版本 Flags - 第 5 个字节是一些标志位 第 6 位表示是否有音频 第 8 位表示是否有视频 其余为 0 HeaderSize - 后 4 个字节表示 Header 的长度,一般是 9 FLV Body 存放主要数据的地方,放置着一个个 Tag Tag PreviousTagSize - 前 4 个字节表示上一个 Tag 的长度,方便回倒 之后就是具体的 Tag 数据 Tag Header - 11 字节 Tag Data Script Tag Data 存放的是 MetaData 数据,主要包括宽、高、时长、采样率等基础信息 使用 2 个 AMF 包来存放信息 第一个 AMF 包是 onMetaData 包 第二个 AMF 包的第一个字节是数组类型,值是 0x08,紧接着 4 个字节为数组元素的个数,后面即为各数组元素的封装,数组元素为元素名称和值组成的对 Video Tag Data 第 1 个字节包含了这个 Tag 的视频帧类型和视频编码方式 对于 H264 数据,紧接着会有 4 字节的 AVC Packet Type 格式 接下来就是存放具体的视频数据 如果 AVC 包类型是 0,则数据格式如下 Audio Tag Data 第一个字节表示音频的编码方式、采样率和位宽等信息 之后是音频数据 # MP4MP4 封装相比 FLV 更常见,但是也更复杂一些。其实它们的基本的思想还是一样的,就是用一个规定的格式组织存放音视频数据和一些基础信息。跟 FLV 由一个个 Tag 组成有点类似,MP4 由一个个 box 组成,每一个 box 存放了不同的数据,而且 box 里面还可以嵌套着 box。
MP4 最外层的 box 主要有三个,分别是 File Type box(ftyp box)、Movie box(moov box)和 Media Data box(mdat box)。其中最重要、最复杂的就是 moov box 了,它里面存放了音视频的基本信息和每一个音视频数据的具体位置。
在 MP4 文件中,视频的一帧和音频的一段编码数据称为一个 sample。连续的几个 sample 称之为 chunk,而视频的所有 sample 称为一个视频 track,同样音频的所有 sample 也称为一个音频 track。一般 MP4 文件是由音频 track 和视频 track 组成,而 track 由 sample 组成,其中若干个 sample 组成一个 chunk。
每一个 box 都是由 Box Header 和 Box Data 组成。
根据 Box Header 中的 type 我们将 box 分为不同类型的 box,每一种不同的 box 对应的 Box Data 都是不一样。Box Data 里面又可以嵌套 box。MP4 的总体 box 分布图如下图所示
ftyp box File Type box,存放文件类型和版本信息,一般在文件的开始位置 Box Data 包含了 4 字节的主版本(major brand)、4 字节的版本号(minor version)和若干个 4 字节数组组成的兼容版本(compatible_brands) mdat box Media Data box,存放音视频的实际数据 由头部和数据两部分组成,box type 是 “mdat” 的 ASCII 码值 moov box Movie box,用来存放 Metadata 信息 moov box 里面会一层层嵌套很多层 box 总体嵌套逻辑就是 movie 里面是 track,track 里面是 sample,多个 sample 又组成了一个个 chunk # 音画同步# PTS & DTSPTS 表示的是视频帧的显示时间,DTS 表示的是视频帧的解码时间 。对于同一帧来说, DTS 和 PTS 可能是不一样的。
为什么呢?主要的原因是 B 帧,因为 B 帧可以双向参考,可以参考后面的 P 帧,那么就需要将后面用作参考的 P 帧先编码或解码,然后才能进行 B 帧的编码和解码。所以就会导致一个现象,后面显示的帧需要先编码或解码,这样就有解码时间和显示时间不同的问题了。如果说没有 B 帧的话,只有 I 帧和 P 帧就不会有 PTS 和 DTS 不同的问题了。
# 时间基时间基就是时间的单位 。比如说,编程的时经常使用 ms(毫秒)这个时间单位,毫秒是 1/1000 秒,如果你用毫秒表示时间的话,时间基就是 1/1000。再比如说 RTP 的时间戳,它的单位是 1/90000 秒,也就是说 RTP 时间戳的时间基是 1/90000, RTP 的时间戳每增加 1,就是指时间增加了 1/90000 秒。
对于 FLV 封装,时间基是 1/1000,即 FLV 里面的 DTS 和 PTS 的单位都是 ms。MP4 的话,时间基在 box 中的 time_scale,需要从 box 中读取解析出来的,不是固定的。
# 音视频同步的类型视频同步到音频(最常用的音视频同步方式 ) 指音频按照自己的节奏播放,不需要调节 如果视频相对音频快了的话,就延长当前播放视频帧的时间,以此来减慢视频帧的播放速度 如果视频相对音频慢了的话,就加快视频帧的播放速度,甚至通过丢帧的方式来快速赶上音频 音频同步到视频 视频按照自己的节奏播放,不需要调节 如果音频相对视频快了的话,就降低音频播放的速度,如重采样音频增加音频的采样点,延长音频的播放时间 如果音频相对视频慢了,就加快音频的播放速度,如重采样音频数据减少音频的采样点,缩短音频的播放时间 音频和视频都做调整同步 如 WebRTC 里面的音视频同步就是音频和视频都做调整,如果前一次调节的是视频的话,下一次就调节音频,相互交替进行,整体的思路还是跟前面两种方法差不多 这种一般在非 RTC 场景也不怎么使用 为什么一般都是视频同步到音频?
因为人耳的敏感度很高,相对于视频来说,音频的调整更容易被人耳发现。因此对音频做调节,要做好的话,难度要高于调节视频的速度,所以一般不太会使用这种同步方法。
# 基于纯浏览器视频会议方案# WebRTCWebRTC 的 Web App 似乎并不复杂,几乎所有的前后端模块都能找到成熟的开源实现。然而对于传统的视频会议提供商来说,支持 WebRTC 却并非这么简单。
存在的问题:
QoS(Quality of Service,服务质量) 对于成熟的视频会议企业来说,无论是 ARQ、FEC 还是 CC,都会有一套自己私有的网络适应性算法(QoS)框架, 但这些经过长时间验证且成熟的网络适应性算法却无法直接用在 Web App 后端的 SFU,因为浏览器端并不支持 更进一步来说,不同的浏览器,其自身的网络适应性兼容方案 也并不一致,再考虑到各种浏览器版本升级带来的进一步差异,在服务端做兼容各浏览器的网络适应性算法(QoS)其成本显然是相当高的 客户端 WebRTC 诞生之初想解决的仅仅是 P2P 的媒体传输而并非会议,它提供了基于 P2P 传输简单易用的一揽子方案,但却也因此没有提供更多底层媒体处理接口 开发者没有办法进一步处理任何的媒体流 ,进而无法在 Web App 中提供很多富有竞争力的功能,比如各种实时滤镜、虚拟背景、端对端加密等等 虽然 Chrome 开始提供 MediaStreamTrack 的 Insertable Stream 接口可以处理 WebRTC 中的媒体流,但要等到各主流浏览器都开始支持,还需要很长时间 TCP 443 端口兼容性 很多企业内部的网络防火墙仅对外开放 TCP 443 端口,且该端口仅允许 TLS 数据包通过,但所有浏览器 WebRTC 的 TURN/STUN 实现都不支持 TLS # 转移方案Web App 为了支持仅开放了 443 端口的企业,不得不实现一套新的失败转移方案。
不再将媒体的处理和传输部分托管给 WebRTC,取而代之的是一个基于 WebWorker 的媒体流水线处理机 (WebWorker based Media Processing Pipeline),用来对从本地设备采集到的设备进行编码,并打包成 RTP 包,再通过 WebSocket 信令通道发送给服务端。
完整的视频流发送过程:
通过 getUserMedia(getDisplayMedia)接口从设备获取视频流,再每间隔 33 毫秒(帧率为 30)执行以下流程
1.1 将此视频流当前帧通过 Canvas API 绘制到画布中 1.2 通过 Canvas API 读取画布中当前帧的 RGBA 图像 1.3 将 RGBA 图像数据转化为 YUV 格式 1.4 将 YUV 格式的图像数据发送到 OpenH264 模块进行编码 1.5 将编码后的数据包根据不同的 NALUnitType 封装为不同的 RTP Payload 1.6 将封装后的 RTP Payload 打包为 RTP 包 1.7 将 RTP 包进一步打成信令数据包 1.8 将此信令数据包通过信令通道发送到服务端
要注意的是,这里的第 3 ~ 6 步都是计算密集型的操作,为了避免阻塞 JS 主线程,需要将其用 WebWoker 包装,使这些步骤运行在非主线程中。同时,第 4 步视频编码操作需要借助 WebAsseamly 将第三方的 Video Codec(比如 OpenH264)编译为 Wasm 格式,导出为 JS 模块,方可被前端 App 引用。
视频流接收过程如下,每当从信令服务器接收到类型为 RTP 数据包的 PDU 时
2.1 将此 PDU 解析为 RTP 数据包 2.2 根据不同的 NALUnitType 将 RTP 数据包中的 RTP Payload 解析出来 2.3 将解析出来的 RTP Payload 发送到 OpenH264 模块进行解码,得到 YUV 格式的单帧图像 2.4 将单帧 YUV 图像转为 RGBA 格式的图像 2.5 通过 Canvas API 将 RGBA 格式的图像绘制到画布中
这里的第 3 步同样需要使用 WebWoker 包装,并引用基于 WebAsseamly 的 OpenH264 库,以避免 JS 主线程阻塞。 另一个需要注意的问题是,这里的第 4 步和第 5 步,需要使用 WebGL 进行性能优化,否则视频解码渲染的性能可能无法达到预期。
这个方案完全移除了对 WebRTC 一揽子解决方案的依赖,并且初步实现了自己的媒体处理流水线 。即可以对视频媒体流进行进一步的定制化处理,从而支持更多面向客户的功能。
因为有了对媒体处理的完全掌控力,则也可以在浏览器端实现自己的 QoS 方案,且这些方案都在 JS 层实现,可以兼容所有浏览器,从而避免服务端对不同浏览器 QoS 方案的支持。进一步说,将客户端的 QoS 实现策略通过 WebAsseamly 移植到 Web App 这边来,也是完全可行的方案。
但问题在于,作为失败转移方案,这里的媒体流数据是通过信令通道(WebSocket)进行传输的。在常规情况下,希望媒体数据能够走自己单独的数据通道,所以这里自然可以选择基于 SCTP(UDP based)传输协议的 DataChannel 作为媒体的传输通道。
# 优化方案在上面的失败转移方案的基础上,增加额外的基于 SCTP 传输协议的传输通道 DataChannel,以供媒体传输使用。此外,进一步丰富媒体处理 流水线,使其能够实现滤镜、虚拟背景等媒体处理需求。最后,再加上统一的可跨浏览器的 QoS 算法,最终的架构图如下:
增加了如上图中所示的橘色模块以后,系统开始变得丰满起来。媒体定制处理 加入到了流水线,能实现跨平台地满足更多的用户功能;完全自己实现的跨浏览器 QoS 方案 ,让前后端的算法变得统一起来,无需再考虑 QoS 的兼容性问题。
前端 DataChannel 和后端的 SCTP 的介入,让媒体流在绝大多数情况下,通过 DataChannel(SCTP)进行传输, 从而更好地利用 UDP 协议的优势,因为它的可靠性可以得到选择,从而弥补 TCP 按序重发机制在流媒体传输应用中的缺陷。而这里的 WebSocket,作为媒体数据传输通道,仅会发生在失败转移的特殊情况下。
# 进一步优化依然可以优化的空间:
基于 WebAsseamly 的 OpenH264 库的编码效率问题,尤其是在一些低端设备上,会导致 CPU 占用过高,并导致其发送或接收的视频发生卡顿 SCTP 的传输机制问题,虽然 SCTP 支持多流传输,能缓解队头阻塞的问题,但协议本身无法完全避免队头阻塞 SCTP 协议建立连接时需要 4-RTT,其握手策略也可以继续优化,从而进一步减少视频首帧的加载时间 Chrome 浏览器率先实现了基于 W3C 的 WebTransport 和 WebCodecs 规范 WebTransport 进一步优化了媒体传输通道,而 WebCodecs 规范则定义了浏览器所提供的更底层的媒体编解码接口 可以直接调用浏览器提供的本地视频编解码器,而不再需要调用 WebAssembly Based OpenH264 Codec 进行视频编解码,从而极大地提高了视频编解码效率