实现一个wasm视频解码渲染的小demo,网页端集成emcc编译的ffmpeg库,实现视频解码,使用WebGL实现视频渲染。demo中包含了一个基于mongoose的微型Web服务器,用于网页的Web服务和视频流传输,基本无需额外搭建环境以及编译第三方库,可以简单地移植到嵌入式系统中用于网页视频播放视频。学习过程中主要参考了大神代码和文章
编译WebAssembly版本的FFmpeg(ffmpeg.wasm):(2)使用Emscripten编译 - 腾讯云开发者社区-腾讯云
demo地址
wasm_websocket_player: wasm 解码渲染demo
1.编译
1.1 ffmpeg emcc版本编译
首先需要获取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
1.2 客户端源代码编译
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文件。
1.3 demo中server的编译与demo运行
demo中提供了一个微型Web server,提供http服务以及websocket数据传输。考虑到demo主要用于嵌入式平台,这里选择了mongoose作为Web服务器,只需要在源代码中引入一个.c文件和一个.h文件即可使用,无需复杂的编译和依赖库。demo中使用了一个本地h264文件,server收到客户端请求后会读取这个本地文件,通过avformat读取每帧h264,实际使用中可以将这块的代码更换为当前设备的采集和编码。目前调试是在Mac的arm64版本上编译,直接运行server目录下cmake即可。
可以直接在server目录下运行run.sh,即可完成客户端编译,服务端编译以及相关文件的拷贝。目前写死使用8000端口。
2.解码流程实现
2.1 js传递视频流给wasm解码
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<VideoFrameWrapper>()->
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 <emscripten/bind.h>
-
#ifndef NDEBUG
-
#include <sanitizer/lsan_interface.h>
-
#endif
-
-
#include "stream_decoder_wrapper.h"
-
-
using
namespace emscripten;
-
-
EMSCRIPTEN_BINDINGS(
module){
-
... ...
-
-
class_<StreamDecoderWrapper>(
"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();
-
}
-
}
2.2 解码
在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层使用完毕后释放对象时,我们会使用这些方法,否则会造成浏览器内存泄露。
2.3 wasm数据传递给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 <libavutil/frame.h>
-
#include <libavutil/imgutils.h>
-
-
#ifdef __cplusplus
-
}
-
#endif
-
-
#include <memory>
-
#include <iostream>
-
#include <emscripten/val.h>
-
-
class
VideoFrameWrapper :
public std::enable_shared_from_this<VideoFrameWrapper>{
-
-
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<VideoFrameWrapper> 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 <<std::endl;
-
}
-
}
-
-
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"<<std::endl;
-
-
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<VideoFrameWrapper>()->
Alloc(AVMEDIA_TYPE_AUDIO, out_frame);
-
-
lambda(frame_wrapper);
-
return
0;
-
});
-
}
VideoFrameWrapper注册js对象。注意,注册的时候要加一个smart_ptr,这个类在js层也会对对象进行引用操作。同时这里还注册了可以直接访问的属性。
-
#include <emscripten/bind.h>
-
#ifndef NDEBUG
-
#include <sanitizer/lsan_interface.h>
-
#endif
-
-
#include "file_decoder_wrapper.h"
-
#include "stream_decoder_wrapper.h"
-
#include "video_frame_wrapper.h"
-
-
using
namespace emscripten;
-
-
EMSCRIPTEN_BINDINGS(
module){
-
... ...
-
-
class_<VideoFrameWrapper>(
"VideoFrameWrapper")
-
.smart_ptr<std::shared_ptr<VideoFrameWrapper>>(
"shared_ptr<VideoFrameWrapper>")
-
.
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"<<std::endl;
-
-
AVFrame *out_frame =
av_frame_alloc();
-
av_frame_move_ref(out_frame, frame);
-
auto frame_wrapper = std::
make_shared<VideoFrameWrapper>()->
Alloc(AVMEDIA_TYPE_AUDIO, out_frame);
-
long delay_time =
-1;
-
long curr_ts = std::chrono::
duration_cast<std::chrono::milliseconds>(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="<<delay_time<<std::endl;
-
}
-
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);
转载:https://blog.csdn.net/lidec/article/details/128178176