前言
使用到的第三方库:👉👉 ffmpeg.wasm
npm
上包名叫这个 👉👉 @ffmpeg/ffmpeg
ffmpeg.wasm
先来学习一下这个库的基本用法,全部的 API
都在下面了。
安装
npm install @ffmpeg/ffmpeg @ffmpeg/core -S
在模块中导出这两个方法 createFFmpeg
, fetchFile
。
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
createFFmpeg
createFFmpeg
是一个创建 ffmpeg
实例的工厂函数。
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({})
参数如下
-
corePath: 指定
ffmpeg-core.js
的加载路径。 -
log: 是否打开所有日志,默认为
false
-
logger: 获取日志消息的函数,例:
({ message }) => console.log(message)
-
progress: 跟踪进度的函数,例:
p => console.log(p)
ffmpeg.load
ffmpeg.load()
返回一个 Promise
,用来加载 ffmpeg-core.js
核心包,在浏览器环境中,ffmpeg.wasm-core
脚本默认是从 CDN
中获取的,可以在创建 ffmpeg
实例时通过 corePath
来指定到本地路径。
(async () => {
await ffmpeg.load();
})();
ffmpeg.run
ffmpeg.run(...args)
返回一个 Promise
,官网说这个方法和原生的 ffmpeg
一样,需要传递的参数也一样,但我原生的也不会啊😂😂,算了,知道用法就行了。
(async () => {
/* 等价于执行了 `$ ffmpeg -i flame.avi -s 1920x1080 output.mp4` */
await ffmpeg.run('-i', 'flame.avi', '-s', '1920x1080', 'output.mp4');
})();
ffmpeg
参数说明:
参数 | 说明 |
---|---|
基本选项: | |
-formats | 输出所有可用格式 |
-f fmt | 指定格式(音频或视频格式) |
-i filename | 指定输入文件名,在linux下当然也能指定:0.0(屏幕录制)或摄像头 |
-y | 覆盖已有文件 |
-t duration | 记录时长为t |
-fs limit_size | 设置文件大小上限 |
-ss time_off | 从指定的时间(s)开始, [-]hh:mm:ss[.xxx]的格式也支持 |
-itsoffset time_off | 设置时间偏移(s),该选项影响所有后面的输入文件。该偏移被加到输入文件的时戳,定义一个正偏移意味着相应的流被延迟了 offset秒。 [-]hh:mm:ss[.xxx]的格式也支持 |
-title string | 标题 |
-timestamp time | 时间戳 |
-author string | 作者 |
-copyright string | 版权信息 |
-comment string | 评论 |
-album string | album名 |
-v verbose | 与log相关的 |
-target type | 设置目标文件类型("vcd", "svcd", "dvd", "dv", "dv50", "pal-vcd", "ntsc-svcd", ...) |
-dframes number | 设置要记录的帧数 |
视频选项: | |
-b | 指定比特率(bits/s),似乎ffmpeg是自动VBR的,指定了就大概是平均比特率 |
-bitexact | 使用标准比特率 |
-vb | 指定视频比特率(bits/s) |
-vframes number | 设置转换多少桢(frame)的视频 |
-r rate | 帧速率(fps) (可以改,确认非标准桢率会导致音画不同步,所以只能设定为15或者29.97) |
-s size | 指定分辨率 (320x240) |
-aspect aspect | 设置视频长宽比(4:3, 16:9 or 1.3333, 1.7777) |
-croptop size | 设置顶部切除尺寸(in pixels) |
-cropbottom size | 设置底部切除尺寸(in pixels) |
-cropleft size | 设置左切除尺寸 (in pixels) |
-cropright size | 设置右切除尺寸 (in pixels) |
-padtop size | 设置顶部补齐尺寸(in pixels) |
-padbottom size | 底补齐(in pixels) |
-padleft size | 左补齐(in pixels) |
-padright size | 右补齐(in pixels) |
-padcolor color | 补齐带颜色(000000-FFFFFF) |
-vn | 取消视频 |
-vcodec codec | 强制使用codec编解码方式('copy' to copy stream) |
-sameq | 使用同样视频质量作为源(VBR) |
-pass n | 选择处理遍数(1或者2)。两遍编码非常有用。第一遍生成统计信息,第二遍生成精确的请求的码率 |
-passlogfile file | 选择两遍的纪录文件名为file |
-newvideo | 在现在的视频流后面加入新的视频流 |
高级视频选项: | |
-pix_fmt format | set pixel format, 'list' as argument shows all the pixel formats supported |
-intra | 仅适用帧内编码 |
-qscale q | 以<数值>质量为基础的VBR,取值0.01-255,约小质量越好 |
-loop_input | 设置输入流的循环数(目前只对图像有效) |
-loop_output | 设置输出视频的循环数,比如输出gif时设为0表示无限循环 |
-g int | 设置图像组大小 |
-cutoff int | 设置截止频率 |
-qmin int | 设定最小质量,与-qmax(设定最大质量)共用,比如-qmin 10 -qmax 31 |
-qmax int | 设定最大质量 |
-qdiff int | 量化标度间最大偏差 (VBR) |
-bf int | 使用frames B 帧,支持mpeg1,mpeg2,mpeg4 |
音频选项: | |
-ab | 设置比特率(单位:bit/s,也许老版是kb/s)前面-ac设为立体声时要以一半比特率来设置,比如192kbps的就设成96,转换 默认比特率都较小,要听到较高品质声音的话建议设到160kbps(80)以上。 |
-aframes number | 设置转换多少桢(frame)的音频 |
-aq quality | 设置音频质量 (指定编码) |
-ar rate | 设置音频采样率 (单位:Hz),PSP只认24000 |
-ac channels | 设置声道数,1就是单声道,2就是立体声,转换单声道的TVrip可以用1(节省一半容量),高品质的DVDrip就可以用2 |
-an | 取消音频 |
-acodec codec | 指定音频编码('copy' to copy stream) |
-vol volume | 设置录制音量大小(默认为256) <百分比> ,某些DVDrip的AC3轨音量极小,转换时可以用这个提高音量,比如200就是原来的2倍 |
-newaudio | 在现在的音频流后面加入新的音频流 |
字幕选项: | |
-sn | 取消字幕 |
-scodec codec | 设置字幕编码('copy' to copy stream) |
-newsubtitle | 在当前字幕后新增 |
-slang code | 设置字幕所用的ISO 639编码(3个字母) |
Audio/Video 抓取选项: | |
-vc channel | 设置视频捕获通道(只对DV1394) |
-tvstd standard | 设置电视标准 NTSC PAL(SECAM) |
ffmpeg.FS
ffmpeg.FS(method, ...args)
用来运行 FS
操作。
对于 ffmpeg.wasm
的输入/输出文件,需要先将它们保存到 MEMFS
以便 ffmpeg.wasm
能够使用它们。这里我们依赖 Emscripten
提供的 FS
方法♂️。
参数如下
- method: 需要执行的方法名。
- args: 执行方法对应的参数。
/* Write data to MEMFS, need to use Uint8Array for binary data */
// 把文件存入内存中
ffmpeg.FS('writeFile', 'video.avi', new Uint8Array(...));
/* Read data from MEMFS */
// 在内存中读取
ffmpeg.FS('readFile', 'video.mp4');
/* Delete file in MEMFS */
// 在内存中删除
ffmpeg.FS('unlink', 'video.mp4');
ffmpeg.exit
ffmpeg.exit()
用来杀死程序的执行,同时删除 MEMFS
以释放内存。
ffmpeg.setLogging
ffmpeg.setLogging(logging)
控制是否将日志信息输出到控制台。
参数如下
- logging: 在控制台中打开/关闭日志消息。
ffmpeg.setLogging(true);
ffmpeg.setLogger
ffmpeg.setLogger(logger)
设置和获取 ffmpeg.wasm
的输出消息。。
参数如下
- logger: 处理消息的函数。
ffmpeg.setLogger(({ type, message }) => {
console.log(type, message);
/*
* type can be one of following:
*
* info: internal workflow debug messages
* fferr: ffmpeg native stderr output
* ffout: ffmpeg native stdout output
*/
});
ffmpeg.setProgress
ffmpeg.setProgress(progress)
进度处理程序,用于获取 ffmpeg
命令的当前进度。
参数如下
- progress: 处理进度信息的函数。
ffmpeg.setProgress(({ ratio }) => {
console.log(ratio);
/*
* ratio is a float number between 0 to 1. 0 到 1之间的数字
*/
});
fetchFile
fetchFile(media)
返回一个 Promise
, 用于从各种资源中获取文件。要处理的视频/音频文件可能位于远程 URL
或本地文件系统中的某个位置。这个函数帮助你获取文件并返回一个 Uint8Array
变量供 ffmpeg.wasm
使用。
参数如下
- media:
URL
字符串、base64
字符串或File
、Blob
、Buffer
对象。
(async () => {
const data = await fetchFile('https://github.com/ffmpegwasm/testdata/raw/master/video-3s.avi');
/*
* data will be in Uint8Array format
*/
})();
补充
设置 corePath
corePath
支持引入 cdn
const ffmpeg = createFFmpeg({
corePath: 'https://unpkg.com/@ffmpeg/core@0.8.5/dist/ffmpeg-core.js',
})
但是业务需要部署在内网,访问不了 cdn
,幸好 corePath
也支持加载本地文件。
这里应该是支持绝对路径,默认会去访问 public
下面的文件。
const ffmpeg = createFFmpeg({
corePath: 'ffmpeg-core.js',
})
把 node_modules\@ffmpeg\core\dist
下面的三个文件拷贝到 public
中。
设置日志
在创建实例的时候,通过传入 log: true
,开启日志。
const ffmpeg = createFFmpeg({
log: true,
})
也可以通过 ffmpeg.setLogger
自定义日志格式,比如
ffmpeg.setLogger(({ type, message }) => {
console.log('🚀🚀 ~ message', message);
console.log('🚀🚀 ~ type', type);
});
还可以直接在创建实例的时候传入 logger
属性,效果是一样的,建议把 log
属性改为 false
,不然日志会重复。
const ffmpeg = createFFmpeg({
corePath: 'ffmpeg-core.js',
log: false,
logger: ({ type, message }) => {
console.log('🚀🚀 ~ message', message);
console.log('🚀🚀 ~ type', type);
}
})
获取进度
如何获取上传文件的进度呢,可以通过 ffmpeg.setProgress
ffmpeg.setProgress(({ ratio }) => {
console.log('🚀🚀 ~ ratio', ratio);
});
也可以直接在创建实例的时候传入 progress
属性,效果是一样的。
const ffmpeg = createFFmpeg({
corePath: 'ffmpeg-core.js',
log: false,
progress: ({ ratio }) => {
console.log('🚀🚀 ~ ratio', ratio);
}
})
解决错误
如果产生下面这个错误
本地开发的时候需要在 vue.config.js
中添加
devServer: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
}
如果是 vite
的项目这样修改 vite.config.ts
plugins: [
vue(),
vueJsx(),
{
name: 'configure-response-headers',
configureServer: server => {
server.app.use('/node_modules/',(_req, res, next) => {
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
}
}
],
部署的时候需要配置 nginx
或者在后端配置。
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
本地上传视频
上传文件功能我们用 IView
组件库的上传组件,组件库基本都有上传组件,道理是一样的。
页面组件
main.vue
<template>
<div style="width:100%; height:100%; background-color: #DAE4E4;padding:50px">
<upload-btn @before-upload="beforeUpload"></upload-btn>
<video-ref :video-file-url="videoFileUrl"></video-ref>
</div>
</template>
<script>
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg'
import videoRef from '../../components/video/Video.vue'
import UploadBtn from '../../components/video/UploadBtn.vue'
export default {
data() {
return {
ffmpeg: {},
videoFileUrl: "",
}
},
components: {
videoRef,
UploadBtn
},
computed: {},
methods: {
// 页面初始化逻辑
async init() {
await this.initFfmpeg()
},
// 初始化 ffmpeg 功能
async initFfmpeg() {
this.ffmpeg = createFFmpeg({
corePath: 'ffmpeg-core.js',
log: true,
})
},
// 上传文件前的钩子
async beforeUpload(file) {
console.log('🚀🚀 beforeUpload ~ file', file);
await this.ffmpeg.load();
await this.getVideoFileUrl(file)
},
async getVideoFileUrl(file) {
// 获取资源文件
const result = await fetchFile(file)
console.log('🚀🚀 ~ result', result);
// 对于 ffmpeg.wasm 的输入/输出文件,需要先将它们保存到 MEMFS 以便 ffmpeg.wasm 能够使用它们
this.ffmpeg.FS('writeFile', `${file.name}`, result);
await this.ffmpeg.run('-i', `${file.name}`, '-acodec', 'aac', '-vcodec', 'libx264', '-y', `${file.name.split('.')[0]}.mp4`);
// 在内存中读取文件
const data = this.ffmpeg.FS('readFile', `${file.name.split('.')[0]}.mp4`);
// 获取内存中的播放地址
this.videoFileUrl = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }))
console.log('🚀🚀 ~ this.videoFileUrl', this.videoFileUrl);
}
},
async mounted() {
// 页面初始化逻辑
await this.init()
}
}
</script>
子组件上传按钮
UploadBtn.vue
<template>
<div class="upload-btn">
<i-upload :before-upload="beforeUpload"
:show-upload-list="false"
action=""
name="file"
accept="video/*">
<i-button>点击上传视频</i-button>
<br>
<br>
</i-upload>
</div>
</template>
<script>
export default {
methods: {
// 上传文件前的钩子
async beforeUpload(file) {
this.$emit("before-upload", file)
},
}
}
</script>
子组件视频播放器
Video.vue
// 播放视频我们采用 `HTML5` 原生标签 `video`。
<template>
<div class="video-box">
<video id="videoRef"
ref="videoRef"
v-if="videoFileUrl"
class="videoRef"
controls>
<source :src="videoFileUrl"
type="video/mp4" />
</video>
</div>
</template>
<script>
export default {
props: {
videoFileUrl: {
default: () => (''),
},
},
}
</script>