写点什么

WebAssembly 技术 _ 加载 ffmpeg 在 Web 端调用解码

作者:DS小龙哥
  • 2022 年 5 月 05 日
  • 本文字数:13969 字

    阅读完需:约 46 分钟

1. 前言

WebAssembly 就是运行在 Web 平台上的 Assembly。Assembly 是指汇编代码,是直接操作 CPU 的指令代码,比如 x86 指令集上的汇编代码有指令集、寄存器、栈等等设计,CPU 根据汇编代码的指导进行运算。汇编代码相当于 CPU 执行的机器码能够转换成的人类适合读的一种语言。


Wasm 的技术优势:性能高效:WASM 采用二进制编码,在程序执行过程中的性能优越;存储成本低:相对于文本格式,二进制编码的文本占用的存储空间更小;多语言支持:用户可以使用 C/C++/RUST/Go 等多种语言编写智能合约并编译成 WASM 格式的字节码;


2. emcc 编译的 ffmpeg 静态库

(1)CSDN 上的下载地址


下载地址: https://download.csdn.net/download/xiaolong1126626497/82868215


(2)GitHub 仓库下载地址


https://github.com/wang-bin/avbuild


https://sourceforge.net/projects/avbuild/files/


https://sourceforge.net/projects/avbuild/files/wasm/


(3)这里有编译好的 ffmpeg.wasm 文件,前端 JS 可以直接调用完成视频转码等功能https://github.com/ffmpegwasm/ffmpeg.wasm


const fs = require('fs');const { createFFmpeg, fetchFile } = require('@ffmpeg/ffmpeg');
const ffmpeg = createFFmpeg({ log: true });
(async () => { await ffmpeg.load(); ffmpeg.FS('writeFile', 'test.avi', await fetchFile('./test.avi')); await ffmpeg.run('-i', 'test.avi', 'test.mp4'); await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4')); process.exit(0);})();
复制代码


(4)ffmpeg 编译 wasm 文件的源码,可以自行编译 wasm 文件:https://github.com/ffmpegwasm/ffmpeg.wasm-core

3. 调用 ffmpeg 库-打印版本号

3.1 准备 ffmpeg 库文件

3.2 编写 C 语言代码

下面只是编写了一个打印版本号的函数,用于测试 ffmpeg 的库和相关函数是否可以正常调用。


#include <stdio.h>#include <stdlib.h>#include <string.h>#include <libavcodec/avcodec.h>#include <libavformat/avformat.h>#include <libavutil/imgutils.h>#include <libswresample/swresample.h>#include <emscripten/emscripten.h>#include <libavcodec/version.h>
//获取版本号void print_version(){ unsigned codecVer = avcodec_version(); int ver_major, ver_minor, ver_micro; ver_major = (codecVer >> 16) & 0xff; ver_minor = (codecVer >> 8) & 0xff; ver_micro = (codecVer) & 0xff; printf("当前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);
}
复制代码

3.3 编译生成 wasm 和 js 文件

emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a  ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_print_version']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js
复制代码


编译成功后生成的 wasm 和 js 文件:


3.3 编写 index.html 代码

编写 HTML 文件调用 js 文件里的接口。


<!doctype html><html lang="en-us">  <head>    <meta charset="utf-8">    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">    <title>js调用c语言函数示例</title>  </head>    <body>        <script type='text/javascript'>       function run1()    {    _print_version();    }        </script>    <input type="button" value="打印版本号" onclick="run1()" />    <script async type="text/javascript" src="ffmpeg_decoder.js"></script>  </body></html>
复制代码

3.4 开启服务器

cmd 命令行运行 python,开启 http 服务器。


python -m http.server
复制代码

3.5 访问测试

打开谷歌浏览器,输入http://127.0.0.1:8000/index.html地址,按下 F12 打开控制台,点击页面上的按钮看控制台输出。



完成调用,已成功打印版本号。

4. 调用 ffmpeg 库-解码视频信息

wasm 编译的 ffmpeg 代码,不能使用avformat_open_input 直接打开文件地址,打开网络地址,只能从内存中读取数据进行解码。前端 js 加载了本地磁盘文件后,需要通过内存方式传递给 wasm-ffmpeg 接口里,然后 ffmpeg 再进行解码。


下面 C 语言代码里演示了调用 ffmpeg 解码内存里视频文件过程,解码读取分辨率、总时间,解帧数据等。代码只是为了演示如何调用 ffmpeg 的测试代码,代码比较简单,只是解码了第一帧数据,得到了 YUV420P 数据,然后保存在文件中。

4.1 编写 C 语言代码

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <libavcodec/avcodec.h>#include <libavformat/avformat.h>#include <libavutil/imgutils.h>#include <libswresample/swresample.h>#include <emscripten/emscripten.h>#include <libavcodec/version.h>
//EMSCRIPTEN_KEEPALIVE
/*存储视频文件到磁盘参数:char *name 文件名称char *buf 写入的数据unsigned int len 写入长度*/int write_file(char *name, char *buf, unsigned int len){ //创建文件 FILE *new_fp = fopen(name, "wb"); if (new_fp == NULL) { printf("%s 文件创建失败.\n", name); return -1; } else { printf("%s 文件创建成功.\n", name); } //写入磁盘 int cnt = fwrite(buf, 1, len, new_fp); printf("成功写入=%d 字节\n", cnt);
//关闭文件 fclose(new_fp);
return cnt;}

/*获取文件大小*/long get_FileSize(char *name){ /*1. 打开文件*/ FILE *fp = fopen(name, "rb"); if (fp == NULL) { printf("% 文件不存在.\n", name); return -1; } /*2. 将文件指针偏移到文件结尾*/ fseek(fp, 0, SEEK_END);
/*3. 获取当前文件指针距离文件头的字节偏移量*/ long byte = ftell(fp);
/*4. 关闭文件*/ fclose(fp);
return byte;}

/*读文件char *buf*/unsigned char *read_file(char *name){ //创建文件 FILE *fp = fopen(name, "rb"); if (fp == NULL) { printf("%s 文件打开失败.\n", name); return -1; }
//获取文件大小 int size = get_FileSize(name);
//申请空间 unsigned char *buf = (unsigned char *)malloc(size); if (buf == NULL) { printf("空间申请失败:%d byte.\n", size); return NULL; } //读取文件到内存 int cnt = fread(buf, 1, size, fp); printf("成功读取=%d 字节\n", cnt);
//关闭文件 fclose(fp);
return buf;}

//获取版本号void print_version(){ unsigned codecVer = avcodec_version(); int ver_major, ver_minor, ver_micro; ver_major = (codecVer >> 16) & 0xff; ver_minor = (codecVer >> 8) & 0xff; ver_micro = (codecVer) & 0xff; printf("当前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);
}

