sonysuqin / WasmVideoPlayer

Play file/stream with wasm & webgl & web audio api, using ffmpeg for multi codec support, especially for h265,support http, websocket, http-flv stream.
GNU General Public License v3.0
1.31k stars 376 forks source link

@[TOC](基于WASM的H265 Web播放器)

1 背景

目前这个时间点,原生支持H265(HEVC)播放的浏览器极少,可以说基本没有,主要原因一个是H265的解码有更高的性能要求,从而换取更高的压缩率,目前大多数机器CPU软解H265的超清视频还是有点吃力,硬解兼容性又不好,另外一个原因主要是H265的专利费问题。因此H265有被各大浏览器厂商放弃的趋势,转而去支持更加开放的AV1编码,但是AV1编码的商用和普及估计还有段时间。

H265与H264相比主要的好处在于相同分辨率下降低了几乎一倍的码率,对带宽压力比较大的网站来说,使用H265可以极大削减带宽消耗(尽管可能面临专利费麻烦),但是由于浏览器的支持问题,目前H265的播放主要在APP端实现,借助硬件解码,可以获得比较好的性能和体验。

本文相关的代码使用WASM、FFmpeg、WebGL、Web Audio等组件实现了一个简易的支持H265的Web播放器,作为探索、验证,just for fun。

2 代码

github地址: https://github.com/sonysuqin/WasmVideoPlayer.

3 依赖

3.1 WASM

WASM的介绍在这里,可以在浏览器里执行原生代码(例如C、C++),要开发可以在浏览器运行的原生代码,需要安装他的工具链,我使用的是目前最新的版本(1.39.5)。编译环境有Ubuntu、MacOS等,这里有介绍。

3.2 FFmpeg

主要使用FFmpeg来做解封装(demux)和解码(decoder),由于使用了FFmpeg(3.3),理论上可以播放绝大多数格式的视频,这里只针对H265编码、MP4封装,在编译时可以只按需编译最少的模块,从而得到比较小的库。

使用Emscripten编译FFmpeg主要参考下面这个网页,做了一些修改: https://blog.csdn.net/Jacob_job/article/details/79434207

3.3 WebGL

H5使用Canvas来绘图,但是默认的2d模式只能绘制RGB格式,使用FFmpeg解码出来的视频数据是YUV格式,想要渲染出来需要进行颜色空间转换,可以使用FFmpeg的libswscale模块进行转换,为了提升性能,这里使用了WebGL来硬件加速,主要参考了这个项目,做了一些修改: https://github.com/p4prasoon/YUV-Webgl-Video-Player

3.4 Web Audio

FFmpeg解码出来的音频数据是PCM格式,可以使用H5的Web Audio Api来播放,主要参考了这个项目,做了一些修改: https://github.com/samirkumardas/pcm-player

4 播放器实现

这里只是简单实现了播放器的部分功能,包括下载、解封装、解码、渲染、音视频同步等基本功能,每个环节还有很多细节可以优化。目前可以支持FFmpeg的各种内置codec,如H264/H265等,默认支持MP4/FLV文件播放、HTTP-FLV流的播放。

4.1 模块结构

在这里插入图片描述

4.2 线程模型

理论上来说,播放器应该使用这样的线程模型,各个模块在各自线程各司其职: 在这里插入图片描述 但是WASM目前对多线程(pthread)的支持不够好,各个浏览器的WASM多线程支持还处于试验阶段,因此现在最好不要在原生代码里编写pthread的代码。这里使用了Web Worker,把下载和对FFmpeg的调用放到单独的线程中去。

主要有三个线程:

4.3 Player

4.3.1 接口

如上面提到的,Player会进行速率控制,因此需要把文件分成chunk,按照chunk方式进行下载。下载的数据先发给Player,由Player转交给Decoder(理论上应该直接交给Decoder,但是Downloader无法直接与Decoder通信)。 对流式的数据,则使用Fetch

4.5 Decoder

这个模块需要加载原生代码生成的胶水代码(glue code),胶水代码会加载wasm。

self.importScripts("libffmpeg.js");

4.5.1 接口

这些方法都由Player模块通过postMessage异步调用。

4.5.2 缓存

这里简单使用了WASM的MEMFS文件接口(WASM的文件系统参考),使用方式就是直接调用stdio的方法,然后在emcc的编译命令中加入编译选项:

-s FORCE_FILESYSTEM=1 

MEMFS会在内存中虚拟一个文件系统,Decoder收到Player发过来的文件数据直接写入缓存,由解码任务读取缓存。 对流式的数据,则使用FFmpeg的环形缓存FIFO。

4.5.3 解码

Javascript与C的数据交互:

发送:
……
this.cacheBuffer = Module._malloc(chunkSize);
……
Decoder.prototype.sendData = function (data) {
    var typedArray = new Uint8Array(data);
    Module.HEAPU8.set(typedArray, this.cacheBuffer); //拷贝
    Module._sendData(this.cacheBuffer, typedArray.length); //传递
};

接收:
this.videoCallback = Module.addFunction(function (buff, size, timestamp) {
    var outArray = Module.HEAPU8.subarray(buff, buff + size); //拷贝
    var data = new Uint8Array(outArray);
    var objData = {
        t: kVideoFrame,
        s: timestamp,
        d: data
    };
    self.postMessage(objData, [objData.d.buffer]); //发送给Player
});
需要把回调通过openDecoder方法传入C层,在C层调用。

5 编译

5.1 安装Emscripten

参考其官方文档

5.2 下载FFmpeg

git clone https://git.ffmpeg.org/ffmpeg.git

这里切到了3.3分支。

5.3 下载本文的代码

保证FFmpeg目录和代码目录平级。

git clone https://github.com/sonysuqin/WasmVideoPlayer.git

5.4 编译

进入代码目录,执行:

./build_decoder.sh

6 测试

可以使用任意的Http Server(Apache、Nginx等),例如: 如果安装了node/npm/http-server,则在代码目录下执行:

http-server -p 8080 .

在浏览器输入即可:

http://127.0.0.1:8080

Demo地址:播放

7 浏览器支持

目前(20190207)没有做太多严格的浏览器兼容性测试,主要在Chrome上开发,以下浏览器比较新的版本都可以运行: