实现一个wasm视频解码渲染的小demo,网页端集成emcc编译的ffmpeg库,实现视频解码,使用WebGL实现视频渲染。demo中包含了一个基于mongoose的微型Web服务器,用于网页的Web服务和视频流传输,基本无需额外搭建环境以及编译第三方库,可以简单地移植到嵌入式系统中用于网页视频播放视频。学习过程中主要参考了大神代码和文章
编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(2)使用Emscripten编译 - 腾讯云开发者社区-腾讯云
demo地址
wasm_websocket_player: wasm 解码渲染demo
首先需要获取emcc用于编译,Mac下可以直接通过brew install来获取。下一步就是通过emcc,将ffmpeg编译对应的静态库。注意这里需要将ffmpeg中平台相关以及汇编相关的选项禁掉,毕竟这里最终都是在js虚拟机中执行,硬件加速相关的操作都需要去掉。下面是demo中编译ffmpeg使用的命令,源文件在demo的third_party文件下。
- mkdir ffmpeg-emcc
- cd FFmpeg_new
- #make clean
- emconfigure ./configure --cc="emcc" --cxx="em++" --ar="emar" \
- --ranlib=emranlib --prefix=../ffmpeg-emcc/ \
- --enable-cross-compile --target-os=none \
- --arch=x86_32 --cpu=generic --enable-gpl \
- --disable-avdevice \
- --disable-postproc --disable-avfilter \
- --disable-programs \
- --disable-everything --enable-avformat \
- --enable-decoder=hevc --enable-decoder=h264 --enable-decoder=h264_qsv \
- --enable-decoder=hevc_qsv \
- --enable-decoder=aac \
- --disable-ffplay --disable-ffprobe --disable-asm \
- --disable-doc --disable-devices --disable-network \
- --disable-hwaccels \
- --disable-debug \
-
- --enable-protocol=file --disable-indevs --disable-outdevs \
- --enable-parser=hevc --enable-parser=h264
-
- emmake make -j4
- emmake make install
ffmpeg静态链接库生成后,下一步就可以编译demo中客户端相关的源码,包括我们自己调用ffmpeg库的代码,c层与js层交互的代码,以及ffmpeg静态链接库,最终生成一个js文件和一个.wasm库,在网页中我们通过调用生成的js文件进行解码。下面是编译命令,源文件在demo工程的client文件下的build_with_emcc.sh。
- export TOTAL_MEMORY=67108864
-
- CURR_DIR=$(pwd)
- export FFMPEG_PATH=$CURR_DIR/../third_party/ffmpeg-emcc
-
- emcc --bind ../common/video_decoder.cc ../common/h264_reader.cc ../common/frame_queue.cc main.cc\
- -std=c++11 \
- -s USE_PTHREADS=1\
- -g \
- -I "${FFMPEG_PATH}/include" \
- -L ${FFMPEG_PATH}/lib \
- -lavutil -lavformat -lavcodec \
- -s WASM=1 -Wall \
- -s EXPORTED_FUNCTIONS="['_malloc','_free']" \
- -s ASSERTIONS=0 \
- -s ALLOW_MEMORY_GROWTH=1 \
- -s TOTAL_MEMORY=167772160 \
- -o ${PWD}/player.js
最终会生成player.js以及player.wasm文件。
demo中提供了一个微型Web server,提供http服务以及websocket数据传输。考虑到demo主要用于嵌入式平台,这里选择了mongoose作为Web服务器,只需要在源代码中引入一个.c文件和一个.h文件即可使用,无需复杂的编译和依赖库。demo中使用了一个本地h264文件,server收到客户端请求后会读取这个本地文件,通过avformat读取每帧h264,实际使用中可以将这块的代码更换为当前设备的采集和编码。目前调试是在Mac的arm64版本上编译,直接运行server目录下cmake即可。
可以直接在server目录下运行run.sh,即可完成客户端编译,服务端编译以及相关文件的拷贝。目前写死使用8000端口。

wasm内存分配与释放
这里首先介绍一下js与底层wasm的交互方式。一般视频流数据数量较小,可以直接为其分配内存空间,这里我们直接通过在js层调用_malloc和_free进行分配和释放内存,这些内存可以被wasm代码所使用。这里首先分配wasm可以使用的内存,下一步就是将js的Uint8Array数据拷贝给这块内存,这样wasm中的代码就可以操作这块内存了。
js传递数据给wasm
这里可以在C++层通过EMSCRIPTEN_BINDINGS对C++函数进行封装,基本数据类型可以使用普通的C/C++数据类型,传入js所分配的内存,在C/C++层直接使用uintptr_t类型即可。下面使用我们deocder类来进行说明。
decoder类的C++类,emscripten::val lambda类型可以将一个js函数传入wasm作为回调函数。
- class StreamDecoderWrapper{
- public:
-
- StreamDecoderWrapper(){}
- ~StreamDecoderWrapper(){}
-
- void OpenAvcDecoder(emscripten::val lambda){
- ... ...
- decoder.OpenWithCodecID(AV_CODEC_ID_H264);
- decoder.RegisterDecodeCallback([lambda, this](AVFrame *frame)->int{
-
- ... ...
- auto frame_wrapper = std::make_shared
()->Alloc(AVMEDIA_TYPE_AUDIO, out_frame); - ... ...
- lambda(frame_wrapper);
- return 0;
- });
- }
-
- void DecodeVideoPacket(uintptr_t buf_p, int size){
-
- uint8_t *data = reinterpret_cast<uint8_t *>(buf_p);
- ... ...
- }
-
- void CloseDecoder(){
- ... ...
- }
-
- private:
- ... ...
- };
注册StreamDecoderWrapper,让js代码可以识别这个类。这个操作类似jni的动态注册,将字符串与C++类名和方法名对应,这样在js层中可以直接使用这个字符串创建对象并调用方法。
- #include
- #ifndef NDEBUG
- #include
- #endif
-
- #include "stream_decoder_wrapper.h"
-
- using namespace emscripten;
-
- EMSCRIPTEN_BINDINGS(module){
- ... ...
-
- class_
("StreamDecoderWrapper") - .constructor<>()
- .function("openAvcDecoder", &StreamDecoderWrapper::OpenAvcDecoder)
- .function("decodeVideoPacket", &StreamDecoderWrapper::DecodeVideoPacket)
- .function("closeDecoder", &StreamDecoderWrapper::CloseDecoder);
-
- ... ...
- }
js层调用wasm类StreamDecoderWrapper,可以完全当作是一个js类,通过new创建对象并调用方法。
- class StreamDecoderWrapperJS{
-
- #stream_decoder_inner = null;
-
- StreamDecoderWrapperJS(){
- }
-
- openAvcDecoder(frame_callback){
- this.#stream_decoder_inner = new Module.StreamDecoderWrapper()
- this.#stream_decoder_inner.openAvcDecoder((videoFrameWrapperJS)=>{
- ... ...
- frame_callback(videoFrameWrapperJS)
-
- videoFrameWrapperJS.delete();
- })
- }
-
- decodeVideoPacket(data, size, headsize){
-
- ... ...
- let data_array = new Uint8Array(data)
- let data_slice = data_array.slice(headsize, headsize+size)
- let data_len = size;
- let buf = _malloc(data_len);
-
- HEAPU8.set(data_slice, buf);
-
- this.#stream_decoder_inner.decodeVideoPacket(buf, data_len)
-
- _free(buf);
-
- ... ...
- }
-
- closeDecoder(){
- this.#stream_decoder_inner.closeDecoder();
- }
- }

在wasm中收到js传来的buffer数据后,就可以进行下一步解码。代码如下,可以看到这里都是普通C/C++的数据类型,js层传来的buf_p在这里直接就是一个uint8_t类型的buffer,拿到正确数据交给ffmpeg进行解码即可。
- void DecodeVideoPacket(uintptr_t buf_p, int size){
-
- uint8_t *data = reinterpret_cast<uint8_t *>(buf_p);
- if(data && (size != 0)){
- ... ...
- decoder.Decode(data, size);
- ... ...
- }
- }
ffmpeg解码代码这里就不再赘述,还不太了解的朋友可以参考ffmpeg中doc下的例子。这里需要明确,AVPacket用于封装视频流buffer,AVFrame用于封装解码后的YUV数据,AVFrame中的数据可以通过 av_frame_move_ref 方法移动其内部存放的buffer,av_frame_unref给buffer减引用,引用为0就销毁buffer。后续在js层使用完毕后释放对象时,我们会使用这些方法,否则会造成浏览器内存泄露。

解码完毕后,需要将YUV数据传递回js层,用于渲染。同样,这里也是通过注册C++类,映射一个对应的js类,在js层操作这个类,不同的是上一个我们创建的解码器会存在较长时间,而这里创建的视频帧类在使用完毕后需要立刻释放。
视频帧frame的C++类。其中wasm中的内存并不需要拷贝,可以直接通过emscripten::typed_memory_view 映射,在js层直接使用映射得到的内存句柄即可。这里把YUV的内存都进行了映射,同时还能返回视频帧的宽高和stride等信息。
- #ifndef _VIDEO_FRAME_WRAPPER_H_
- #define _VIDEO_FRAME_WRAPPER_H_
-
- #ifdef __cplusplus
- extern "C" {
- #endif
-
- #include
- #include
-
- #ifdef __cplusplus
- }
- #endif
-
- #include
- #include
- #include
-
- class VideoFrameWrapper : public std::enable_shared_from_this
{ -
- public:
- VideoFrameWrapper(){}
- ~VideoFrameWrapper(){
- Free();
- }
-
- int type() const { return type_; }
- uint8_t *data() const { return frame_->data[0]; }
- int linesizeY() const { return frame_->linesize[0]; }
- int linesizeU() const { return frame_->linesize[1]; }
- int linesizeV() const { return frame_->linesize[2]; }
- int width() const { return frame_->width; }
- int height() const { return frame_->height; }
- int format() const { return frame_->format; }
-
- double pts() const { return frame_->pts; }
-
- int data_ptr() const { return (int)(frame_->data[0]); } // NOLINT
- int size() const {
- return av_image_get_buffer_size(
- AV_PIX_FMT_YUV420P, frame_->width, frame_->height, 1);
- }
- emscripten::val GetBytes() {
- return emscripten::val(
- emscripten::typed_memory_view(size(), frame_->data[0]));
- }
- emscripten::val GetBytesY() {
- return emscripten::val(
- emscripten::typed_memory_view(size(), frame_->data[0]));
- }
- emscripten::val GetBytesU() {
- return emscripten::val(
- emscripten::typed_memory_view(size(), frame_->data[1]));
- }
- emscripten::val GetBytesV() {
- return emscripten::val(
- emscripten::typed_memory_view(size(), frame_->data[2]));
- }
-
- std::shared_ptr
Alloc(AVMediaType type, AVFrame *frame) { - type_ = type;
- frame_ = frame;
- return shared_from_this();
- }
-
- void Free() {
- type_ = AVMEDIA_TYPE_UNKNOWN;
- if (frame_ != nullptr) {
- av_frame_unref(frame_);
- av_frame_free(&frame_);
- frame_ = nullptr;
- std::cout << "Frame::Free 1 this="<< (std::hex) <<this <
- }
- }
-
- private:
- int type_;
- AVFrame *frame_;
- };
-
- #endif
VideoFrameWrapper 传递给js层。这里首先创建一个AVFrame,将解码后的内存转给这个AVFrame,之后创建VideoFrameWrapper,将其作为一个shared_ptr返回给js层。可见wasm可以将shared_ptr传递给js,那么js中也需要对shared_ptr进行管理。
- void OpenAvcDecoder(emscripten::val lambda){
- std::cout<<"StreamDecoderWrapper::OpenAvcDecoder create"<
-
- decoder.OpenWithCodecID(AV_CODEC_ID_H264);
- decoder.RegisterDecodeCallback([lambda, this](AVFrame *frame)->int{
-
- AVFrame *out_frame = av_frame_alloc();
- av_frame_move_ref(out_frame, frame);
- auto frame_wrapper = std::make_shared
()->Alloc(AVMEDIA_TYPE_AUDIO, out_frame); -
- lambda(frame_wrapper);
- return 0;
- });
- }
VideoFrameWrapper注册js对象。注意,注册的时候要加一个smart_ptr,这个类在js层也会对对象进行引用操作。同时这里还注册了可以直接访问的属性。
- #include
- #ifndef NDEBUG
- #include
- #endif
-
- #include "file_decoder_wrapper.h"
- #include "stream_decoder_wrapper.h"
- #include "video_frame_wrapper.h"
-
- using namespace emscripten;
-
- EMSCRIPTEN_BINDINGS(module){
- ... ...
-
- class_
("VideoFrameWrapper") - .smart_ptr
>("shared_ptr" ) - .property("type", &VideoFrameWrapper::type)
- .property("data", &VideoFrameWrapper::data_ptr)
- .property("linesizeY", &VideoFrameWrapper::linesizeY)
- .property("linesizeU", &VideoFrameWrapper::linesizeU)
- .property("linesizeV", &VideoFrameWrapper::linesizeV)
- .property("width", &VideoFrameWrapper::width)
- .property("height", &VideoFrameWrapper::height)
- .property("format", &VideoFrameWrapper::format)
- .property("pts", &VideoFrameWrapper::pts)
- .property("size", &VideoFrameWrapper::size)
- .function("getBytes", &VideoFrameWrapper::GetBytes)
- .function("getBytesY", &VideoFrameWrapper::GetBytesY)
- .function("getBytesU", &VideoFrameWrapper::GetBytesU)
- .function("getBytesV", &VideoFrameWrapper::GetBytesV);
- }
js层调用。这里js层可以读取到回调对象的属性,还可以将其作为一个js对象传递,最终这个对象调用delete进行释放。
- openAvcDecoder(frame_callback){
- this.#stream_decoder_inner = new Module.StreamDecoderWrapper()
- this.#stream_decoder_inner.openAvcDecoder((videoFrameWrapperJS)=>{
- let w = videoFrameWrapperJS.width;
- let h = videoFrameWrapperJS.height;
-
- frame_callback(videoFrameWrapperJS)
-
- videoFrameWrapperJS.delete();
- })
- }
3.WebGL渲染
js层得到YUV的内存句柄就可以使用WebGL进行渲染。浏览器端WebGL可以直接将canvas作为画布,不需要EGL之类的复杂操作。外部获取canvas标签后,直接用其获取context,后续OpenGL操作在这个context上进行即可。
- class WebGLPlayer {
- constructor(canvas) {
- this.canvas = canvas;
- this.gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
- ... ...
- }
- }
shader编译,这里和一般OpenGL的shader操作一样,编译顶点和片元shader,获取顶点坐标和纹理坐标索引,获取YUV三个纹理的索引。
- #init() {
- if (!this.gl) {
- console.log("[ERROR] WebGL not supported");
- return;
- }
-
- const gl = this.gl;
- gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
-
- const program = gl.createProgram();
-
- const vertexShaderSource = [
- "attribute highp vec3 aPos;",
- "attribute vec2 aTexCoord;",
- "varying highp vec2 vTexCoord;",
- "void main(void) {",
- " gl_Position = vec4(aPos, 1.0);",
- " vTexCoord = aTexCoord;",
- "}",
- ].join("\n");
- const vertexShader = gl.createShader(gl.VERTEX_SHADER);
- gl.shaderSource(vertexShader, vertexShaderSource);
- gl.compileShader(vertexShader);
- {
- const msg = gl.getShaderInfoLog(vertexShader);
- if (msg) {
- console.log("[ERROR] Vertex shader compile failed");
- console.log(msg);
- }
- }
-
- const fragmentShaderSource = [
- "precision highp float;",
- "varying lowp vec2 vTexCoord;",
- "uniform sampler2D yTex;",
- "uniform sampler2D uTex;",
- "uniform sampler2D vTex;",
- "const mat4 YUV2RGB = mat4(",
- " 1.1643828125, 0, 1.59602734375, -.87078515625,",
- " 1.1643828125, -.39176171875, -.81296875, .52959375,",
- " 1.1643828125, 2.017234375, 0, -1.081390625,",
- " 0, 0, 0, 1",
- ");",
- "void main(void) {",
- " // gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 0., 1.0);",
- " gl_FragColor = vec4(",
- " texture2D(yTex, vTexCoord).x,",
- " texture2D(uTex, vTexCoord).x,",
- " texture2D(vTex, vTexCoord).x,",
- " 1",
- " ) * YUV2RGB;",
- "}",
- ].join("\n");
- const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
- gl.shaderSource(fragmentShader, fragmentShaderSource);
- gl.compileShader(fragmentShader);
- {
- const msg = gl.getShaderInfoLog(fragmentShader);
- if (msg) {
- console.log("[ERROR] Fragment shader compile failed");
- console.log(msg);
- }
- }
-
- gl.attachShader(program, vertexShader);
- gl.attachShader(program, fragmentShader);
- gl.linkProgram(program);
- gl.useProgram(program);
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
- console.log("[ERROR] Shader link failed");
- }
-
- const vertices = new Float32Array([
- // positions // texture coords
- -1.0, -1.0, 0.0, 0.0, 1.0, // bottom left
- 1.0, -1.0, 0.0, 1.0, 1.0, // bottom right
- -1.0, 1.0, 0.0, 0.0, 0.0, // top left
- 1.0, 1.0, 0.0, 1.0, 0.0, // top right
- ])
- const verticesBuffer = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
- gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
-
- const vertexPositionAttribute = gl.getAttribLocation(program, "aPos");
- gl.enableVertexAttribArray(vertexPositionAttribute);
- gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 20, 0);
-
- const textureCoordAttribute = gl.getAttribLocation(program, "aTexCoord");
- gl.enableVertexAttribArray(textureCoordAttribute);
- gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 20, 12);
-
- gl.y = new Texture(gl);
- gl.u = new Texture(gl);
- gl.v = new Texture(gl);
- gl.y.bind(0, program, "yTex");
- gl.u.bind(1, program, "uTex");
- gl.v.bind(2, program, "vTex");
- }
在得到我们上一部抛出的封装了解码数据的VideoFrameWrapper后就可以进行渲染了。这里注意ffmpeg解码后的YUV数据不一定是连续的,一定分别拿出AVFrame的每个分量,分别映射出来,否则可能会导致花屏。最终通过gl.y.fill gl.u.fill gl.v.fill 分别给yuv对应纹理上传buffer。这样就完成了渲染操作。
- render(frame) {
- if (!this.gl) {
- console.log("[ERROR] Render failed due to WebGL not supported");
- return;
- }
-
- const gl = this.gl;
-
- let port_width = gl.canvas.width;
- let port_height = gl.canvas.height;
-
- gl.viewport(0, 0, port_width, port_height);
-
- gl.clearColor(0.0, 0.0, 0.0, 0.0);
- gl.clear(gl.COLOR_BUFFER_BIT);
-
- const width = frame.width;
- const linesize = frame.linesize;
- const height = frame.height;
- const bytes = frame.bytes;
-
- const byteYLinesize = frame.linesizeY;
- const byteULinesize = frame.linesizeU;
- const byteVLinesize = frame.linesizeV;
-
- console.log('render width='+width+' linesizeY='+byteYLinesize)
-
- const len_y = byteYLinesize * height;
- const len_u = byteULinesize * height >> 1;
- const len_v = byteVLinesize * height >> 1;
-
- const byteY = frame.getBytesY()
- const byteU = frame.getBytesU()
- const byteV = frame.getBytesV()
-
- gl.y.fill(byteYLinesize, height, byteY.subarray(0, len_y));
- gl.u.fill(byteULinesize, height >> 1, byteU.subarray(0, len_u));
- gl.v.fill(byteVLinesize, height >> 1, byteV.subarray(0, len_v));
-
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
-
- //gl.finish();
- //gl.commit();
- }
渲染播放丢帧问题
在实际操作中发现播放过程中丢帧严重,经过排查是在解码完成后直接抛出帧,由于解码时间不均匀导致有些帧播放后很快又被新帧覆盖,导致播放卡顿。目前在c层解码完毕后增加了一个delay操作,按照解码时间和帧率进行延迟等待,用于平滑渲染。此处考虑是否可以引入一个线程,或者是否有其比较好的解决方式。目前控制播放代码如下
- decoder.RegisterDecodeCallback([lambda, this](AVFrame *frame)->int{
-
- std::cout<<"OpenAvcDecoder debug2"<
-
- AVFrame *out_frame = av_frame_alloc();
- av_frame_move_ref(out_frame, frame);
- auto frame_wrapper = std::make_shared
()->Alloc(AVMEDIA_TYPE_AUDIO, out_frame); - long delay_time = -1;
- long curr_ts = std::chrono::duration_cast
(std::chrono::system_clock::now().time_since_epoch()).count(); - if(last_out_ts != 0){
- long curr_gap = curr_ts - last_out_ts;
- if(curr_gap > 0 && curr_gap < gap){
- delay_time = gap - curr_gap;
- }
- }
- last_out_ts = curr_ts;
- if(delay_time > 0){
- usleep(delay_time * 1000);
- std::cout<<"OpenAvcDecoder delay_time="<
- }
- lambda(frame_wrapper);
- return 0;
- });
4.Server端交互
server端与js端通过Websocket进行数据交互,目前提供了一个简单的协议头,用于请求视频和停止视频
- //json data type == 1
- //video data type == 2
- // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
- //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- //| 'A' | 'A' |v=1| type | rec |
- //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- //| payload length |
- //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
server端提供了一个H264FileVideoCapture类用于模拟视频采集和编码,如果需要使用自己的采集编码可以重新实现一个H264FileVideoCapture和MediaStreamer。
4.1 mongoose支持wasm多线程
wasm开启多线程,需要浏览器开启Cross-origin保护,否则直接报错。
这里需要mongoose在收到网页请求的时候,在响应头中增加设置,代码实现如下
- struct mg_http_serve_opts opts = {.root_dir = s_web_root};
- //wasm 多线程需要增加响应头
- opts.extra_headers = "Cross-Origin-Embedder-Policy:require-corp\r\nCross-Origin-Opener-Policy:same-origin\r\n";
- mg_http_serve_dir(c, (struct mg_http_message *)ev_data, &opts);
-
相关阅读:
Android问题笔记四十二:signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) 的解决方法
【web开发】spring security配合验证码,java获取地理位置(绘制百度地图)、天气
Mybatis(动态sql和分页)
初识 SpringMVC,运行配置第一个Spring MVC 程序
对象的解构赋值(基本用法1)
程序猿成长之路之密码学篇-RSA非对称分组加密算法介绍
327.区间和的个数
【算法】单调栈
【文件操作API的使用】
【java实战项目】90分钟轻松学会java开发飞机大战小游戏
-
原文地址:https://blog.csdn.net/lidec/article/details/128178176