# 探索网页视频播放
# 原文说明
本文相关的代码使用 WASM、FFmpeg、WebGL、Web Audio 等组件实现了一个简易的支持 H265 的 Web 播放器,作为探索、验证,just for fun。
# 如何使用
# 安装 Emscripten
cd /
mkdir env
cd /env
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
echo 'source "/env/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile
# 下载 FFmpeg
cd /
mkdir project
cd /project
mkdir wasmvideo
cd wasmvdideo
git clone https://github.com/FFmpeg/FFmpeg.git --depth=1 -b release/3.3
mv FFmpeg ffmpeg
# 下载项目代码
git clone https://github.com/sonysuqin/WasmVideoPlayer.git --depth=1
# 开始编译
cd WasmVideoPlayer
./build_decoder.sh
# 警告 1
emcc: warning: EXTRA_EXPORTED_RUNTIME_METHODS is deprecated, please use EXPORTED_RUNTIME_METHODS instead [-Wdeprecated]
修改如下
rm -rf libffmpeg.wasm libffmpeg.js
# 设置总内存为64MB
export TOTAL_MEMORY=67108864
# 设置导入方法
export EXPORTED_FUNCTIONS="[ \
'_initDecoder', \
'_uninitDecoder', \
'_openDecoder', \
'_closeDecoder', \
'_sendData', \
'_decodeOnePacket', \
'_seekTo', \
'_main',
'_malloc',
'_free'
]"
echo "Running Emscripten..."
emcc decoder.c dist/lib/libavformat.a dist/lib/libavcodec.a dist/lib/libavutil.a dist/lib/libswscale.a \
-O3 \
-I "dist/include" \
-s WASM=1 \
-s TOTAL_MEMORY=${TOTAL_MEMORY} \
-s EXPORTED_FUNCTIONS="${EXPORTED_FUNCTIONS}" \
-s EXPORTED_RUNTIME_METHODS="['addFunction']" \
-s RESERVED_FUNCTION_POINTERS=14 \
-s FORCE_FILESYSTEM=1 \
-o libffmpeg.js
echo "Finished Build"
# 报错 1
TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.
修改 nginx 配置
http {
types {
application/wasm wasm;
}
}
查看视频信息
mv video/h265_high.mp4 video/h265_high.bak
ffmpeg -i video/h265_high.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video/h265_high.bak':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2mp41
Duration: 00:07:47.98, start: 0.000000, bitrate: 437 kb/s
Stream #0:0(und): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 852x480 [SAR 640:639 DAR 16:9], 400 kb/s, 25 fps, 25 tbr, 12800 tbn, 25 tbc (default)
Metadata:
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (HE-AAC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 32 kb/s (default)
Metadata:
handler_name : SoundHandler
ffmpeg -i video/h265_high.bak -c copy -t 60 video/h265_high.mp4
ffmpeg -i video/h265_high.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video/h265_high.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2mp41
encoder : Lavf58.29.100
Duration: 00:01:00.08, start: 0.000000, bitrate: 368 kb/s
Stream #0:0(und): Video: hevc (Main) (hvc1 / 0x31637668), yuv420p(tv, progressive), 852x480 [SAR 640:639 DAR 16:9], 331 kb/s, 25 fps, 25 tbr, 12800 tbn, 25 tbc (default)
Metadata:
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (HE-AAC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 32 kb/s (default)
Metadata:
handler_name : SoundHandler
# 播放器原理学习
# 依赖
Wasm FFmpeg
主要使用 FFmpeg 来做解封装(demux)和解码(decoder),由于使用了 FFmpeg(3.3),理论上可以播放绝大多数格式的视频,这里只针对 H265 编码、MP4 封装,在编译时可以只按需编译最少的模块,从而得到比较小的库。
使用 Emscripten 编译 FFmpeg 主要参考下面这个网页,做了一些修改: https://blog.csdn.net/Jacob_job/article/details/79434207
WebGL
H5 使用 Canvas 来绘图,但是默认的 2d 模式只能绘制 RGB 格式,使用 FFmpeg 解码出来的视频数据是 YUV 格式,想要渲染出来需要进行颜色空间转换,可以使用 FFmpeg 的 libswscale 模块进行转换,为了提升性能,这里使用了 WebGL 来硬件加速,主要参考了这个项目,做了一些修改: https://github.com/p4prasoon/YUV-Webgl-Video-Player
Web Audio
FFmpeg 解码出来的音频数据是 PCM 格式,可以使用 H5 的 Web Audio Api 来播放,主要参考了这个项目,做了一些修改: https://github.com/samirkumardas/pcm-player
# 模块结构
# 线程模型
主要有三个线程:
- 主线程(Player):界面控制、播放控制、下载控制、音视频渲染、音视频同步;
- 解码线程(Decoder Worker):音视频数据的解封装、解码;
- 下载线程(Downloader Worker):下载某个 chunk。 线程之间通过 postMessage 进行异步通信,在需要传输大量数据(例如视频帧)的地方,需要使用 Transferable 接口来传输,避免大数据的拷贝损耗性能。
# 对外接口
- play:开始播放;
- pause:暂停播放;
- resume:恢复播放;
- stop:停止播放;
- fullscreen:全屏播放;
# 下载控制
为防止播放器无限制地下载文件,在下载操作中占用过多的 CPU,浪费过多带宽,这里在获取到文件码率之后,以码率一定倍数的速率下载文件。
# 缓冲控制
缓存控制对这个播放器的意义重大,在这个时间点,WASM 还无法使用多线程以及多线程的同步,FFmpeg 的同步读数据接口必须保证返回数据。所以这里有两个措施,1:在未获取到文件元信息之前的数据缓存;2:解码帧缓存。必须控制好这两个缓存,才能保证任何时候 FFmpeg 需要读取数据时都能够返回数据,在数据不足时停止解码,进入 Buffer 状态,数据足够时继续解码播放,返回 Play 状态,保证 FFmpeg 不会报错退出播放。
# Downloader
这个模块很简单,只是单纯为了不在主线程做太多事情而分离,功能主要有:
通过 Content-Length 字段获取文件的长度; 通过 Range 字段下载一个 chunk。 如上面提到的,Player 会进行速率控制,因此需要把文件分成 chunk,按照 chunk 方式进行下载。下载的数据先发给 Player,由 Player 转交给 Decoder(理论上应该直接交给 Decoder,但是 Downloader 无法直接与 Decoder 通信)。 对流式的数据,则使用 Fetch。
发送:
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 层调用。
# 项目资料
https://github.com/sonysuqin/WasmVideoPlayer
https://github.com/ErosZy/WXInlinePlayer
https://github.com/liang520/webAssemblyPlayer
https://github.com/langhuihui/jessibuca
https://github.com/cisco/openh264
https://github.com/udevbe/tinyh264
https://github.com/strukturag/libde265
https://github.com/goldvideo/decoder_wasm
https://github.com/goldvideo/h265player