Web端H.265播放器研发解密

发布于 2020-09-21  556 次阅读


原文地址 [fed.taobao.org](https://fed.taobao.org/blog/taofed/do71ct/web-player-h265/)

音视频编解码对于前端工程师是一个比较少涉足的领域,涉及到流媒体技术中的文本、图形、图像、音频和视频多种理论知识的学习,才能够应用到具体实践中,我们自研 web 播放器并支持 h.265 解码,在码率优化的大背景下(保持画质不变情况下,应用图像增强、roi 区域检测、智能场景分类和 h265 编解码等多种技术能力,将码流降低 50%。达到减少带宽成本,提升视频服务 QoE 的目的),真正做到了 h265 解码播放的全域覆盖。本文主要分享了我们基于 WebAssembly 实现 H.265 格式的解封装、解码和播放。

H.265 又称 HEVC(全称 High Efficiency Video Coding,高效率视频编码),是 ITU-T H.264/MPEG-4 AVC 标准的继任者。相比 H.264,H.265 拥有更高的压缩率,也就意味着同样码率(又称比特率是指每秒传送的比特 (bit) 数。单位为 bps(Bit Per Second),比特率越高,每秒传送数据就越多,画质就越清晰),H.265 的画质会更清晰,更高的压缩率就能使用更低的存储和传输成本。

  • 带宽成本:在有限带宽下 H.265 能传输更高质量的网络视频,理论上,H.265 最高只需 H.264 编码的一半带宽即可传输相同质量视频。更低的带宽可以更好的降低存储及传输成本,并为未来基于短视频及直播领域更多更复杂好玩的互动玩法做铺垫。

  • 转码成本:但是当前主流浏览器均不支持 H.265 原生视频播放,因此通常视频生产端需要针对浏览器做一次 H.264 视频的转码来适配浏览器端如 PC 场景的播放,而增加了转码成本。如在淘宝直播中,假设以每天 5 万场直播计算,每场直播转码成本 20 元,一天就是 100 万的转码成本。
    为此,我们团队对浏览器端 H.265 视频播放的可行性及兼容性进行了一次探索,为移动端及 PC 端全量 H.265 做准备,也对浏览器端视音频处理、WebAssembly 实践进行一次深入的尝试。

H.264 vs H.265

H.264 是当下用的最为广泛的视频编码格式,H.265 标准围绕着现有的视频编码标准 H.264,保留原来的某些技术,同时对一些相关的技术加以改进。新技术使用先进的技术用以改善码流、编码质量、延时和算法复杂度之间的关系,达到最优化设置。H.265 和 H.264 都是基于块的视频编码技术,主要的差别在于编码单元的大小以及一些编码算法细节,H.265 将图像划分为 “编码树单元 (coding tree Unit, CTU)”,而不是像 H.264 那样的 16×16 的宏块。根据不同的编码设置,编码树单元的尺寸可以被设置为 64×64 或有限的 32×32 或 16×16。一般来说区块尺寸越大,压缩效率就越好。具体的算法及相关细节这里不具体展开了,还有一些其他的压缩算法如因为 H.265 专利限制而生的开放编码格式如 AV1 等,读者可以参考其他相关文章。

如下图,可以看到同样主观画面质量,H.265(500K) 仅需 H.264(800K) 一半左右的带宽码率。

浏览器现状

如下图,因为 H.265 专利及硬解支持情况不完善的原因,主流现代浏览器均不兼容 H.265 编码的视频播放(Edge 新版本以插件方式支持),但是因为 Apple 对 H.265 的支持(这里作者认为这可能是一个很重要的标志,因为技术的发展很多时候不光是这个技术本身所决定的,而是很多因素共同作用的结果,商业也是其中很重要的一个因素),移动端 ios safari 在 11.0 版本以上支持原生播放。

想要在浏览器端播放 H.265 视频原生的 <video /> 标签没有办法支持,但是因为视频格式本身是连续图像画面音频的集合,参考了 chromium 的源码及 video 标签内部的实现原理,可以通过 <canvas /> + Web Audio API 的结合来模拟实现一个虚拟的 video 标签来实现播放器功能。

demo

因为直播流时效性的缘故,发布了一个播放 H.265 mp4 视频(该视频地址直接在浏览器中播放只有声音而没有画面)的在线 demo,读者可以有一个直观感受。
地址:https://g.alicdn.com/videox/mp4-h265/1.0.2/index.html
效果:

基于传统播放器的架构,我们设计的播放器架构如下:

视音频基础

因为前端领域对视频领域的涉及场景不多,一个 <video /> 标签就可以满足大部分场景,但是经历了这几年直播和短视频的爆发,视频的需求和功能也变得越来越复杂,开发之前阅读了很多视音频领域相关的书籍和文章,在此先对视音频基础进行一个简单的介绍。

视频中我们通常说的视频的格式,比如 .mp4, .mov, .wmv, .m3u8, .flv 等等被称为 container。在一个视频文件中音频、视频数据是分开存储的,使用的压缩算法也不一样。其中 container 作为容器主要包含了 video 数据、audio 数据、metadata(用于检索视音频 payload 格式等信息)。每个格式的封装格式不一样,比如 FLV 格式的基本单元是 Tag,而 MP4 格式的基本单元是 Box,辅助的 meta 信息用于检索找到对应的原始数据。

而平时听到的 H.264, H.265 等视频编码标准被称为 codec (COmpress and DECompress)。一个视频格式比如 mp4 可以使用任何标准化的压缩算法,这些信息都会被包含在一个视频文件的 meta 信息中来告诉播放器该用什么编解码算法来播放。

客户端播放器

一个传统的客户端播放器播放一个视频流经过了如下各个环节:

拉取数据 => 解封装 => 音视频解码 => 采样 / 像素数据发送到设备进行渲染。

对于流媒体,播放器客户端通过拉流以数据源(音视频流)为中心,进行管道式的传输。在此期间,对视频流的读取,转换,分类,复制等一系列操作处理,以封装的 mp4 流为例,需要对流进行解封装、解码、渲染等步骤:

浏览器 video 标签

在探究的过程中,为了了解主流浏览器不支持 H.265 视频播放的原因,以及浏览器端实现播放器原理的了解,通过对 Chromium 浏览器官方文档及 video 标签实现源码的阅读,整理了一个流程图。

可以看到浏览器内部对视频流播放的实现,在经过了 PipelineController 等数据传输管道的处理后利用 FFmpeg 软解或者 Gpu 硬解之后交给视频设备及音频设备进行同步及渲染。其中 H.265 的视频因为硬解支持情况不完善,软解可能有性能风险,所以在 chrome 中被关闭了不支持,在 chromium 中可以通过参数打开。我们就依照这个思路,利用浏览器提供的接口来实现一个模拟的 video 标签。

开发思路

开发思路按照从简单到复杂的过程,对任务进行拆分,来完成 H.265 视频点播及直播等各个场景的覆盖,以 mp4 短视频出发完成播放流程,再覆盖直播场景,考虑如网络抖动、内存控制等复杂因素,再针对直播 m3u8 等回放文件进行播放并开发视频 seek、倍速等功能。

mp4 播放 =>flv 播放 =>hls 播放 => 加入 seek、倍速等功能

可行性分析

  • 思路:在最开始进行可行性分析时,参考结合了已有工具 videoconverter.js 和 libde265.js 对 H.265 视频 ffmpeg 的编译提取了 hevc 文件及 mp3 音频文件在浏览器端进行了播放。

  • demo 地址:https://sparkmorry.github.io/mse-learning/h265/

  • 表现:将 720P 的 mp4 视频进行视频和音频的分离,通过 <canvas /> 绘制图像,通过 <audio /> 标签播放音频,画面在 Macbook Pro 上 Chrome 浏览器下在 23fps 左右。

  • 问题:

  • 不能达到解码性能标准: 720P 的视频在 Macbook Pro 上仅在 23fps 左右,而原视频是 25fps,不能达到解码性能标准,无法流畅播放。

  • 无法做到音画同步: 该方案因为直接提取了 hevc 裸流文件,无法获取视频和音频每帧的 pts 时间戳,无法做到严格的音画同步。

  • 解决方案:

  • 性能:因为 libde265.js 是 asm.js,通过对 libde265.js 开源库的改造,打包 WebAssembly 测试性能情况

  • 音画同步:参考 flv.js、hls.js 等开源视频库的方案,根据曾经的实践经历,js 在解封装方面的性能能够完成视频流文件解封装,获取每帧视频、音频播放的 pts 及原始数据交给解码器进行解码再渲染。

  • 方案调整:

MP4 点播流播放

  • 思路:根据上一过程调整的解决方案,通过 js 对 mp4 流进行解封装,因为音频解码的复杂度不高,也先用 js 进行解码,仅将视频解码模块用已有的三方模块 libde265 并替换为 wasm 解决性能问题,音视频解码模块都自身维护一段缓存区,负责存储解封装模块传过来的 packet 数据,解决音画同步的问题。

  • 表现:通过开源 libde265 实现的视频解码模块,针对于 720p 的视频流,平均解码时间是 45ms,不能满足每一帧音频播放时间间隔(40ms)。

  • 问题:视频解码性能仍然不够。

  • 解决方案:

  • 丢帧:保证了音频同步,丢掉部分非参考帧,但损失了部分体验。所以提升解码性能和改善播放策略才能有可能满足当前方案的可行性。提升解码性能和改善播放策略。

  • 提升解码性能:用解码性能更好的 ffmpeg 替换掉 libde265。

  • 改善播放流程:因为每个 requestAnimationFrame 循环任务都是同步的,边解码边播放。引入用 WebWorker 线程。通过改善视频解码模块,解码器内部开启循环解码,当外部的视频播放设备需要播放下一帧时,直接从解码器解码完的帧缓存中读取下一帧数据。实现了 worker 和主线程并行执行。

  • 方案调整:

  • demo 地址:https://static-assets.cyt-rain.cn/h265/index.html

  • 设计流程

FLV 直播流播放

  • 思路:mp4 视频流畅播放,但在直播场景(如 FLV 视频流)中,客户端需要和服务端建立长链接,不断接收流消息,借用 FFmpeg 本身对流媒体的支持,对视频数据进行解封装及解码。

  • 表现:无法编译 FFmpeg 网络库,TCP 无法建立连接。

  • 问题:
    无法编译 FFmpeg 网络库:TCP 建立连接创建 Socket 时报错,Emscripten 工具无法编译 TCP 连接相关配置
    codec 不支持:FLV 官方协议不支持 H.265。

  • 解决方案:

  • 无法编译 FFmpeg 网络库:主线程利用 fetch 方法进行拉流,放到 FFmpeg 自定义缓冲区进行解封装及解码。因为直播流长时间播放需要不停的开辟、释放内存空间,采用环形的数据缓冲区。

  • FLV 官方协议不支持 H.265:对 FFmpeg 及编码端对 H.265 进行扩展,因为 FFmpeg 内部数据结构嵌套较深,替换 js 解封装函数直接用 FFmpeg 的解封装函数。

  • 方案调整:

  • 设计流程

播放流程

  1. 因为 FFmpeg 支持多种格式解封装,只需要在在主线程中通过浏览器 API(通常是 fetch 方法)拉取原始流数据并放到缓存中,等初始缓存到一个阈值时开启 Worker 进行解封装及解码;

  2. 在子线程(Worker)中通过主线程 fetch 方法触发的数据回调接收数据存入环形缓冲区(内存环)中;

  3. 子线程将读取到的音频帧输送到主线程中,通过 Web Audio API 缓存音频数据,根据已解码的视频帧缓存队列循环解码保证缓存中一直缓存 10 帧 rgba 图像数据;

  4. 主线程中 canvas 根据音频播放回调的 pts 消费并渲染视频图像;

  5. 循环以上操作直到 fetch 接口返回流已结束。

解码器编译

通过 Emscripten 工具可以把 C 语言编写的 FFmpeg 库编译成 wasm 并在浏览器中应用到视音频解码中。
我们的视频解码场景和通常的播放器一样,通过依赖 FFmpeg 的通用接口来实现解封装和解码的工作。先通过 emscripten 编译 ffmpeg 库,再通过静态库的方式依赖到解封装和解码入口程序中。

性能测试

测试视频

因为 flv 直播视频受时效性影响较大,拿 720P 高清的 H.265 mp4 视频作为稳定输入测试

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'https://gw.alicdn.com/bao/uploaded/LB1l2iXISzqK1RjSZFjXXblCFXa.mp4?file=LB1l2iXISzqK1RjSZFjXXblCFXa.mp4':
  Metadata:
    major\_brand     : isom
    minor\_version   : 512
    compatible\_brands: isomiso2mp41
    encoder         : www.aliyun.com - Media Transcoding
  Duration: 00:01:00.10, start: 0.000000, bitrate: 907 kb/s
    Stream #0:0(und): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, bt709, progressive), 1280x720, 854 kb/s, 25 fps, 25 tbr, 12800 tbn, 25 tbc (default)
    Metadata:
      handler\_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 48 kb/s (default)
    Metadata:
      handler\_name    : SoundHandler

测试机器

  • lenovo ThinkPad T430

  • CPU: Intel(R) Core(TM) i5-3230M CPU@2.60GHz  x64 处理器

  • 内存: 8 GB

  • 系统: windows 10

  • MacBook Pro (Retina, 15-inch, Mid 2015)

  • CPU: 2.2 GHz Intel Core i7

  • 内存: 16 GB

  • 系统: macOS 10.14.2

性能情况

  • MBP 下表现
decoder.wasm 大小 decoder.js 大小 平均每帧解码时长 内存占用 cpu 占用
1.4M 168K 26.79ms 27M 17~25%
  • 针对两个 pc 笔记本进行了测试,平均每帧解码(包含 yuv420 转 rgba)时长在各个浏览器的表现情况如下:
    注:此处 Native(原生)表示针对 mac 系统原生编译的 FFmpeg 作为依赖的解码器(相对不考虑具体如 x86、arm 等计算机架构的 WebAssembly 的跨平台方案而言)
设备 Chrome Safari FireFox Edge Native
MacOS(i7) 26.79ms 22.19ms 24.77ms - 5.08ms
windows(i5) 33.51ms - 36.74ms 86.72ms 未测试

意味着最高能提供 720P 高清视频如下帧率视频流畅播放的能力:

设备 Chrome Safari FireFox Edge 视频基准 Native
MacOS(i7) 37fps 45fps 40fps - 25fps 196fps
windows(i5) 30fps - 27fps 12fps 25fps 未测试

可以看到这两台机器中,在非高速运动等普通的如电商场景 25fps 帧率的高清 720p 视频已经能达到生产环境的标准,但是距离原生的速度还有一定距离。

浏览器兼容性

主要用到了 WebAssembly 及 WebWorker 的支持,实际测试中主流浏览器 Chrome、Safari、Firefox、Edge 均能通过兼容性测试。

  • WebAssembly

  • WebWorker

当前的技术方案已经能在大部分机器的主流浏览器上流畅的播放 720P 的高清直播流,但是在 Edge 浏览器及性能稍差的机器上还是存在高清视频解码性能不能满足流畅播放的风险,针对 WebAssembly 达到 native 速度的目标还有一定距离,尤其是汇编并行计算的支持,在视音频及大规模数据处理中是很常见的性能优化策略,作者整理了几个优化的方向,在未来还有更多探索的空间:

  • 汇编
    FFmpeg 中解码有较多利用汇编进行并行计算的优化,但是汇编指令是 cpu specific 的(比如 x86 指令和 arm 指令),而 wasm 是跨平台的基于栈的虚拟机。Emscripten 不支持汇编的编译,考虑用 clang 等 llvm 前端将 FFmpeg 的. c 和汇编. asm 文件编译成 LLVM IR(LLVM Intermediate Representation),然后通过 fastcomp 或者其他后端来编译测试。

  • 硬解
    FFmpeg3.3 以上开始支持自动硬解探测,支持的硬件设备根据不同操作系统及硬件会有不同的支持,具体参考:https://trac.ffmpeg.org/wiki/HWAccelIntro 。因为 wasm 是跨平台的虚拟指令集,支持程度还要待进一步探究。

  • 多线程
    FFmpeg 内部解码有多线程来提高解码性能,通过 pthread 可以支持跨平台的多线程支持的,但是如果不支持共享内存,则线程之间的数据传输会有很多性能消耗(深拷贝或者 Transfered Object)。浏览器端共享内存通过 SharedArrayBuffer 来实现,因为有安全隐患,大部分主流浏览器关闭了 SharedArrayBuffer、Chrome67 + 开始恢复。考虑到兼容性多线程的支持还要再进行尝试。

  • WebGL 渲染
    解码平均时长中有 4ms 左右 (15%) 在 yuv 转 rgba 上,通过 WebGL 可以用 gpu 加速图像的计算,但是同时与 WebGL 的数据交换又会产生一定的性能损耗,需要再测试查看性能结果

通过 H.265 视频播放将开源视音频库 FFmpeg 的能力及 WebAssembly 性能的优势在浏览器端视音频处理上有了一次深入的尝试。视频作为一种多媒体形式,相比现有的文字、图像、音频都能有更生动及更丰富信息的表现。尤其经过了直播和短视频的爆发增长后,成为了一种基础的多媒体形式,也是网络及移动端手机性能等技术发展的体现。未来随着 5G 及更高性能的硬件设备的发展会被更广泛的应用到各个领域。浏览器在这场视频革命中也是不可或缺的一个环节,通过这次的探索,为未来浏览器端扩展视音频处理的通用能力提供了想象的空间,同时也在浏览器端通过 WebAssembly 向 native 性能及能力靠近的路上做了一个落地的尝试,虽然从测试情况看现在的表现还不如 native,但是随着标准及技术的演进,为未来对性能要求比较高的图形图像及人工智能等相关方向在浏览器端处理一定会渐渐被广泛的应用起来,比如如下几个方向:

  • 扩展浏览器端视频播放能力
    借助 FFmpeg 强大的编解码能力,除了 H.265 视频的播放,未来还可以在浏览器端兼容各种格式及编码类型的视频播放。如不同的编码格式 AV1、不同的容器格式 mov 格式等等。

  • 扩展浏览器端视音频处理能力
    借助 FFmpeg 及其他语言框架的现有能力,还可以在视音频领域做更多复杂的操作如视频滤镜、视频剪切、视频格式转换等功能,减少网络传输及存储的成本。

  • 基于 WebAssembly 的高性能 web 应用
    借助 WebAssembly 的跨平台优势,可以将传统的其他语言的开源框架如图形相关开源库 OpenGL、SDL 等的能力移植到浏览器上来。借助性能上的优势也可以将传统的图像、3D 等运算能力要求较高的应用扩展到浏览器端。

  • Chromium 媒体元素源码: https://github.com/chromium/chromium/tree/master/media

  • WebAssembly: https://webassembly.org/

  • 优秀的开源视音频处理框架 FFmpeg: https://www.ffmpeg.org/

  • 基于 LLVM 编译的 WebAssembly 打包工具集 Emscripten:https://emscripten.org/index.html

  • 基于 WebAssembly 的 ogg 播放器:https://github.com/brion/ogv.js

  • 基于 FFmpeg 的简单播放器:https://github.com/leixiaohua1020/simplest_ffmpeg_player

本文介绍了我们在 Web 端 H.265 播放器上研发的过程和进展,后续还有很多继续优化和深入的点。对相关知识感兴趣的同学欢迎沟通交流,附上我们前端团队的介绍:

淘宝技术部内容与开放平台前端团队是淘系核心的商业变革阵地,相对于横向资源前端团队,我们更深入在内容电商、音视频技术领域,探寻创新商业模式及业界领先技术。

本团队目前正火热招聘 20 届优秀毕业生,欢迎有志之士加入!
有意向者可直接联系团队负责人: 灵玉 lingyu.csh@taobao.com

题图出处:https://unsplash.com/photos/oTjFWTHDRZQ


或许明日太阳西下倦鸟已归时