WebAssembly之客户端与网页进行画面实时传输实现简易1对1视频-Web端

489 篇文章 14 订阅
464 篇文章 13 订阅

1、引言

前两篇:

2、WebAssembly

WebAssembly(简称wasm)是一个虚拟指令集体系架构(virtual ISA),整体架构包括核心的ISA定义、二进制编码、程序语义的定义与执行,以及面向不同的嵌入环境(如Web)的应用编程接口(WebAssembly API)。其初始目标是为C/C++等语言编写的程序经过编译,在确保安全和接近原生应用的运行速度更好地在Web平台上运行。

简单的解释就是:可以通过WebAssembly建立c/c++与JavaScript的通信

3、为什么需要c/c++

因为要用FFmpeg库去解码传输过来的H.264裸流,FFmpeg基于c编写的,故需要c解码之后传输给JavaScript。

4、如何使用WebAssembly技术

4.1 FFmpeg源码下载

音视频(5)客户端与网页进行画面实时传输实现简易1对1视频-客户端开发 已经讲过了,读者可以自行跳转去看如何操作~

4.2 Emscripten编译工具下载与环境搭建

下载地址: Emscripten

按照官方文档中的步骤一步一步可以将环境搭建起来

5、编译FFmpeg为静态文件(.a)

当环境搭建完成后并且确保命令emc以及emc++是存在的 /ffmpeg/目录下创建 js_build.sh脚本文件

CPPFLAGS="-D_POSIX_C_SOURCE=200112 -D_XOPEN_SOURCE=600" \
​
emconfigure ./configure \
     --cc="emcc" \
     --cxx="em++" \
     --ar="emar" \
     --prefix=$(pwd)/dist2 \
     --cpu=generic \
     --target-os=none \
     --ranlib=emranlib \
     --arch=x86_64 \
     --disable-doc \
     --disable-debug \
     --disable-ffmpeg \
     --disable-ffplay \
     --disable-ffprobe \
     --disable-symver \
     --disable-everything \
     --disable-asm \
     --enable-gpl \
     --enable-version3 \
     --enable-nonfree \
     --enable-cross-compile \
     --enable-small \
     --enable-parser=aac \
     --enable-parser=h264 \
     --enable-demuxer=h264 \
     --enable-demuxer=mov \
     --enable-demuxer=aac \
     --enable-decoder=h264 \
     --enable-decoder=aac  
make clean
make
make install

--cc="emcc --cxx="em++ --ar="emcar"编译工具全部换成Emscripten中自带的工具 其他的可以在ffmpeg 命令中自行选择,是否需要将其他库进行编译。 --prefix=$(pwd)/dist2 编译后的文件保存在dist2中 执行

6、解码H.264