int ffmpeg_laliu_run_flag = 1;
/*功能: 这是FFMPEG回调函数,返回1表示超时 0表示正常ffmpeg阻塞完成一些任务的时候,可以快速强制退出.*/static int interrupt_cb(void *ctx){ if (ffmpeg_laliu_run_flag == 0)return 1; return 0;}

//存放视频解码的详细信息struct M_VideoInfo{ int64_t duration; int video_width; int video_height;};
struct M_VideoInfo m_VideoInfo;

//读取数据的回调函数------------------------- //AVIOContext使用的回调函数! //注意:返回值是读取的字节数 //手动初始化AVIOContext只需要两个东西:内容来源的buffer,和读取这个Buffer到FFmpeg中的函数 //回调函数,功能就是:把buf_size字节数据送入buf即可 //第一个参数(void *opaque)一般情况下可以不用
/*正确方式*/struct buffer_data{ uint8_t *ptr; /* 文件中对应位置指针 */ size_t size; ///< size left in the buffer /* 文件当前指针到末尾 */};
// 重点,自定的buffer数据要在外面这里定义struct buffer_data bd = { 0 };
//用来将内存buffer的数据拷贝到bufint read_packet(void *opaque, uint8_t *buf, int buf_size){
buf_size = FFMIN(buf_size, bd.size);
if (!buf_size) return AVERROR_EOF; printf("ptr:%p size:%zu bz%zu\n", bd.ptr, bd.size, buf_size);
/* copy internal buffer data to buf */ memcpy(buf, bd.ptr, buf_size); bd.ptr += buf_size; bd.size -= buf_size;
return buf_size;}

