从零开始 fluent-ffmpeg(一)

最近在了解音视频相关知识,所以不可避免的就会接触到ffmpeg这个玩意儿,但是ffmpeg的原生命令行较为复杂,而 fluent-ffmpeg 则将这些命令抽象为一个npm包,对于前端来说,无疑是借助这种方式上手更舒服(只不过作者已经好久没更新了。。。)

环境搭建以及安装

首先需要 ffmpeg >= 0.9 的环境,低于这个版本某些功能不兼容,而且作者也不会再去对低版本兼容了

以windows系统为例,先去官网下载: http://ffmpeg.org/download.html#build

然后就是正常的下载解压过程,完成之后,将解压后的bin目录的路径添加到系统的环境变量中:

添加完成之后,查看是否生效;

ffmpeg -version

出现编译库信息,环境配置完成。如果没有出现,重启大法欢迎你

环境搭建完成后,新建一个node环境目录

npm install fluent-ffmpeg

此外,还需要安装一个视频播放器VLC,可以看到很详细视频的元数据:

videolan.org/vlc/downlo

正常下载即可,这样前期的准备工作就差不多了

API

作为视频处理工具,最开始得先了解fluent-ffmpeg的工作流,包括一些钩子

当正确处理好输入输出的时候,其他的大部分api都是调用下即可

输入和输出

即导入源文件,写法非常多:

const fs = require('fs')
const Ffmpeg = require('fluent-ffmpeg')
// 方式一:直接构造函数传入文件路径,或者直接调用不实例化也可以
const command = new Ffmpeg('./test.mp4') // 不用new直接调用也可
// 方式二:创建文件读取流
const inputStream = fs.createReadStream('./test.mp4')
const command = Ffmpeg(inputStream)
// 方式三:传入constraints约束对象
const command = Ffmpeg({
  source: './test.mp4',
  // 其他约束项,这里留给大家自己去试验了就不讲效果了
// 方式四:input函数
const command = Ffmpeg().input('./test.mp4')

即导出处理后的视频,同样写法很多:

// 方式一:save()方式导出,参数为输出路径
command.save('./output.avi')
// 方式二:pipe()可写流方式导
const outputStream = fs.createWriteStream('./output.avi')
command
  .format('avi')
  .pipe(outputStream)
// 方式三:output + run
const outputStream = fs.createWriteStream('./output.avi')
command
  .output(outputStream)
  .format('avi')
  .run()

注意,以第二、三种流方式进行导出的话,需要对输出文件的格式进行一个规范,否则会报错: github.com/fluent-ffmpe

了解了输入输出之后,其他的api就是链式调用。这里我们用 noAudio 来实现一个最简单的demo:

const fs = require('fs')
const Ffmpeg = require('fluent-ffmpeg')
const command = Ffmpeg('./test.mp4')
command
  .noAudio()
  .save('./output.avi')

OK,这个demo就实现了将原来的MP4视频消音并转码成avi格式了

生命周期钩子

输入输出解决之后,开始了解它提供给我们的转码过程中的钩子

start

ffmpeg 进程开始,回调参数为完整的命令行

codecData

编解码器以及视频的详细信息,字段如下:

  • format:支持的输入格式
  • audio:音频编解码器
  • video:视频编解码器
  • duration:输入的时间
  • video_details:输入视频详情
  • audio_details:输出视频详情

progress

转码的进度信息,结构如下:

{
  // 已处理的总帧数
  frames: number;
  // FFmpeg 当前正在处理的帧速率
  currentFps: number;
  // FFmpeg 当前正在处理的吞吐量
  currentKbps: number;
  // 目标文件的当前大小(以KB为单位)
  targetSize: number;
  // 当前帧的时间戳(以秒为单位)
  timemark: string;
  // 进度百分比的估计
  percent: number;

stderr

FFmpeg 输出,FFmpeg进程执行过程中每次向stderr输出一行(line)的时候就会触发该回调

error

转码错误

end

处理完成

下面给demo加上一些log:

const fs = require('fs')
const Ffmpeg = require('fluent-ffmpeg')
const command = Ffmpeg('./test.mp4')
command
  .noAudio()
  .on('start', (commandLine) => {
    console.log('ffmpeg started with command: ', commandLine)
  .on('end', () => {
    console.log('ffmpeg finished')
  .on('error', err => {
    console.log(err.message)
  .save('./output.avi')

视音频配置

上述基本流程了解后,下列的配置函数我们只需要在输出之前链式调用即可

inputFPS(fps) - 指定输入帧率

command.inputFPS(60)

fps(fps) - 设置输出帧率

command.fps(60)

seekInput - 设置输入时间(单位为s 或者 [[hh:]mm:]ss[.xxx]时间戳字符串)

command.seekInput(120) // 从输入视频2分钟处开始处理

seek - 设置输出的其实时间(单位同seekInput)

command.seek(120) // 将转码后的视频从2分钟后截取,之前处理的帧废弃

format - 设置输出格式

command.format('avi')

loop([duration]) - 循环输入(持续时间 - 单位格式同seekInput)

const command = Ffmpeg('./test.png')
command.loop(60) // 输入源貌似只能为图片,如果用视频则会报错

noAudio - 禁用音频

command.noAudio()

noVideo - 禁用视频

command.noVideo()

size(size) - 设置输出帧大小(分辨率)

参数格式可以为如下方式:

  • 640x480: 固定分辨率格式,除非调用 autopad 自动填充,否则视频会被拉伸或压缩到合适的尺寸
  • 640x? 或 ?x480: 高度或宽度自适应,如果使用了 aspect 设置了宽高比,就按传入的宽高比计算,否则直接按输入的宽高比进行换算
  • 50%: 缩放,百分比表示缩放到原画面分辨率的百分比,宽高比不变

aspect(aspect) - 设置输出帧的宽高比

参数格式可以为 数字型 或者 'X:Y' 格式的字符串,值得注意的是,如果是 size 函数传的是固定分辨率格式或者百分比的分辨率格式,以及size没有调用的情况,该函数是不生效的

autopad([color]) - 启用自动填充输出视频

参数为填充的颜色值,该配置是否生效是依赖于上述两个配置的调用以及传参的情况的:

  • size 未调用或者调用了没有设置分辨率或者设置的是百分比参数的分辨率, autoPad 函数不生效
  • size 用固定分辨率格式的参数时, autoPad 生效,添加填充以保持输入的长宽比
  • size 用宽或高自适应的参数时,只有调用 aspect 设置宽高比才会生效,否则不生效

其实个人总结就是,只有当输出的分辨率宽高比相较于输入的分辨率的宽高比有变化的时候,自动填充才会生效。这其实也很好理解,如果改变了原来的分辨率比例,视频帧肯定会拉伸导致失真,所以正常来说肯定是保持原来的宽高比是最好的体验,这个时候就用自动填充去补充宽度或者高度因为分辨率比例计算之后不足的部分

// 生效,固定分辨率
command
  .size('640x480')
  .autopad('red')
// 生效,宽或高自适应,且设置了宽高比
command
  .size('640x?')
  .aspect(1)
  .autopad('red')

keepDAR - 保持显示纵横比

保持显示宽高比。即最终播放出来的画面的宽与高之比。比如常见的16:9和4:3等。缩放视频也要按这个比例来,否则会使图像看起来被压扁或者拉长了

command.keepDAR()

screenshots(options) - 生成缩略图

提取一个或多个缩略图,并将其另存为 PNG 文件

options 有如下配置项:

  • folder:生成图像文件的输出文件夹,默认为当前文件夹
  • filename:文件名(配置模式可见下文),默认为 tn.png
  • timemarks 或 timestamps::指定应在其中拍摄缩略图的视频中的时间戳 数组 。每个时间戳可能是一个数字(以秒为单位),百分比字符串(例如“ 50%”)或格式为“hh:mm:ss.xxx”的时间戳字符串(小时,分钟和毫秒均为可选)
  • count:生成个缩略图数量,会按数量将视频等分之生成(例如,当请求 3 个缩略图时,分别为视频长度的 25%,50%和 75%)。如果配置了timemarks 或者 timestamps,那么该项不生效
  • size:缩略图分辨率,与上文提到的 size() 配置参数格式一样。注意:生成缩略图时,不应使用 size() 方法

filename 选项为生成的文件指定文件名模式,提供了如下占位符:

  • %s:偏移量(以秒为单位)
  • %w:屏幕截图宽度
  • %h:屏幕截图高度
  • %r:屏幕截图分辨率(与’%wx%h’相同)
  • %f:输入文件名
  • %b:输入基本名称(不带扩展名的文件名)
  • %i:屏幕快照在时间标记数组中的索引

下面是一个demo:

command
  .screenshots({
    folder: './imgs',
    filename: 'test-t_%s-w_%w-h_%h-r_%r-f_%f-b_%b-i_%i.png',
    count: 1,
    timemarks: [1],
    size: '50%'

好了到目前为止,fluent-ffmpeg的基本使用,都已经介绍的差不多了,可以利用现有的一些api实现一些最基本的功能。

之所以分成两个部分,是因为第一部分的知识可以让人很快就实现一些基本的效果,其他的api要么就设计到音视频一些参数新的概念,要么就是ffmpeg的原生命令,或者就是ffmpeg 进程相关。反正作者脑子并发度并不高,不太喜欢一篇文档里放的东西太多太复杂。

举个例子,本文没提到的 mergeToFile 这个api,我一开始的理解就是只要找两个输入源,调用一下这个,就行了:

command
  .input('./test.mp4')
  .input('./test1.mp4')
  .on('start', (commandLine) => {
    console.log('ffmpeg started with command: ', commandLine)
  .on('end', (stdout, stderr) => {
    console.log('ffmpeg finished')
    console.log(stderr)
  .on('error', err => {
    console.log(err.message)
  .mergeToFile('./merge.avi', '/merge')

但是很遗憾 ffmpeg 直接报错了:

如果换成完全相同的输入源,就可以了:

command
  .input('./test.mp4')
  .input('./test.mp4')
  .on('start', (commandLine) => {
    console.log('ffmpeg started with command: ', commandLine)
  .on('end', (stdout, stderr) => {
    console.log('ffmpeg finished')
    console.log(stderr)