FFmpeg初探——基于FFmpeg的图片合成视频

article/2025/10/28 21:41:39

cf79698e2830aadff8c43a0dce2e415d.png

前言

商家在发布商品的时候,大部分情况下是没有视频的,这样往往会造成商品展示不全等问题,而视频制作又比较麻烦,为了解决此痛点,我们需要提供一键合成视频的功能。

之所以选择 FFmpeg,是因为我们期望后续能够进行视频剪辑、字幕添加等更复杂的音视频操作。下面我们就来了解下什么是 FFmpeg。

什么是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 进行的工作,将多张不同大小尺寸的图片合成为带有一定动画切换效果的视频:cdb1163114cd3ecdb5c8cbac0ca6d89b.png8eb4eb801e0295cde4e2d30ff533773e.png02bba4f7a5d5ce005e554211c70c3c30.png1be522a58968179ec71fc9216f0c2891.png

👇👇👇👇👇👇👇👇👇 (为方便上传,视频转换成了gif)

68276a5bea8f61cc999d8d7ebea19990.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 的冰山一角,它还有很多更加强大的能力,大家如果对音视频感兴趣可以深入进行学习。

当然,如果对本文中实际的解决方案有疑问或者有更好的建议,欢迎进行讨论~

参考文献

FFmpeg文档(http://www.ffmpeg.org/)

npm fluent-ffmpeg(https://www.npmjs.com/package/fluent-ffmpeg)

npm @ffmpeg-installer/ffmpe(https://www.npmjs.com/package/@ffmpeg-installer/ffmpeg)

837222a276b7b9d6cc02a71f2804b2ed.png

大转转FE

喜欢就加入我们吧


http://chatgpt.dhexx.cn/article/yf7qWIEm.shtml

相关文章

视频照片合成软件哪个好?快速把手机照片做成视频,简单操作,效果精美!

视频照片合成软件哪个好&#xff1f;怎么把照片合成视频&#xff1f;如何快速把手机照片做成视频&#xff1f; 这是我用数码大师把手机照片合成视频的效果截图&#xff1a; 第一步&#xff1a;快速导入多张照片&#xff0c;为照片配上文字 点击“添加相片”就能快速导入照片…

ffmpeg图片+音频合成视频

命令如下&#xff0c;个人纪录 ffmpeg -framerate 0.05 -f image2 -loop 1 -y -i d:/img/img%d.jpg -i d:/img/gyz.mp3 -s 1080*1920 -r 25 -t 100 d:/img/output.mp4 -framerate 速率&#xff0c;越小每张图片停留时间越长 -loop 循环一遍文件夹内的图片 -i 图片路径&#x…

用php把图片合成视频,图片音乐合成视频 多张图片合成视频|图片合成视频软件...

在网络上我们经常见到的电子相册其本质就是图片音乐合成视频&#xff0c;使用一些图片合成视频软件将多张图片合成视频&#xff0c;外加点炫酷的转场特效&#xff0c;so easy的就能完成了。o(*≧▽≦)ツ 想不想知道具体的操作过程&#xff1f;有兴趣的童鞋可以看看下文的~ 这是…

电脑图片合成视频用什么软件?3分钟快速教程,多张图片做成精美视频!

电脑图片合成视频怎么做&#xff1f;图片视频制作用什么软件好&#xff1f;现在大家的照片或图片很多&#xff0c;其实在电脑上把图片做成视频是非常方便的&#xff0c;还能整理好照片&#xff0c;节省空间&#xff0c;图片/照片视频看起来也更加美观。今天直接用数码大师教大家…

当请求类型是octet-stream时,SpringBoot 如何完成文件上传

一、问题背景 这个问题困扰了我一上午&#xff0c;搜索了很多博客&#xff0c;发现网上的springboot都是使用Multipart来接收文件&#xff0c;而客户端使用的是binary&#xff0c;用二进制流来上传文件的&#xff0c;下面记录一下我的解决历程。 二、基础知识 一个请求的参数…

No converter for [class xxx] Content-Type ‘appliction/octet-stream;charset=UTF-8‘ 的解决办法

报错的类&#xff1a;AbstractMessageConverterMethodProcessor 报错的代码块 报错原因 respose在被传入其他的方法后&#xff0c;其content type 被篡改了&#xff0c;导致与request 的content type 不一致导致的 解决方案一&#xff1a;将方法直接return null; 解决方案二&…

vue2后端返回application/octet-stream这个类型的文件,前端实现下载。

描述&#xff1a;调用接口以后&#xff0c;后台返回的数据 前端实现&#xff0c;下载功能。 前端打印res&#xff1a; 那么接口必须加上这个 页面下载&#xff1a; downloadFunc(data) {const date new Date(new Date() 8 * 3600 * 1000).toISOString().replace(/T/g, ).re…

处理Nginx返回octet-stream数据流的配置

解决 修改Nginx的配置将add_header Content-length 0&#xff1b;删除&#xff0c;处理 Content-Type为application/octet-stream 一、请求报文 二、异常信息 对应前端页面的异常信息为&#xff1a; Network Error epoll_wait() reported that client prematurely closed c…

请求状态为200,前端报系统出错,后端日志报“Content type ‘application/octet-stream‘not supported“错误

请求后端出现"Content type application/octet-stream‘not supported“错误 错误描述&#xff1a; Content type application/octet-stream‘not supported&#xff0c;即内容类型‘application/octet-stream’不支持。 而我们传的参数需要application/octet-stream类型…

No converter for XXX with preset Content-Type ‘application/octet-stream;charset=UTF-8‘

org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class com.lzy.common.core.config.Result] with preset Content-Type ‘application/octet-stream;charsetUTF-8’ 转载&#xff1a;https://blog.csdn.net/u012377333/article/deta…

octet-stream文件怎么看

抓到一个报文中出现文件的类型是octet-stream,一脸懵比&#xff0c;好吧&#xff0c;只能查看下&#xff0c;原来是二进制流&#xff0c;还得再细细品味&#xff01; 借鉴URL&#xff1a; http://tool.oschina.net/commons

Octree

Octree (by J. Eder 9225396) What is an Octree? Using an Octree Types of Octrees What does it represent? How is it managed? How to structure Octree-structures Pointerless full Octree Traditional design of Pointer Octrees Branch-On-Need-Octree …

SpringBoot:使用application/octet-stream 流式上传大文件,解决Mutipartfile multipart/form-data上传产生临时文件问题

目录 multipart原理简介multipart的流是服务器临时文件流multipartfile生成临时文件到默认文件夹临时文件的目录可配Mutipartfile的文件默认来自临时文件 Mutipartfile产生临时文件的好处坏处好处坏处 纯流式上传application/octet-stream multipart原理简介 multipart的流是服…

请求后端出现“Content type ‘application/octet-stream‘not supported“错误解决方案

首先看报错。此报错是Springboot 报错。 看看Postman 正确的传递方式。 Vue应该怎么传递呢&#xff1f;使用 FormData 对象。 // 通过这个方式就可以指定 ContentType 了 form_data.append(req, new Blob([JSON.stringify({a: 1, b: 2})], {type: application/json}))说明…

crontab

1、先手动执行定时任务以此来判断脚本是否有问题。 2、确认服务器是否开启定时任务计划服务 命令&#xff1a;service crond status service crond start 3、检查定时任务配置的语法 crontab -l SHELL/bin/bash 0 15 * * 1-5 cd /root/ye/project/StatShareDataProject/&…

记 Content type ‘application/octet-stream‘ not supported

项目场景&#xff1a; 实现一个入参方式为RequestPart RequestParam(“files”)的接口&#xff0c;即该接口要包含文件上传和其它(实体类)入参。 示例代码&#xff1a; PostMapping(value "", consumes {"multipart/form-data"})public Map<String,…

No converter for [class XXX] with preset Content-Type ‘application/octet-stream;charset=UTF-8‘

场景 此接口是下载文件&#xff0c;但逻辑中存在异常情况&#xff0c;并且响应对象设置了此响应头&#xff1a; response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);当内部抛出了异常后并且给了一个通用的实体对象&#xff0c;是JSON格…

Octet 和 Byte 的区别

2019独角兽企业重金招聘Python工程师标准>>> 关于程序的文章中 Octet 和 Byte是常见的词汇&#xff0c; 他们都表示8 bit。在读RFC或网络设备文档时&#xff0c;经常见到Octet这量词来指代8位&#xff08;bit&#xff09;&#xff0c;为啥不用Byte呢&#xff1f;有啥…

关于ESI研究前沿的思考和使用方法研究

边文越 李国鹏 周秋菊 冷伏海中国科学院科技战略咨询研究院。原文发表于《情报学报》DOI&#xff1a;10.3772/j.issn.1000-0135.2022.03.004 摘要 近年来&#xff0c;Essential Science Indicators&#xff08;ESI&#xff09;数据库研究前沿成为国内外情报学界的研究热点…