暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Vue+WebAssembly | 无服务端 录屏、转码、转GIF

技术源share 2023-01-13
821

wasm
,全称 WebAssembly
,官网描述:一种用于基于堆栈的虚拟机的二进制指令格式。wasm
被设计为一个可移植的目标,用于编译C/c++/Rust
等高级语言,支持在 web 上部署客户端和服务器应用程序。wasm
的开发者参考文档可以在 MDN[1] 找到。

用大白话讲就是你用 C/C++/Rust
等语言写的代码,编译后(这个文件一般以 wasm 结尾)得到汇编指令,然后通过JavaScript
相关 API
配合将该文件加载到 Web 容器中,字面理解 WebAssembly
就是运行在 Web 容器
里的 Assembly
(汇编),你可以理解为一种技术就行,其他的不用纠结。既然要用它那必然是因为它有自己独特的优势:即提供了一种以接近本地速度在 Web 上运行以多种语言编写的代码的方法,尤其是涉及到 CPU
或者GPU
计算的时候,比如:媒体编解码
深度学习计算
图像处理
等场景。而本文中我们就以媒体操作
为例,看看能玩出什么花样。

演示

在线演示地址[2]

宽带比较低,多等一会等 wasm 文件加载完毕即可体验哦。

GIF.gif

预备知识

1.wasm
文件在浏览器中单独是无法玩的,既然是浏览器那必然离不开JavaScript
相关的 API
,因为我们在使用现成的一些编译好的模块的时候一定会有初始化的步骤。

2.对于 WebAssembly
而言,它最终成型的文件就是底层代码,既然是代码那就可以操作内存,相关内存限制为 2-4G,相关文档[3],但是对于我们使用者而言,内存和我们自己的机器是相关的,如果你的电脑只有 4G 内存,而且因为其他的程序已经占用了大部分,那么对于 wasm 代码而言,可以操作的内存就很少了。

实战

这里我们用已经编译好的包 FFmpeg.wasm
,去实现我们 Web 端的媒体编辑,FFmpeg 官网地址[4]。首先我们了解下这个包能干什么?

  • 浏览器内存中直接操作文件系统(文件系统接口文档[5])。
  • 支持原生 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")
}

加载成功则如下显示:

1.png

文件操作

因为涉及到转码,那么必然是要对浏览器产生的相关文件进行所谓的 “本地化”操作,拿到二进制流之后,通过文件系统的 API 去本地化到内存文件系统(MEMFS)中,这里的 API 并不是FFmpeg.wasm
自己的,而是它去调用了相关的 API[6]去实现了这个目的,详细的我们不再阐述,因为里面东西还是比较多的。

以本文 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

2.png

视频转 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')

3.png

获取录屏的二进制数据并本地化

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
}

完整代码

完整代码[7]

最后

  • 上面涉及到了和摄像头、流媒体相关的,如果大家对 WebRTC 感兴趣的话可以看看掘金小册使用 WebRTC 搭打造私有化直播会议系统[8]
  • 有问题评论区一起讨论

参考资料

[1]

MDN: https://developer.mozilla.org/en-US/docs/WebAssembly

[2]

在线演示地址: https://lav.wangsrbus.cn/

[3]

相关文档: https://v8.dev/blog/4gb-wasm-memory#:~:text=Thanks%20to%20recent%20work%20in%20Chrome%20and%20Emscripten%2C,That%E2%80%99s%20up%20from%20the%20previous%20limit%20of%202GB.

[4]

FFmpeg官网地址: https://github.com/ffmpegwasm/ffmpeg.wasm

[5]

文件系统接口文档: https://emscripten.org/docs/api_reference/Filesystem-API.html

[6]

相关的API: https://emscripten.org/docs/api_reference/Filesystem-API.html

[7]

完整代码: https://gist.github.com/wangsrGit119/46380e8bedc48157037c4ca622b7f30e

[8]

使用WebRTC搭打造私有化直播会议系统: https://juejin.cn/book/7168418382318927880


文章转载自技术源share,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论