背景:
最近遇到一个需求:需要对长视频的音频做提取,最后对音频识别出文字,算法和后端都可以对视频进行视频切割和提取,那我就想前端能不能实现呢,就开始调研方案。
后端和算法使用的都是FFmpeg这个库实现视频的操作,其实咱们前端也有FFmpeg.js,能对视频进行操作。
一、FFmpeg.wasm 简介
先讲FFmpeg,FFmpeg 是一个开源的音视频处理工具,支持录制、转换和流式传输音视频。它提供了丰富的功能,如格式转换、视频剪辑、音频提取等。
FFmpeg官网链接;其本身是一个开源的客户端工具,同时也提供了多种语言的api支持。
FFmpeg.wasm是c语言编写的程序,通过编译后可以在web平台上使用(浏览器),
原理:WebAssembly(简称wasm)是一个虚拟指令集体系架构(virtual ISA),整体架构包括核心的ISA定义、二进制编码、程序语义的定义与执行,以及面向不同的嵌入环境(如Web)的应用编程接口(WebAssembly API)。其目标是为 C/C++等语言编写的程序经过编译,在确保安全和接近原生应用的运行速度更好地在 Web 平台上运行。
简单来说:WebAssembly 是一种高效的二进制格式,能够在现代浏览器中快速执行。
运行原理:
-
加载和初始化 : 当用户在浏览器中加载
ffmpeg.js
时,浏览器会下载编译后的 WebAssembly 模块和相关的 JavaScript 代码。 -
调用 FFmpeg 功能 : 用户可以通过 JavaScript 调用 FFmpeg 的命令和功能,传递参数和数据。
ffmpeg.js
会将这些调用转换为相应的 WebAssembly 函数调用。 -
处理音视频: FFmpeg 在浏览器中执行音视频处理任务,使用虚拟文件系统读取输入文件,进行处理,然后将结果输出到虚拟文件系统中。
-
下载结果: 处理完成后,用户可以通过 JavaScript 下载处理后的文件,所有操作都在客户端完成,无需与服务器交互。
二、常用FFmpeg方法
ffmpeg.js
提供了一系列方法来处理音视频文件。以下是一些常用的方法和功能:
1. 基本方法
-
FFmpeg.load()
:- 加载 FFmpeg 的核心库。这个方法需要在使用其他 FFmpeg 功能之前调用。
-
FFmpeg.run(...args)
:-
执行 FFmpeg 命令。参数是一个字符串数组,表示要执行的命令及其参数。例如:
javascriptawait ffmpeg.run('-i', 'input.mp4', 'output.avi');
-
2. 文件操作
-
FFmpeg.FS(command, filename, data)
:- 在虚拟文件系统中执行文件操作。常用的命令包括:
writeFile
: 将数据写入虚拟文件系统。readFile
: 从虚拟文件系统读取文件。unlink
: 删除虚拟文件系统中的文件。
javascript// 写入文件 ffmpeg.FS('writeFile', 'input.mp4', await fetchFile('path/to/local/file.mp4')); // 读取文件 const data = ffmpeg.FS('readFile', 'output.avi');
- 在虚拟文件系统中执行文件操作。常用的命令包括:
3. 音视频处理
-
格式转换:
-
使用
run
方法进行格式转换。例如,将 MP4 转换为 AVI:javascriptawait ffmpeg.run('-i', 'input.mp4', 'output.avi');
-
-
视频剪辑:
-
可以通过指定时间戳来剪辑视频。例如,剪辑从 10 秒到 20 秒的片段:
javascriptawait ffmpeg.run('-i', 'input.mp4', '-ss', '10', '-to', '20', 'output.mp4');
-
-
音频提取:
-
从视频中提取音频:
javascriptawait ffmpeg.run('-i', 'input.mp4', '-q:a', '0', '-map', 'a', 'output.mp3');
-
4. 视频压缩和调整
-
视频压缩:
-
可以通过调整比特率来压缩视频:
javascriptawait ffmpeg.run('-i', 'input.mp4', '-b:v', '1000k', 'output.mp4');
-
-
调整分辨率:
-
改变视频的分辨率:
javascriptawait ffmpeg.run('-i', 'input.mp4', '-vf', 'scale=640:360', 'output.mp4');
-
5. 其他功能
-
添加水印:
-
可以在视频上添加水印:
javascriptawait ffmpeg.run('-i', 'input.mp4', '-i', 'watermark.png', '-filter_complex', 'overlay=10:10', 'output.mp4');
-
-
合并视频:
-
合并多个视频文件:
javascriptawait ffmpeg.run('-i', 'concat:input1.mp4|input2.mp4', '-c', 'copy', 'output.mp4');
-
6. 获取处理结果
- 获取处理后的文件 :
-
使用
readFile
方法获取处理后的文件数据,并可以将其转换为 Blob 以便下载:javascriptconst data = ffmpeg.FS('readFile', 'output.mp4'); const blob = new Blob([data.buffer], { type: 'video/mp4' }); const url = URL.createObjectURL(blob);
-
三、具体功能实现
需求:
本地选择长视频,允许对长视频选择时间段裁剪,音频提取,图片帧提取。
1、ffmpeg引入:
npm install vue video.js @ffmpeg/ffmpeg
这里需要注意,安装依赖的时候我出现报错了 ,所以参考FFmpeg安装避坑,最后有效安装。
步骤:
1、手动在package.json文件中加入配置后下载
2、将nodemodules中的这三个文件复制到pubilc下
启动项目之后可以使用ffmpeg的能力了
2、切割视频
vue
/**
* 切割视频方法
* 使用FFmpeg对视频进行时间段切割
* 步骤:
* 1. 设置处理状态并加载FFmpeg
* 2. 获取原视频文件并写入FFmpeg文件系统
* 3. 执行FFmpeg命令进行切割
* 4. 读取切割后的视频并创建下载链接
* 5. 将结果添加到处理文件列表中
*/
async cutVideo() {
try {
// 设置处理状态
this.processing = true;
this.processingStatus = '正在切割视频...';
// 确保FFmpeg已加载
await this.loadFFmpeg();
// 获取视频文件并写入FFmpeg虚拟文件系统
const videoBlob = await fetch(this.videoSource).then(r => r.blob());
ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoBlob));
// 执行FFmpeg命令进行视频切割
// -i: 输入文件
// -ss: 开始时间点(秒)
// -t: 持续时间(秒)
// -c copy: 复制编解码器(不重新编码,速度快)
await ffmpeg.run(
'-i', 'input.mp4',
'-ss', `${this.startTime}`,
'-t', `${this.endTime - this.startTime}`,
'-c', 'copy',
'output.mp4'
);
// 从FFmpeg文件系统读取切割后的视频
const data = ffmpeg.FS('readFile', 'output.mp4');
// 创建Blob对象用于下载
const blob = new Blob([data.buffer], { type: 'video/mp4' });
// 将处理结果添加到文件列表
this.processedFiles.push({
name: 'cut_video.mp4', // 文件名
type: 'video', // 文件类型
blob: blob, // 文件数据
url: this.createDownloadUrl(blob) // 下载链接
});
} catch (error) {
console.error('视频切割失败:', error);
} finally {
// 重置处理状态
this.processing = false;
}
},
打印如下: 结果如下:
3、提取音频
/**
* 提取音频方法
* 使用FFmpeg从视频中提取音频轨道并转换为MP3格式
* 步骤:
* 1. 设置处理状态并加载FFmpeg
* 2. 从processedFiles中获取已处理的视频文件
* 3. 将视频写入FFmpeg文件系统
* 4. 执行FFmpeg命令提取音频
* 5. 读取提取的音频并创建下载链接
* 6. 将结果添加到处理文件列表中
*/
async extractAudio() {
try {
// 设置处理状态
this.processing = true;
this.processingStatus = '正在提取音频...';
// 确保FFmpeg已加载
await this.loadFFmpeg();
// 从处理文件列表中获取视频文件
const videoFile = this.processedFiles.find(f => f.type === 'video');
if (!videoFile) throw new Error('未找到视频文件');
// 将视频文件写入FFmpeg虚拟文件系统
ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile.blob));
// 执行FFmpeg命令提取音频
// -i: 输入文件
// -vn: 禁用视频输出
// -acodec libmp3lame: 使用LAME编码器将音频编码为MP3格式
await ffmpeg.run(
'-i', 'input.mp4',
'-vn',
'-acodec', 'libmp3lame',
'audio.mp3'
);
// 从FFmpeg文件系统读取提取的音频
const data = ffmpeg.FS('readFile', 'audio.mp3');
// 创建Blob对象用于下载
const blob = new Blob([data.buffer], { type: 'audio/mp3' });
// 将处理结果添加到文件列表
this.processedFiles.push({
name: 'extracted_audio.mp3', // 文件名
type: 'audio', // 文件类型
blob: blob, // 文件数据
url: this.createDownloadUrl(blob) // 下载链接
});
} catch (error) {
// 错误处理
console.error('音频提取失败:', error);
} finally {
// 重置处理状态
this.processing = false;
}
},
音频提取打印:
结果如下:
4、视频帧提取
/**
* 从视频中提取关键帧
* 使用FFmpeg每60秒提取一帧图像,并保存为JPEG格式
* 提取的帧会被添加到processedFiles数组中供后续使用
*/
async extractFrames() {
try {
// 设置处理状态为正在进行中
this.processing = true;
this.processingStatus = '正在抽取关键帧...';
// 确保FFmpeg已加载完成
await this.loadFFmpeg();
// 从处理文件列表中查找视频文件
const videoFile = this.processedFiles.find(f => f.type === 'video');
if (!videoFile) throw new Error('未找到视频文件');
// 将视频文件写入FFmpeg虚拟文件系统
ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile.blob));
// 执行FFmpeg命令提取帧
// -i: 指定输入文件
// -vf fps=1/60: 设置视频过滤器,每60秒提取1帧
// -frame_pts 1: 在输出文件名中包含显示时间戳
// frame_%d.jpg: 输出文件名格式,_%d会被替换为帧序号
await ffmpeg.run(
'-i', 'input.mp4',
'-vf', 'fps=1/60',
'-frame_pts', '1',
'frame_%d.jpg'
);
// 存储提取的帧
const frames = [];
// 遍历提取的帧,最多提取maxFrames个
for (let i = 1; i <= this.maxFrames; i++) {
try {
// 从FFmpeg文件系统读取帧数据
const data = ffmpeg.FS('readFile', `frame_${i}.jpg`);
// 创建Blob对象用于预览和下载
const blob = new Blob([data.buffer], { type: 'image/jpeg' });
// 将帧信息添加到数组
frames.push({
name: `frame_${i}.jpg`, // 帧文件名
type: 'image', // 文件类型
blob: blob, // 帧数据
url: this.createDownloadUrl(blob) // 创建下载链接
});
} catch (e) {
// 如果读取失败,说明没有更多帧了,退出循环
break;
}
}
// 将提取的帧添加到处理文件列表
this.processedFiles.push(...frames);
} catch (error) {
// 错误处理
console.error('帧提取失败:', error);
} finally {
// 重置处理状态
this.processing = false;
}
}
},
结果如下:
遇到的问题:
在引入之后发现跨域问题:ReferenceError: SharedArrayBuffer is not defined
别慌:在vue.config.js中增加下面的配置:(我使用的是vite,webpack自行修改)
js
devServer: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin", // 保护你的源站点免受攻击
"Cross-Origin-Embedder-Policy": "require-corp", // 保护受害者免受你的源站点的影响
},
}
上线后还有这样的问题,增加nginx配置:
js
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
add_header Cross-Origin-Resource-Policy same-origin;
完整的项目代码我放在github,欢迎star,下载下来可以直接对本地视频进行剪辑
后续我尝试一下前端实现音频文字的提取。
四、总结
ffmpeg.js
提供了丰富的功能来处理音视频文件,包括格式转换、剪辑、提取音频、压缩、调整分辨率、添加水印等。通过这些方法,开发者可以在浏览器中实现强大的音视频处理功能。 能够在浏览器中运行而不需要安装其他工具,主要是因为它利用了 WebAssembly 和 Emscripten 技术,将 FFmpeg 编译为可以在浏览器中高效执行的格式。通过虚拟文件系统和浏览器 API,ffmpeg.js
实现了音视频处理的完整功能,提供了一个强大的客户端解决方案。