//ffmpeg解码使用的全局变量unsigned char * iobuffer;AVFormatContext * format_ctx;int video_width = 0;int video_height = 0;int video_stream_index = -1;char* video_buffer;

/*函数功能: 初始化解码环境函数参数:unsigned char *buf 视频文件的内存地址unsigned int len 视频文件长度*/ int initDecoder(unsigned char *buf,unsigned int len){ int ret = 0;
bd.ptr = buf; /* will be grown as needed by the realloc above */ bd.size = len; /* no data at this point */
//注册ffmpeg av_register_all();
unsigned int version = avformat_version(); printf("ffmpeg版本: %d\r\n",version);
// Allocate an AVFormatContext format_ctx = avformat_alloc_context(); if (format_ctx == NULL) { printf("avformat_alloc_context 失败.\n"); return -1; }
iobuffer = (unsigned char *)av_malloc(32768); AVIOContext *avio = avio_alloc_context(iobuffer, 32768, 0, NULL, read_packet, NULL, NULL); format_ctx->pb = avio; ret = avformat_open_input(&format_ctx, "nothing", NULL, NULL);
format_ctx->interrupt_callback.callback = interrupt_cb; //--------注册回调函数
AVDictionary* options = NULL;
//ret = avformat_open_input(&format_ctx, url, NULL, NULL); if (ret != 0) { char buf[1024]; av_strerror(ret, buf, 1024); printf("无法打开视频内存,return value: %d \n",ret); return -1; }
printf("正在读取媒体文件的数据包以获取流信息.\n");
// 读取媒体文件的数据包以获取流信息 ret = avformat_find_stream_info(format_ctx, NULL); if (ret < 0) { printf("无法获取流信息: %d\n",ret); return -1; }
AVCodec *video_pCodec; // audio/video stream index printf("视频中流的数量: %d\n",format_ctx->nb_streams); printf("视频总时间:%lld 秒\n",format_ctx->duration / AV_TIME_BASE);

//得到秒单位的总时间 m_VideoInfo.duration = format_ctx->duration / AV_TIME_BASE;
for (int i = 0; i < format_ctx->nb_streams; ++i) { const AVStream* stream = format_ctx->streams[i]; printf("编码数据的类型: %d\n",stream->codecpar->codec_id);
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { //查找解码器 video_pCodec = avcodec_find_decoder(AV_CODEC_ID_H264); //打开解码器 int err = avcodec_open2(stream->codec, video_pCodec, NULL); if (err != 0) { printf("H264解码器打开失败.\n"); return 0; } video_stream_index = i; //得到视频帧的宽高 video_width = stream->codecpar->width; video_height = stream->codecpar->height; //保存宽和高 m_VideoInfo.video_height = video_height; m_VideoInfo.video_width = video_width;
//解码后的YUV数据存放空间 video_buffer = malloc(video_height * video_width * 3 / 2);
printf("视频帧的尺寸(以像素为单位): (宽X高)%dx%d 像素格式: %d\n", stream->codecpar->width,stream->codecpar->height,stream->codecpar->format); } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { } }
if (video_stream_index == -1) { printf("没有检测到视频流.\n"); return -1; }
printf("初始化成功.\n"); return 0;}

//获取视频总时长int64_t GetVideoDuration(){ return m_VideoInfo.duration;}
//获取视频宽int64_t GetVideoWidth(){ return m_VideoInfo.video_width;}
//获取视频高int64_t GetVideoHeight(){ return m_VideoInfo.video_height;}

