wasm
,全称 WebAssembly
,官网描述: 一种用于基于堆栈的虚拟机的二进制指令格式。wasm
被设计为一个可移植的目标,用于编译C/c++/Rust
等高级语言,支持在web上部署客户端和服务器应用程序。wasm
的开发者参考文档可以在 MDN 找到。
用大白话讲就是你用 C/C++/Rust
等语言写的代码,编译后(这个文件一般以wasm结尾)得到汇编指令,然后通过JavaScript
相关 API
配合将该文件加载到Web容器中,字面理解 WebAssembly
就是运行在 Web 容器
里的 Assembly
(汇编),你可以理解为一种技术就行,其他的不用纠结。
既然要用它那必然是因为它有自己独特的优势:即提供了一种以接近本地速度在 Web 上运行以多种语言编写的代码的方法,尤其是涉及到 CPU
或者GPU
计算的时候,比如:媒体编解码
、深度学习计算
、图像处理
等场景。而本文中我们就以媒体操作
为例,看看能玩出什么花样。
演示
宽带比较低,多等一会等wasm文件加载完毕即可体验哦。
预备知识
1.wasm
文件在浏览器中单独是无法玩的,既然是浏览器那必然离不开JavaScript
相关的 API
,因为我们在使用现成的一些编译好的模块的时候一定会有初始化的步骤。
2.对于 WebAssembly
而言,它最终成型的文件就是底层代码,既然是代码那就可以操作内存,相关内存限制为2-4G,相关文档,但是对于我们使用者而言,内存和我们自己的机器是相关的,如果你的电脑只有4G内存,而且因为其他的程序已经占用了大部分,那么对于wasm代码而言,可以操作的内存就很少了。
实战
这里我们用已经编译好的包 FFmpeg.wasm
,去实现我们Web端的媒体编辑,FFmpeg官网地址。
首先我们了解下这个包能干什么?
- 浏览器内存中直接操作文件系统(文件系统接口文档)。
- 支持原生 FFmpeg 的指令,重点。
- 编解码支持 h.265/264等,重点。
我们在本文中用到的就是前面2条,文件操作和FFmpeg原生指令执行,接下来看看我们要达成的目标。
- 视频转码
- 视频转Gif
- 浏览器文件快捷操作
wasm代码加载
初始化加载实例并加载wasm代码文件,这里我们并没有看到怎么去加载wasm代码的,实际上我们使用的这个组件给我们封装好了,内部通过
WebAssembly
模块的相关JavaScript的 API加载 wasm 汇编码然后去初始化。
const ffmpeg = createFFmpeg({
log: true,//打开日志
progress:p=>{console.log(p)},//回调 展示进度
corePath: new URL('assets/f-core/ffmpeg-core.js', document.location).href,//本地离线wasm代码文件
});
async function init(){
await ffmpeg.load();
console.log("ffmpeg loaded")
}
加载成功则如下显示:
文件操作
因为涉及到转码,那么必然是要对浏览器产生的相关文件进行所谓的 “本地化”操作,拿到二进制流之后,通过文件系统的 API 去本地化到内存文件系统(MEMFS)中,这里的API并不是FFmpeg.wasm
自己的,而是它去调用了相关的API去实现了这个目的,详细的我们不再阐述,因为里面东西还是比较多的。
以本文Demo操作为例
- 获取文件二进制流
// file 可以为连接文件 也可以为 二进制的数据
let data = await fetchFile(this.file)
- 写入MEMFS
this.ffmpeg.FS("writeFile", "suke.mp4", data);
- 读取文件到浏览器中
//读取视频数据
let memfsData = ffmpeg.FS('readFile', 'suke.mp4');
//创建虚拟URL
this.transcodeUrl = URL.createObjectURL(new Blob([memfsData.buffer], { type: "video/mp4" }));
- 文件系统中删除数据
ffmpeg.FS('unlink', 'suke.mp4');
媒体操作
将相关文件本地化到文件系统之后,我们就可以利用 ffmpeg的原生命令去执行转码操作,比如将mp4格式转为avi的
//本地化文件
this.ffmpeg.FS("writeFile", "suke.avi", await fetchFile(this.file));
//进行转码
await this.ffmpeg.run("-i", "suke.avi", "suke.mp4");
//转码完成后读取数据
const data = this.ffmpeg.FS("readFile", "suke.mp4");
//获取页面DOM实例,然后挂载虚拟URL
const video = document.getElementById('playerForTransf');
this.transcodeUrl = URL.createObjectURL(
new Blob([data.buffer], { type: "video/mp4" })
);
video.src = this.transcodeUrl
视频转GIF
这个操作实际上和上一步一样,都是执行的原生ffmpeg的命令,然后输出GIF文件,展示并下载
//GIF时长
let time = this.formForGifParams.time+""
//GIF的FPS
let fps = this.formForGifParams.fps
//GIF的清晰度
let vh = this.formForGifParams.scale
let params = `fps=${fps},scale=${vh}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`
//this.chunks 为本地录制屏幕的二进制数据 也可以和文件连接一样 本地化到文件系统中
this.ffmpeg.FS("writeFile", "out.mp4", await fetchFile((this.chunks)));
//转换为GIF操作
await this.ffmpeg.run("-t", time, "-i", "out.mp4", "-vf",params, "-loop", "0" ,"output.gif");
//读取GIF数据
const data = this.ffmpeg.FS("readFile", "output.gif");
const image = document.getElementById('imagePre');
//本地DOM实例中展示
image.src = URL.createObjectURL(
new Blob([data.buffer], { type: "image/gif" })
);
//操作完成后删除文件
this.ffmpeg.FS('unlink','out.mp4')
获取录屏的二进制数据并本地化
async function screenRecord(){
ElNotification({
title: '温馨提示',
message: '开始录屏,请选择要录制的窗口',
type: 'success',
})
stream = await navigator.mediaDevices.getDisplayMedia(mediaConstraints);
var options = {mimeType: recordMediaType};
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.start();
//停止录屏后触发保存
mediaRecorder.ondataavailable = async function(e) {
console.log("data available", e.data);
chunks.value = e.data
//这一步并不一定要在这里本地化到文件系统 等需要媒体操作的时候再进行最佳
this.ffmpeg.FS("writeFile", "suke.avi", await fetchFile(this.chunks));
}
recordStatus.value = mediaRecorder.state
}
获取摄像头录制的二进制数据并本地化
async function localCamRecord(){
ElNotification({
title: '温馨提示',
message: '开始摄像头画面录制',
type: 'success',
})
stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
var options = {mimeType: recordMediaType};
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.start();
//停止录屏后触发保存
mediaRecorder.ondataavailable = async function(e) {
console.log("data available", e.data);
chunks.value = e.data
//同上 按照实际情况处理
this.ffmpeg.FS("writeFile", "suke.avi", await fetchFile(this.chunks));
}
recordStatus.value = mediaRecorder.state
}
完整代码
最后
- 上面涉及到了和摄像头、流媒体相关的,如果大家对WebRTC感兴趣的话可以看看掘金小册使用WebRTC搭打造私有化直播会议系统
- 有问题评论区一起讨论
评论区