创建test_js_ffmpeg.c文件编写解码代码:

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
​
AVFormatContext *fmt_ctx;
AVCodec *codec;
AVCodecContext *codec_ctx;
AVCodecParameters *codec_param;
struct SwsContext *img_ctx;
unsigned char *out_buffer;
AVPacket *avPacket;
AVFrame *rgbFrame;
AVFrame *yuv420Frame;
AVCodecParserContext *parser;
​
uint8_t* buf;
uint8_t *rgb_buffer;
uint8_t *getFrameBuffer(AVFrame *pFrame, AVCodecContext *pCodecCtx) {
    int width = pCodecCtx->width;
    int height = pCodecCtx->height;
    for (int y = 0; y < height; y++) {
        memcpy(rgb_buffer + y * pFrame->linesize[0], pFrame->data[0] + y * pFrame->linesize[0], width * 3);
    }
    return rgb_buffer;
}
​
//开辟一段内存
uint8_t * getBufferAddress(int len) {
    if(buf != NULL) {
        av_free(buf);
    }
    buf = (uint8_t*) av_malloc(len);
    return buf;
}
​
void decode(int size,void (*func)(uint8_t *data))
{
    //此时js已经利用Module.HEAPU8.set 将数据存入以buf首地址开始的内存中
    avPacket->data = buf;
    avPacket->size = size;
    if (avcodec_send_packet(codec_ctx, avPacket) == 0)
    {
        while (avcodec_receive_frame(codec_ctx, yuv420Frame) == 0)
        {
            // yuv转换rgb
            sws_scale(img_ctx,
                      (const uint8_t *const *)yuv420Frame->data,
                      yuv420Frame->linesize,
                      0,
                      codec_ctx->height,
                      rgbFrame->data,
                      rgbFrame->linesize);
                      //func回调函数 可与js交互
            func(getFrameBuffer(rgbFrame,codec_ctx));
        }
    }
}
​
​
int init(int w,int h)
{
    rgbFrame = av_frame_alloc();
    yuv420Frame = av_frame_alloc();
​
    codec_param = avcodec_parameters_alloc();
    codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    codec_ctx = avcodec_alloc_context3(codec);
    avcodec_parameters_to_context(codec_ctx, codec_param);
    codec_ctx->width = w;
    codec_ctx->height = h;
    codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    codec_ctx->frame_bits = 3000;
    
    if (avcodec_open2(codec_ctx, codec, NULL) != 0)
    {
        return -1;
    }
    img_ctx = sws_getContext(
        codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
        codec_ctx->width, codec_ctx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR,
        NULL, NULL, NULL);
    //分配图像空间
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codec_ctx->width,
                                            codec_ctx->height, 1);
    out_buffer = (unsigned char *)av_malloc(numBytes * sizeof(unsigned char *));
    //分配avpacket结构体
    avPacket = av_packet_alloc();
    if (avPacket == NULL)
    {
        return -1;
    }
    int i = 0;
    //调整packet数据
    av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, out_buffer, AV_PIX_FMT_RGB24,
                         codec_ctx->width, codec_ctx->height, 1);
    rgb_buffer = (uint8_t *)malloc(codec_ctx->height * codec_ctx->width * 3);
    return 1;
}
​
int isInit(){
    if(codec_ctx!=NULL) {
        return 1;
    }
    return 0;
}
  • rgb_buffer 用于缓存解码后的yuv转rgb24的数据

  • buf 开辟一段内存用于接收客户端传来的h264裸流数据

  • getBufferAddress方法 返回buf内存数组首地址给JS

  • yuv420Frame接收解码后的yuv数据

  • rgbFrame接收yuv转换到rgb后的数据

  • av_frame_alloc()初始化/开辟一段frame内存

  • avcodec_parameters_alloc()初始化编/解码器参数表

  • avcodec_find_decoder(AVCodecID) 寻找H264解码器

  • avcodec_alloc_context3() 初始化编/解码器上下文

  • avcodec_parameters_to_context()填充解码器数据

  • avcodec_open2()打开解码器

  • sws_getContext() 根据图像格式计算并获取图像转换器上下文 当前是以AV_PIX_FMT_YUV420P格式转换到AV_PIX_FMT_RGB24格式

  • avPacket编码包 用于存储传输来的H.264裸流数据

  • av_image_get_buffer_size()分配一帧图片RGB24格式的存储空间大小

  • out_buffer申请的一张图片RGB24格式的字节大小

  • av_packet_alloc() 初始化分配avpacket内存

  • av_image_fill_arrays() 将申请的一段内存空间out_buffer,填充到目标RGBframe中然后内存对其

  • avPacket->data指向H.264内存的指针

  • avPacket->size数据大小

  • avcodec_send_packet()将编码的数据发送到解码器解码

  • avcodec_receive_frame()解码器中取出解码后的数据yuvframe

  • sws_scale()转换YUV420P到RGB24到缓冲区rgbframe中

  • getFrameBuffer() rgb24计算公式 返回rgb24内存地址

解码库编写完成后,需要将写好的c文件test_js_ffmpeg.c编译成js文件和wasm文件

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

7、编译c到JS和WASM文件

确保emc环境已完成 创建js_ex_build.sh文件

export TOTAL_MEMORY=268435456
​
export FFMPEG_PATH=/Users/z1zzhyluojin/ffmpeg-4.3.4/dist2
​
​
emcc ./test_js_ffmpeg.c ${FFMPEG_PATH}/lib/libavformat.a ${FFMPEG_PATH}/lib/libavcodec.a ${FFMPEG_PATH}/lib/libswscale.a ${FFMPEG_PATH}/lib/libavutil.a \
    -O3 \
    -I "${FFMPEG_PATH}/include" \
    -o library.js \
    -s WASM=1 \
    -s TOTAL_MEMORY=${TOTAL_MEMORY} \
    -s EXPORTED_FUNCTIONS='["_init","_decode","_getBufferAddress","_isInit"]' \
    -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap","addFunction"]' \
    -s INLINING_LIMIT=1 \
    -s ASSERTIONS=1 \
    -s ALLOW_TABLE_GROWTH
  • emcc xxxx 链接库 链接源码,以及链接编译好的静态库(.a) 可按需链接引入

  • -s TOTAL_MEMORY要确保该值是64kb到倍数

  • -o library.js 输出的js文件 命名为library.js

  • -s EXPORTED_FUNCTIONS 需要导出可供js使用的函数列表

  • -s EXTRA_EXPORTED_RUNTIME_METHODS 需要到处的运行时可用的函数,这个是工具链可提供的函数,其中ccall可以用来调用c语言导出的函数addFunction比较重要,可以用来定义回调函数在c语言中调用回传到js中