//获取视频帧//传入参数时间单位--秒unsigned char *GetVideoFrame(int time){ AVPacket pkt; double video_clock; AVFrame *SRC_VIDEO_pFrame = av_frame_alloc();
printf("开始解码.\n");
printf("跳转状态:%d\n",av_seek_frame(format_ctx, -1, time*AV_TIME_BASE, AVSEEK_FLAG_ANY));
while (1) { int var = av_read_frame(format_ctx, &pkt); //读取一帧数据 if (var < 0) { printf("数据读取完毕:%d\n", var); break; }
printf("开始..\n"); //如果是视频流节点 if (pkt.stream_index == video_stream_index) { //当前时间 video_clock = av_q2d(format_ctx->streams[video_stream_index]->time_base) * pkt.pts; printf("pkt.pts=%0.2f,video_clock=%0.2f\n", pkt.pts, video_clock);
//解码视频 frame //发送视频帧 if (avcodec_send_packet(format_ctx->streams[video_stream_index]->codec, &pkt) != 0) { av_packet_unref(&pkt);//不成功就释放这个pkt continue; }
//接受后对视频帧进行解码 if (avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame) != 0) { av_packet_unref(&pkt);//不成功就释放这个pkt continue; }
//转格式 /* sws_scale(img_convert_ctx, (uint8_t const **)SRC_VIDEO_pFrame->data, SRC_VIDEO_pFrame->linesize, 0,video_height, RGB24_pFrame->data, RGB24_pFrame->linesize);*/
memset(video_buffer, 0, video_height * video_width * 3 / 2); int height = video_height; int width = video_width;
printf("decode video ok\n"); int a = 0, i; for (i = 0; i < height; i++) { memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[0] + i * SRC_VIDEO_pFrame->linesize[0], width); a += width; } for (i = 0; i < height / 2; i++) { memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[1] + i * SRC_VIDEO_pFrame->linesize[1], width / 2); a += width / 2; } for (i = 0; i < height / 2; i++) { memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[2] + i * SRC_VIDEO_pFrame->linesize[2], width / 2); a += width / 2; }
//保存在文件中: //write_file("./666.yuv", video_buffer, video_height * video_width * 3 / 2);
printf("退出成功....\n"); break; }
//释放包 av_packet_unref(&pkt); } av_free(SRC_VIDEO_pFrame); return video_buffer;}

//销毁内存void DeleteMemory(){ //释放空间 av_free(iobuffer);}
复制代码

4.2 编译生成 wasm 和 js 文件

emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a  ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_initDecoder','_write_file','_print_version','_get_FileSize','_read_file','_GetVideoFrame','_GetVideoWidth','_GetVideoDuration','_GetVideoHeight','_DeleteMemory']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js
复制代码


编译成功后生成的 wasm 和 js 文件:


4.3 编写 index.html 代码

完成了视频选择,播放,调用了 C 语言编写的接口完成解码返回,但是没有渲染。


