WebAssembly技术_加载ffmpeg在Web端调用解码
【摘要】 WebAssembly 就是运行在 Web 平台上的 Assembly。Assembly 是指汇编代码,是直接操作 CPU 的指令代码,比如 x86 指令集上的汇编代码有指令集、寄存器、栈等等设计,CPU 根据汇编代码的指导进行运算。汇编代码相当于 CPU 执行的机器码能够转换成的人类适合读的一种语言。
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文件载入测试。获取一帧图片。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
yd_2237822662023/11/09 08:39:431楼编辑删除举报