8、JS渲染输出图像

获取解码数据,转换rgb赋值到ImageData对象通过canvas进行渲染画面

<!DOCTYPE html>
<html lang="en">
​
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #myCanvas {
            background-color: #000000;
        }
    </style>
</head>
​
<body>
    <!-- 引入生成的library.js 引入本地文件socketio库-->
    <script src="./socketio.js"></script>
    <script src="./library.js"></script>
    <canvas id="myCanvas"></canvas>
</body>
<script>
    let canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext('2d');
    setTimeout(() => {
        const socket = io("ws://30.37.82.1:10086");
            socket.on('connect', function () {
                //连接成功
            });
            //订阅服务端消息
            socket.on('message1', function (data,width,height) {
                console.log(width,height);
                //连接成功
                if(Module._isInit()<1) {
                //初始化
                    Module._init(width,height);
                    
                }
                var messageData = new Uint8Array(data);
                let dst = Module._getBufferAddress(messageData.length);
                Module.HEAPU8.set(messageData, dst);
                Module['_decode'](messageData.length, [addFunction(function (data) {
                    let rgb24 = Module.HEAPU8.subarray(data, data + width * height * 3);
                    drawImage(width,height,rgb24);
                }, 'vi').toString()]);
            });
​
            socket.on('disconnect', function (data) {
                //连接断开
            });
​
    }, 1000);
​
    function drawImage(width, height, imageBuffer) {
        ctx = canvas.getContext('2d');
        canvas.width = width;
        canvas.height = height;
        let imageData = ctx.createImageData(width, height);
        let j = 0;
        for (let i = 0; i < imageBuffer.length; i++) {
            if (i && i % 3 == 0) {
                imageData.data[j] = 255; 
                j += 1;
            }
            imageData.data[j] = imageBuffer[i]; 
            j += 1;
        }
        ctx.putImageData(imageData, 0, 0, 0, 0, width, height);
    }
</script>
​
</html>

服务端传输来的data数据需要转换成Uint8Array类型才能正确的传输到C层 Module是编译到JS暴露出来的模块对象,在这个对象下面存在着导出的c函数 调用方式:

  • Module.xxxx

  • Module['xxxxx']

_getBufferAddress() 对应着c层的getBufferAddress()返回一个存储H.264的内存地址给JS Module.HEAPU8.set(messageData, dst) 其中HEAPU8是工具链默认编译好暴露出的方法用于内存操作,这里是将h.264数据存储到dst的首地址开始的内存中,这样做c层就能拿到js传入的数据

9、从 C 调用 JavaScript 函数作为函数指针

Module['_decode'](messageData.length, [addFunction(function (data) { let rgb24 = Module.HEAPU8.subarray(data, data + width * height * 3); drawImage(width,height,rgb24); }, 'vi').toString()]);

这段代码首先调用c层的导出的解码函数decode传入第一个参数:数据大小length,第二个参数比较有意思,他是官方提供的回调函数写法和调用方式,是一个列表,依次对应,在c层的回调是这样的(*func)(uint8_t *data)这里就对应的function (data){} addFunction 是将函数列表包裹起来传送,另外需要传入第二个参数,即 Wasm 函数签名字符串。签名字符串中的每个字符都代表一种类型。第一个字符表示函数的返回类型,其余字符用于参数类型。

'v': 空类型

'i':32位整数类型

'j':64位整数类型(目前在JavaScript中不存在)

'f':32位浮点型

'd':64位浮点型 然后将function回调转换成toString类型 Module.HEAPU8.subarray(data, data + width * height * 3);工具链提供的获取c层传回的内存指针数据 c层回调中返回的data类型是uint_8* 指针也就是返回的首地址,将首地址传入subarray,第二个参数是结尾地址,因为rgb数据当然是w * h * 3 的数据大小所以第二个参数就是data+width * height * 3

接下来就是拿到数据之后的drawImage绘制到Canvas中

10、效果

 

原文链接:音视频(7)WebAssembly之客户端与网页进行画面实时传输实现简易1对1视频-Web端 - 掘金

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值