<!doctype html><html lang="en-us">  <head>    <meta charset="utf-8">    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">    <title>js调用c语言函数示例</title>  </head>    <body>        <input id="myfile" type="file"/>  <video id="output-video" width="300" controls></video>   <div><canvas id="glcanvas" width="640" height="480"></canvas></div>     <script>        //代码摘自:https://github.com/ivan-94/video-push/blob/master/yuv/index.html#L312          const video = document.getElementById('glcanvas');          let renderer;                class WebglScreen {              constructor(canvas) {              this.canvas = canvas;              this.gl =                  canvas.getContext('webgl') ||                  canvas.getContext('experimental-webgl');              this._init();              }
_init() { let gl = this.gl; if (!gl) { console.log('gl not support!'); return; }
// 图像预处理 gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); // GLSL 格式的顶点着色器代码 let vertexShaderSource = ` attribute lowp vec4 a_vertexPosition; attribute vec2 a_texturePosition; varying vec2 v_texCoord; void main() { gl_Position = a_vertexPosition; v_texCoord = a_texturePosition; } `;
let fragmentShaderSource = ` precision lowp float; uniform sampler2D samplerY; uniform sampler2D samplerU; uniform sampler2D samplerV; varying vec2 v_texCoord; void main() { float r,g,b,y,u,v,fYmul; y = texture2D(samplerY, v_texCoord).r; u = texture2D(samplerU, v_texCoord).r; v = texture2D(samplerV, v_texCoord).r; fYmul = y * 1.1643828125; r = fYmul + 1.59602734375 * v - 0.870787598; g = fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375; b = fYmul + 2.01723046875 * u - 1.081389160375; gl_FragColor = vec4(r, g, b, 1.0); } `;
let vertexShader = this._compileShader( vertexShaderSource, gl.VERTEX_SHADER, ); let fragmentShader = this._compileShader( fragmentShaderSource, gl.FRAGMENT_SHADER, );
let program = this._createProgram(vertexShader, fragmentShader);
this._initVertexBuffers(program);
// 激活指定的纹理单元 gl.activeTexture(gl.TEXTURE0); gl.y = this._createTexture(); gl.uniform1i(gl.getUniformLocation(program, 'samplerY'), 0);
gl.activeTexture(gl.TEXTURE1); gl.u = this._createTexture(); gl.uniform1i(gl.getUniformLocation(program, 'samplerU'), 1);
gl.activeTexture(gl.TEXTURE2); gl.v = this._createTexture(); gl.uniform1i(gl.getUniformLocation(program, 'samplerV'), 2); } /** * 初始化顶点 buffer * @param {glProgram} program 程序 */
_initVertexBuffers(program) { let gl = this.gl; let vertexBuffer = gl.createBuffer(); let vertexRectangle = new Float32Array([ 1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0, ]); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 向缓冲区写入数据 gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW); // 找到顶点的位置 let vertexPositionAttribute = gl.getAttribLocation( program, 'a_vertexPosition', ); // 告诉显卡从当前绑定的缓冲区中读取顶点数据 gl.vertexAttribPointer( vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0, ); // 连接vertexPosition 变量与分配给它的缓冲区对象 gl.enableVertexAttribArray(vertexPositionAttribute);
let textureRectangle = new Float32Array([ 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, ]); let textureBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer); gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW); let textureCoord = gl.getAttribLocation(program, 'a_texturePosition'); gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(textureCoord); }
/** * 创建并编译一个着色器 * @param {string} shaderSource GLSL 格式的着色器代码 * @param {number} shaderType 着色器类型, VERTEX_SHADER 或 FRAGMENT_SHADER。 * @return {glShader} 着色器。 */ _compileShader(shaderSource, shaderType) { // 创建着色器程序 let shader = this.gl.createShader(shaderType); // 设置着色器的源码 this.gl.shaderSource(shader, shaderSource); // 编译着色器 this.gl.compileShader(shader); const success = this.gl.getShaderParameter( shader, this.gl.COMPILE_STATUS, ); if (!success) { let err = this.gl.getShaderInfoLog(shader); this.gl.deleteShader(shader); console.error('could not compile shader', err); return; }
return shader; }
/** * 从 2 个着色器中创建一个程序 * @param {glShader} vertexShader 顶点着色器。 * @param {glShader} fragmentShader 片断着色器。 * @return {glProgram} 程序 */ _createProgram(vertexShader, fragmentShader) { const gl = this.gl; let program = gl.createProgram();
// 附上着色器 gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader);
gl.linkProgram(program); // 将 WebGLProgram 对象添加到当前的渲染状态中 gl.useProgram(program); const success = this.gl.getProgramParameter( program, this.gl.LINK_STATUS, );
if (!success) { console.err( 'program fail to link' + this.gl.getShaderInfoLog(program), ); return; }
return program; }
/** * 设置纹理 */ _createTexture(filter = this.gl.LINEAR) { let gl = this.gl; let t = gl.createTexture(); // 将给定的 glTexture 绑定到目标(绑定点 gl.bindTexture(gl.TEXTURE_2D, t); // 纹理包装 参考https://github.com/fem-d/webGL/blob/master/blog/WebGL基础学习篇(Lesson%207).md -> Texture wrapping gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // 设置纹理过滤方式 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); return t; }
/** * 渲染图片出来 * @param {number} width 宽度 * @param {number} height 高度 */ renderImg(width, height, data) { let gl = this.gl; // 设置视口,即指定从标准设备到窗口坐标的x、y仿射变换 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); // 设置清空颜色缓冲时的颜色值 gl.clearColor(0, 0, 0, 0); // 清空缓冲 gl.clear(gl.COLOR_BUFFER_BIT);
let uOffset = width * height; let vOffset = (width >> 1) * (height >> 1);
gl.bindTexture(gl.TEXTURE_2D, gl.y); // 填充纹理 gl.texImage2D( gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data.subarray(0, uOffset), );
gl.bindTexture(gl.TEXTURE_2D, gl.u); gl.texImage2D( gl.TEXTURE_2D, 0, gl.LUMINANCE, width >> 1, height >> 1, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data.subarray(uOffset, uOffset + vOffset), );
gl.bindTexture(gl.TEXTURE_2D, gl.v); gl.texImage2D( gl.TEXTURE_2D, 0, gl.LUMINANCE, width >> 1, height >> 1, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data.subarray(uOffset + vOffset, data.length), );
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
/** * 根据重新设置 canvas 大小 * @param {number} width 宽度 * @param {number} height 高度 * @param {number} maxWidth 最大宽度 */ setSize(width, height, maxWidth) { let canvasWidth = Math.min(maxWidth, width); this.canvas.width = canvasWidth; this.canvas.height = (canvasWidth * height) / width; }
destroy() { const { gl } = this;
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT, ); } } // end of webgl const initialCanvas = (canvas, width, height) => { canvas.width = width; canvas.height = height; return new WebglScreen(canvas); };
const render = (buff,width,height) => { if (renderer == null) { return; } renderer.renderImg(width, height, buff); }; </script> <script type='text/javascript'> function run1() { } function run2() { } //加载本地文件 var file=document.getElementById("myfile"); file.onchange=function(event){ let fileReader = new FileReader(); fileReader.onload = function(){ // 当 FileReader 读取文件时候,读取的结果会放在 FileReader.result 属性中 var fileArray= this.result; console.log(fileArray); let fileBuffer = new Uint8Array(this.result); console.log(fileBuffer); //申请空间 var fileBufferPtr = _malloc(fileBuffer.length) //将fileBuffer里的内容拷贝到fileBufferPtr里 Module.HEAP8.set(fileBuffer,fileBufferPtr) //1. 写文件 //申请空间,存放字符串 //var name = allocate(intArrayFromString("./tmp.mp4"), ALLOC_NORMAL); //var run_var=_write_file(name,fileBufferPtr,fileBuffer.length); //console.log('写文件成功字节数:',run_var); //2. 获取文件大小 //var file_size=_get_FileSize(name); //console.log('获取文件大小:',file_size); //const data = ffmpeg.FS('readFile', 'output.mp4');
//3. 读取文文件 //const data = _read_file(name); // const video = document.getElementById('output-video'); //video.src = URL.createObjectURL(new Blob([fileBuffer.buffer], { type: 'video/mp4' })); //加载内存数据 // Module.HEAPU8.subarray(imgBufferPtr, data);
//4. 初始化解码器,加载文件 _initDecoder(fileBufferPtr,fileBuffer.length); //5. 获取总时间 var time=_GetVideoDuration(); console.log('视频总时间:'+time); //6. 获取视频宽 var Width=_GetVideoWidth(); console.log('视频宽:'+Width); //7. 获取视频高 var Height=_GetVideoHeight(); console.log('视频高:'+Height); renderer = initialCanvas(video,Width,Height); //申请空间,存放字符串 //var name_file = allocate(intArrayFromString("./666.yuv"), ALLOC_NORMAL);
//读取文件 //var yuv_wasm_data=_read_file(name_file); //8. 获取视频帧 var yuv_wasm_data=_GetVideoFrame(10); var renderlength=Width*Height*3/2; var RenderBuffer = new Uint8Array (Module.HEAPU8.subarray(yuv_wasm_data,yuv_wasm_data + renderlength + 1) ); console.log(RenderBuffer); render(RenderBuffer,Width,Height); }; fileReader.readAsArrayBuffer(this.files[0]); } </script>
<input type="button" value="载入文件初始化解码器" onclick="run1()" /> <script async type="text/javascript" src="ffmpeg_decoder.js"></script> </body></html>
复制代码

4.4 开启服务器

命令行运行命令,开启 HTTP 服务器,方便测试:


python -m http.server
复制代码

4.5 测试效果

打开谷歌浏览器,输入http://127.0.0.1:8000/index.html地址,按下 F12 打开控制台,点击页面上的按钮看控制台输出。


(1)输入地址,打开网页



(2)按下 F12,打开控制台



(3)选择一个 MP4 文件载入测试。获取一帧图片。



发布于: 刚刚阅读数: 2
用户头像

DS小龙哥

关注

之所以觉得累,是因为说的比做的多。 2022.01.06 加入

熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域

评论

发布
暂无评论
WebAssembly技术_加载ffmpeg在Web端调用解码_5月月更_DS小龙哥_InfoQ写作社区