WebAssembly 技术 _ 加载 ffmpeg 在 Web 端调用解码
- 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的数据拷贝到buf
int 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 文件载入测试。获取一帧图片。
版权声明: 本文为 InfoQ 作者【DS小龙哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/1c12bbdf577ad18e8caf7b525】。文章转载请联系作者。
DS小龙哥
之所以觉得累,是因为说的比做的多。 2022.01.06 加入
熟悉C/C++、51单片机、STM32、Linux应用开发、Linux驱动开发、音视频开发、QT开发. 目前已经完成的项目涉及音视频、物联网、智能家居、工业控制领域
评论