借助ffmpeg的wasm版本,可以实现本地网页处理视频、预览非mp4格式的视频等,最近用到了视频中分离音频、预览视频、截取分割等功能,简单记录下。
获取 ffmpeg.wasm
ffmpeg.wasm官方文档 ffmpegwasm.netlify.app/docs/overvi…
首先需要执行npm以便在本地生成 ffmpeg.wasm
其实我没有使用npm开发项目,只是为了得到 ffmpeg.wasm 的 umd 版本。
为简单起见,直接拉取的它的示例项目,依次执行以下几个命令
拉取示例源码 git clone https://github.com/ffmpegwasm/ffmpeg.wasm.git .
安装依赖 npm -i
进入原生js示例文件夹 cd apps/vanilla-app
下载资源 npm run download
上面命令执行完毕后,在apps/vanilla-app/public/assets
目录下会生成所有可用的ffmpeg.wasm相关资源,分为 mt
多线程版、esm版、普通版等,根据需要选择。
同时还会生成几个示例代码文件,可以直接照着模仿开干
任务一:在网页中预览mov视频
大家都知道网页原生支持的视频播放格式有限,兼容性最好的就是 mp4 H.264 编码了,比如mov视频就无法在线预览。使用ffmpeg.wasm可解决这个问题,而不必先传给后端转码。
- 上传视频, 获取到上传的视频数据
<input type="file" accept="video/*" onchange="upload(this)">
// 接受上传的视频
function upload(e){
... 忽略一些验证处理等
let file = e.target.files[0];
# 调用自定义函数,使用ffmpeg进行转码后插入页面预览
transcode(file);
}
- 加载ffmpeg并创建 ffmpeg.wasm 实例
为加快速度,可以在页面刚加载完毕后,就启动ffmpeg.wasm的加载和初始化,全局只保存一个ffmpeg实例。
const { fetchFile } = FFmpegUtil;
const { FFmpeg } = FFmpegWASM;
let ffmpeg=null;
// 只加载一次,全局变量报错
async function loadffmpeg(){
if (ffmpeg === null) {
ffmpeg = new FFmpeg();
ffmpeg.on("log", ({ message }) => {
console.log(message);
});
ffmpeg.on("progress", ({ progress, time }) => {
// 处理时的进度信息
let message = `预处理进度${parseInt(
progress * 100
)} %`;
});
# 用于加载 ffmpeg.wasm
await ffmpeg.load({
coreURL: "/static/assets/core/package/dist/umd/ffmpeg-core.js",
});
}
}
- 将视频文件读取并写入FS
使用 ffmpeg 提供的 fetchFile 方法可直接读取上传的视频文件 file 为 Unit8Array,通过 ffmpeg.writeFile 写入到本地虚拟文件系统(FS)中,input_name
可以随意指定一个名字,这个就是输入的视频文件。
let input_name='input.mov';
await ffmpeg.writeFile(input_name, await fetchFile(file));
再指定一个 outname 变量作为处理后的视频名字,比如 let outname='output.mp4'
使用 ffmpeg.exec()
方法执行转码操作,exec 里可使用的参数同原生 ffmpeg 一致。比如可以使用-c:v libx264 -c:a aac
等.
await ffmpeg.exec(['-i', name, outname]);
最后再调用 ffmpeg.readFile
读取结果视频 outname
获得 Unit8Array 数据,交给 new Blob 通过 URL.createObjectURL
生成本地url链接,就可以在线预览了。
async function transcode(file){
# 原视频
let input_name="preview.mov"
# 读取原视频文件为Unit8Array并写入到虚拟文件系统内
await ffmpeg.writeFile(input_name, await fetchFile(file));
let outname = `output.mp4`
# exec 执行ffmpeg 命令
await ffmpeg.exec(['-i', name, outname]);
# 读取结果视频为 Unit8Array
const data = await ffmpeg.readFile(outname);
# 创建预览url, 创建Blob并生成URL
let src = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }))
# 插入页面预览
let vd=document.createElement('video')
vd.controls=true;
vd.src=src;
document.body.appendChild(vd);
}
任务二:从视频中截取一段时间的视频片段
例如想截取视频里 第10秒 到 第15秒 的片段,无需传给服务器处理,使用ffmpeg.wasm就很容易
- 第一步依然是上传视频、读取并写入到虚拟文件系统,和任务一相同,就不赘述了。
- 第2步准备要执行的ffmpeg命令参数,这个任务的参数是
cmd=["-i",input_name,"-ss","00:00:10","-to","00:00:15","output.mp4"]
, 使用 ffmpeg.exec(cmd) 执行这个命令。 - 执行成功后,依然调用readFile将结果视频读入,创建URL预览或下载
const data = await ffmpeg.readFile(outname);
let src = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }))
下载链接 <a href="${src}" download="output-duration.mp4">下载</a>
其他可以做的任务非常多,几乎大部分原生ffmpeg可以执行的任务,ffmpeg.wasm都可以,音频视频合并、连接、加水印、裁剪等等。操作基本都类似,核心就是读取文件(ffmpeg.fetchFile)并写进虚拟文件系统(ffmepg.writeFile),然后ffmpeg.exec调用命令参数执行,最后再通过new Blob(data.buffer)和URL.createObjectURL创建链接,进行预览和下载。
嵌入字幕/字体
值得一提的是,如何涉及嵌入字幕时,需要预先将字体文件也读取并写入虚拟文件系统,否则会报错。
var fontFile = "./simhei.ttf";
var fontURL = "./fonts/simhei.ttf";
#同样熟悉的方法,读取字幕文件然后写入虚拟文件系统
await ffmpeg.writeFile(fontFile, await fetchFile(fontURL));
嵌入字幕时,就可以使用 -vf subtitles=
参数来使用了
subtitles=${this.srtfile['file']}:fontsdir=./:force_style='Fontname=simhei,Fontsize=18'
let cmd=[
"-y",
"-hide_banner",
"-ignore_unknown",
"-i",
this.videoFiles[id]['file'],
'-vf', `subtitles=${this.srtfile['file']}:fontsdir=./:force_style='Fontname=simhei,Fontsize=18'`,
'-c:v',
'libx264',
outname,
]
await ffmpeg.exec(cmd);
总结几个关键函数
ffmepg.readFile:
用于读取两类数据,
一类是字符串,返回也是字符串;
另一类是二进制数据,比如读取 ffmpeg.exec 执行后生成的结果文件,返回的将是 Uint8Array
数据
ffmpeg.readFile(path,encoding?, __namedParameters?): Promise<FileData>
ffmpeg.writeFile:
将数据写入到 ffmpeg.wasm 虚拟文件系统内,待写入的数据同样是两类
一类是纯文本
另一类是 Uint8Array 数据
返回完成或是失败boolean
ffmpeg.writeFile(path, data,__namedParameters?):Promise<boolean>
ffmpeg.fetchFile:
这是一个工具函数,可用于从 url、base64、上传的文件 、Blob 等各类资源中读取数据并返回 Uint8Array ,使用起来比 readFile 更方便,可以很容易的将各类数据转为ffmpeg可以处理的类型。
// URL
await fetchFile("http://localhost:3000/video.mp4");
// base64
await fetchFile("data:<type>;base64,wL2dvYWwgbW9yZ...");
// URL
await fetchFile(new URL("video.mp4", import.meta.url));
// File
fileInput.addEventListener('change', (e) => {
await fetchFile(e.target.files[0]);
});
// Blob
const blob = new Blob(...);
await fetchFile(blob);
Blob / Unit8Array / Base64 / File 互相转换
- 上传文件File转为Unit8Array
使用 ffmpeg.fetchFile 读取上传的视频文件返回 Unit8Array 类型,并调用 ffmpeg.writeFile写入到虚拟文件系统
await ffmpeg.writeFile(input_name, await fetchFile(file));
- 使用 ffmpeg.readFile 读取二进制数据,返回 Unit8Array 数据,然后生成 Blob 用于网页显示和下载
const data = await ffmpeg.readFile(outname);
URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }))
- Blob 数据转为 Unit8Array 数据
ffmpeg.fetchFile(blob)
或
const reader = new FileReader()
reader.readAsArrayBuffer(blob)
reader.onload = () =>{ return reader.result; }
- Unit8Array 数据转为 Blob 数据
new Blob(Unit8Array,{type:"类型"})
- Blob|File数据创建url
URL.createObjectURL(blob|file)
- base64 转为 Unit8Array
除了使用 ffmpeg.fetchFile 外,还可以使用如下方法,继续将结果使用 new Blob() 包装即转为 Blob,使用 new File() 包装即转为 File
var audioData = window.atob(base64);
var audioArrayBuffer = new ArrayBuffer(audioData.length);
var audioView = new Uint8Array(audioArrayBuffer);
for (var i = 0; i < audioData.length; i++) {
audioView[i] = audioData.charCodeAt(i);
}
# audioArrayBuffer 就是Unit8Array
- File|Blob转为 base64
let reader = new FileReader();
reader.readAsDataURL(file|blob);
reader.onload = function (e) {
return reader.result; };
}
- Unit8Array转为base64
const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)))