前言
商家在发布商品的时候,大部分情况下是没有视频的,这样往往会造成商品展示不全等问题,而视频制作又比较麻烦,为了解决此痛点,我们需要提供一键合成视频的功能。
之所以选择 FFmpeg,是因为我们期望后续能够进行视频剪辑、字幕添加等更复杂的音视频操作。下面我们就来了解下什么是 FFmpeg。
音视频开发免费学习地址:https://ke.qq.com/course/3202131?flowToken=1042316
(点击链接免费报名,先关注,不迷路)
什么是FFmpeg
FFmpeg 是一款知名的开源音视频处理软件,它提供了丰富而友好的接口支持开发者进行二次开发,也就是说,我们可以把 FFmpeg 看作是一个跨平台的视频处理程序:
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的 音频/视频编解码库libavcodec。
FFmpeg中的 “FF” 指的是 “Fast Forward”,“mpeg” 则是 “Moving Picture Experts Group”
FFmpeg的原理
FFmpeg对音视频的处理过程可以简概为:解复用 => 解码 => 编码 => 复用器。
FFmpeg的使用
常见情况下使用 FFmpeg 首先要在当前系统配置 FFmpeg 环境,也就是对 FFmpeg 工具进行安装和配置,环境配置完成之后就可以使用命令行工具进行 FFmpeg 的调用。
FFmpeg部分简单的命令行操作示例:
获取音视频文件信息:
$ ffmpeg -i video.mp4
转换视频文件格式(转换 mp4 文件到 avi 文件):
$ ffmpeg -i video.mp4 video.avi
更改视频文件分辨率:
$ ffmpeg -i input.mp4 -filter:v scale=1280:720 -c:a copy output.mp4
视频中提取图像:
$ ffmpeg -i input.mp4 -r 1 -f image2 image-%3d.png
当然,FFmpeg 还可以进行更多的操作,在此不进行更多的举例,感兴趣的同学可以参考官方文档(http://www.ffmpeg.org/)
FFmpeg在Node.js中的应用
在Node.js中有一个非常好用的模块,它就是 fluent-ffmpeg:
This library abstracts the complex command-line usage of ffmpeg into a fluent, easy to use node.js module. In order to be able to use this module, make sure you have ffmpeg installed on your system (including all necessary encoding libraries like libmp3lame or libx264).
简而言之 fluent-ffmpeg 对 FFmpeg 复杂的命令行进行了一定的封装,抽象为我们使用起来非常舒服的各类方法和API,可以看下它的一些常见操作:
fluent-ffmpeg的部分简单示例:
指定输入:
ffmpeg('input1.avi').input('input2.avi').input(fs.createReadStream('input3.avi'));
音频选项:
// 禁用音频
ffmpeg('input1.avi').noAudio();// 设置音频比特率
ffmpeg('input1.avi').audioBitrate(128);// 设置音频频率
ffmpeg('input1.avi').audioFrequency(22050);
视频选项:
// 设置编解码器
ffmpeg('input1.avi').videoCodec('libx264');// 设置输出帧大小和纵横比
ffmpeg('input1.avi').size('640x?').aspect('4:3');
远程环境使用fluent-ffmpeg(@ffmpeg-installer/ffmpeg)
了解到 fluent-ffmpeg 之后,我们会发现它简介中提到的重要一点(make sure you have ffmpeg installed on your system (including all necessary encoding libraries like libmp3lame or libx264).),请确保在系统上安装了 ffmpeg (包括所有必要的编码库,如libmp3lame或libx264)。那么随之而来的一个问题就是,当我把服务部署到远程机器时,远程机器如果没有安装过FFmpeg环境怎么办。
解决这个问题我们用到的是另一个库 @ffmpeg-installer/ffmpeg:
Installs a binary of ffmpeg for the current platform and provides a path and version. Supports Linux, Windows and Mac OS/X.
@ffmpeg-installer/ffmpeg 能为当前平台安装 FFmpeg 二进制文件,让我们具备在多个环境中去调用 FFmpeg 的能力。它和 fluent-ffmpeg 结合使用,只需如下操作:
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
const ffmpeg = require('fluent-ffmpeg');
ffmpeg.setFfmpegPath(ffmpegPath);
将图片合成为视频
下面就是我们想要使用 FFmpeg 进行的工作,将多张不同大小尺寸的图片合成为带有一定动画切换效果的视频:
(为方便上传,视频转换成了gif)
整个过程分为以下几步:
1、输入被操作对象
确定基础视频和目标视频,并输入基础视频。这个步骤主要就是确认了输入和输出的目标。
// 基础视频
const baseVideo = path.join(__dirname, '../assets/baseVideo30.mp4')// 目标视频
const savePath = path.join(__dirname, '../assets/temp-video.mp4')// 基础视频输入
let baseInput = await ffmpeg().input(baseVideo)
2、图片输入
方法和视频的输入相同,因为存在多张图片,所以使用for循环输入
for (let i = 0; i < img_list.length; i++) { baseInput = await baseInput.input(img_list[i])
}
3、complexFilter动画处理
实现这个功能的主要逻辑都在于动画部分,要计算好我们如何让多张图片进行有规律的切换,然后将动画规律嵌套到代码内。上面动画的规则其实就是每张图片都从最左侧运动进入居中位置,停留数秒后,向下方运动直到出视频外。
complexFilter 方法允许为命令设置复杂的 filtergraph 。这个API其实对应原生 FFmpeg 的-filter_complex命令,-filter_complex 可以帮助我们实现加字幕、裁剪、缩放、旋转等。我们这里使用的就是 -filter_complex 对输入对象的控制和处理能力。
首先需要处理图片的大小,让不同比例的图片都能缩放为可以居中并且全部展示的大小,如果我们不对图片进行大小处理的话,默认情况会使用图片的原有大小(即使图片大小超出了视频大小范围)。
处理图片和视频大小使用的是 scale 滤镜,首先将视频比例设置为宽高640、480:
let complexFilter = '[0:v]scale=w=640:h=480[videobase];'
上面的代码我们进行一下拆分解释:
// 0-操作对象编号 v-对象内视频信息
'[0:v]'// scale滤镜,设置输入目标的宽高
'scale=w=640:h=480'// outputs输出流 相当于对当前操作后的对象进行标记
'[videobase]'
将图片大小根据视频比例进行宽高设置:
const videoWidth = 640 // 视频宽
const videoHeight = 480 // 视频高
for (let i = 0; i < img_list.length; i++) {complexFilter += `[${i + 1}:v]scale=w='iw*min(${videoWidth}/iw,${videoHeight}/ih)':h='ih*min(${videoWidth}/iw,${videoHeight}/ih)'[img${i + 1}];`
}
上面的代码我们进行一下拆分解释,
// 此处同[0:v],但是0代表我们第一个输入(base视频),所以根据for循环,每次累加得到输入的图片
'[${i + 1}:v]'// iw ih 就是 inputs width 和 inputs height,代表当前操作的图片的原始宽高
// 此处我们将两个值与视频宽高分别对比,并使用 min() 取最小值进行缩放。
'scale=w='iw*min(${videoWidth}/iw,${videoHeight}/ih)':h='ih*min(${videoWidth}/iw,${videoHeight}/ih)''// 同为 outputs 输出标识,即经过此次循环,我们得到了 img1, img2, img3 ..... 等输出对象
'[img${i + 1}]'
视频和图片的大小比例处理完成后我们使用 overlay 滤镜进行动画处理,其实就是将图片在视频内进行位移。overlay 的能力就是覆盖,将多个输入源进行相互覆盖处理。我们来看这部分的全部代码:
// imgInterval 是通过图片数量和视频长度计算的每张图片展示间隔。
for (let i = 0; i < img_list.length; i++) { // 输入图片的动画控制if (i === 0) {complexFilter += `[videobase][img${i + 1}]overlay='main_w/2-overlay_w/2':'if(gte(t, ${(i + 1) * imgInterval}), min(main_h/2-overlay_h/2+(t-${(i + 1) * imgInterval})*900,main_h), main_h/2-overlay_h/2)'${i === img_list.length - 1 ? '' : `[a${i}];`}`} else {complexFilter += `[a${i - 1}][img${i + 1}]overlay='if(gte(t, ${i * imgInterval}), min(-overlay_w+(t-${i * imgInterval})*900,main_w/2-overlay_w/2),NAN)':'if(gte(t, ${(i + 1) * imgInterval}), min(main_h/2-overlay_h/2+(t-${(i + 1) * imgInterval})*900,main_h), main_h/2-overlay_h/2)'${i === img_list.length - 1 ? '' : `[a${i}];`}`}
}
我们将以上代码拆解,首先,第一张图片默认视频封面,也就是没有入场效果,直接在视频中心的,所以当 i === 0 时单独处理,根据时间判断,当停留时间到达后,会以每秒钟900的速度从下方移动出视频:
// videobase 为上面标记的视频对象,img1 为上面标记的第一张图片对象
// 此处的能力就是将输入对象 img1 覆盖在 videobase 上
'[videobase][img${i + 1}]'// overlay=x(横坐标轴相关操作):y(纵坐标轴相关操作)
// 第一张图片x轴不需要进行移动
'overlay='main_w/2-overlay_w/2''// FFmpeg 的 if语句: if (条件, 条件成立, 条件不成立)
// main_w - 整个视频的宽度
// overlay_w - 当前操控的输入对象(图片)的宽度
// gte(t, ${(i + 1) * imgInterval}) t为当前时间,通过时间控制是否移动
// min(main_h/2-overlay_h/2+(t-${(i + 1) * imgInterval})*900,main_h) 当前高度,900为移动速度(与时间正比), main_h就是临界值
'if(gte(t, ${(i + 1) * imgInterval}), min(main_h/2-overlay_h/2+(t-${(i + 1) * imgInterval})*900,main_h), main_h/2-overlay_h/2)'
非第一行图片时增加了x轴的处理,根据图片排名和图片停留时间计算出开始运动的时间,并由此刻从左侧进入视频。在居中位置停留后,由下方移出,方法同以上y轴的移动方法:
// 覆盖标识
'[a${i - 1}][img${i + 1}]'// 同上解释,main_w/2-overlay_w/2 为x轴移动的临界值,也就是居中位置, 900 就是x轴与时间成正比的速度
'if(gte(t, ${i * imgInterval}), min(-overlay_w+(t-${i * imgInterval})*900,main_w/2-overlay_w/2),NAN)'
需要注意的地方就是每个图片滤镜完成后的输出标识,此标识需出现在下一张图片的被覆盖对象位置上:
'[a${i - 1}][img${i + 1}]' // [a${i - 1}] 即上一张图片的输出标识
滤镜添加完毕后我们就已经完成了主要的工作,整个动画效果都已经衔接在了一起,最后得到的 complexFilter 命令如下所示:
'[0:v]scale=w=640:h=480[videobase];[1:v]scale=w='iw*min(640/iw,480/ih)':h='ih*min(640/iw,480/ih)'[img1];[2:v]scale=w='iw*min(640/iw,480/ih)':h='ih*min(640/iw,480/ih)'[img2];[3:v]scale=w='iw*min(640/iw,480/ih)':h='ih*min(640/iw,480/ih)'[img3];[4:v]scale=w='iw*min(640/iw,480/ih)':h='ih*min(640/iw,480/ih)'[img4];[videobase][img1]overlay='main_w/2-overlay_w/2':'if(gte(t, 3.75), min(main_h/2-overlay_h/2+(t-3.75)*900,main_h), main_h/2-overlay_h/2)'[a0];[a0][img2]overlay='if(gte(t, 3.75), min(-overlay_w+(t-3.75)*900,main_w/2-overlay_w/2),NAN)':'if(gte(t, 7.5), min(main_h/2-overlay_h/2+(t-7.5)*900,main_h), main_h/2-overlay_h/2)'[a1];[a1][img3]overlay='if(gte(t, 7.5), min(-overlay_w+(t-7.5)*900,main_w/2-overlay_w/2),NAN)':'if(gte(t, 11.25), min(main_h/2-overlay_h/2+(t-11.25)*900,main_h), main_h/2-overlay_h/2)'[a2];[a2][img4]overlay='if(gte(t, 11.25), min(-overlay_w+(t-11.25)*900,main_w/2-overlay_w/2),NAN)':'if(gte(t, 15), min(main_h/2-overlay_h/2+(t-15)*900,main_h), main_h/2-overlay_h/2)''
最后我们可以进行一些其他操作,并设置输出路径
baseInput.complexFilter([ // 上面的滤镜complexFilter,]).videoBitrate('2048k') // 比特率.aspect('4:3') // 视频比例.duration(video_long) // 视频停止时间.on('end', () => { // 视频处理完成console.log('video one end');taskInfo.savePath = savePath;resolve();}).on('error', (error) => { // 视频处理失败console.log('an error happend: create one video' + error);reject(error);}).save(savePath); // 保存路径
})
注意mp4的编码问题
大部分同学对MP4的理解是后缀为 .mp4 的文件,但其实MP4有非常复杂的含义(参考MPEG-4 Part 14(http://en.wikipedia.org/wiki/Mp4)),它本身不是一种简单的视频格式,而是一个包装了视频和音频格式的容器。MP4的视频格式可以有 DivX 也可有 H264,vp8,vp9,theora。
每个浏览器因为专利费等原因对不同格式的视频支持情况也不相同,具体可以参考HTML5 video(https://en.wikipedia.org/wiki/HTML5_video)
所以在此次需求中,就遇到了视频无法在浏览器播放的问题,原因就是一开始的basevideo是直接由 FFmpeg 默认产生。为了解决这个问题,我们使用了一个底层编码为 H264 格式的底视频。这里如果各位同学对 FFmpeg 有深入研究,有更好的解决方案的话,欢迎提供其他解决思路~
总结
以上就是本次对 FFmpeg 的一些介绍和实际开发中的使用,这只是 FFmpeg 的冰山一角,它还有很多更加强大的能力,大家如果对音视频感兴趣可以深入进行学习。
当然,如果对本文中实际的解决方案有疑问或者有更好的建议,欢迎进行讨论~