前端视频处理利器:ffmpeg.wasm网页中预览非mp4视频并嵌入字幕

4 阅读4分钟

借助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版、普通版等,根据需要选择。

image.png

同时还会生成几个示例代码文件,可以直接照着模仿开干

image.png

任务一:在网页中预览mov视频

大家都知道网页原生支持的视频播放格式有限,兼容性最好的就是 mp4 H.264 编码了,比如mov视频就无法在线预览。使用ffmpeg.wasm可解决这个问题,而不必先传给后端转码。

  1. 上传视频, 获取到上传的视频数据
<input type="file" accept="video/*" onchange="upload(this)">

// 接受上传的视频
function upload(e){
    ... 忽略一些验证处理等
    let file = e.target.files[0];
    # 调用自定义函数,使用ffmpeg进行转码后插入页面预览
    transcode(file);
}

  1. 加载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",
        });
    }
}

  1. 将视频文件读取并写入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就很容易

  1. 第一步依然是上传视频、读取并写入到虚拟文件系统,和任务一相同,就不赘述了。
  2. 第2步准备要执行的ffmpeg命令参数,这个任务的参数是cmd=["-i",input_name,"-ss","00:00:10","-to","00:00:15","output.mp4"], 使用 ffmpeg.exec(cmd) 执行这个命令。
  3. 执行成功后,依然调用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 互相转换

  1. 上传文件File转为Unit8Array

使用 ffmpeg.fetchFile 读取上传的视频文件返回 Unit8Array 类型,并调用 ffmpeg.writeFile写入到虚拟文件系统

await ffmpeg.writeFile(input_name, await fetchFile(file));

  1. 使用 ffmpeg.readFile 读取二进制数据,返回 Unit8Array 数据,然后生成 Blob 用于网页显示和下载
const data = await ffmpeg.readFile(outname);
URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }))
  1. Blob 数据转为 Unit8Array 数据

ffmpeg.fetchFile(blob)

const reader = new FileReader() 
reader.readAsArrayBuffer(blob) 
reader.onload = () =>{ return reader.result; }
  1. Unit8Array 数据转为 Blob 数据

new Blob(Unit8Array,{type:"类型"})

  1. Blob|File数据创建url

URL.createObjectURL(blob|file)

  1. 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

  1. File|Blob转为 base64
    let reader = new FileReader(); 
    reader.readAsDataURL(file|blob); 
    reader.onload = function (e) { 
        return reader.result; }; 
    }
  1. Unit8Array转为base64
const base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)))