1.放在前面
最近前端小伙伴给我提了一个难题,让我在后端实现一个图片转MP4视频并配音乐,然后提供一个接口给他下载,我虽然没做过相关的功能,但本着不会就抄的的原则,还是硬着头皮答应了…
话不多说,开整!
2.引入依赖
首先我先在网上搜搜有没有相关demo,还真搜到了
这里附上原文链接 https://cloud.tencent.com/developer/article/1640244
引入相关依赖
<!--图片转MP4-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>4.4-1.5.6</version>
</dependency>
新建类
package cn.hjljy.javacv;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
/**
* @author 海加尔金鹰 www.hjljy.cn
* @version V1.0
* @email hjljy@outlook.com
* @description: 图片合成MP4
* @since 2020/5/16 18:00
**/
public class Image2Mp4 {
public static void main(String[] args) throws Exception {
//合成的MP4
String mp4SavePath = "D:\\javacv\\mp4\\img.mp4";
//图片地址 这里面放了22张图片
String img = "D:\\javacv\\img";
int width = 1600;
int height = 900;
//读取所有图片
File file = new File(img);
File[] files = file.listFiles();
Map<Integer, File> imgMap = new HashMap<Integer, File>();
int num = 0;
for (File imgFile : files) {
imgMap.put(num, imgFile);
num++;
}
createMp4(mp4SavePath, imgMap, width, height);
}
private static void createMp4(String mp4SavePath, Map<Integer, File> imgMap, int width, int height) throws FrameRecorder.Exception {
//视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, width, height);
//设置视频编码层模式
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//设置视频为25帧每秒
recorder.setFrameRate(25);
//设置视频图像数据格式
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setFormat("mp4");
try {
recorder.start();
Java2DFrameConverter converter = new Java2DFrameConverter();
//录制一个22秒的视频
for (int i = 0; i < 22; i++) {
BufferedImage read = ImageIO.read(imgMap.get(i));
//一秒是25帧 所以要记录25次
for (int j = 0; j < 25; j++) {
recorder.record(converter.getFrame(read));
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定要结束并释放资源
recorder.stop();
recorder.release();
}
}
}
然后融合音乐
public static boolean mergeAudioAndVideo(String videoPath, String audioPath, String outPut) throws Exception {
boolean isCreated = true;
File file = new File(videoPath);
if (!file.exists()) {
return false;
}
FrameRecorder recorder = null;
FrameGrabber grabber1 = null;
FrameGrabber grabber2 = null;
try {
//抓取视频帧
grabber1 = new FFmpegFrameGrabber(videoPath);
//抓取音频帧
grabber2 = new FFmpegFrameGrabber(audioPath);
grabber1.start();
grabber2.start();
//创建录制
recorder = new FFmpegFrameRecorder(outPut,
grabber1.getImageWidth(), grabber1.getImageHeight(),
grabber2.getAudioChannels());
recorder.setFormat("mp4");
recorder.setFrameRate(grabber1.getFrameRate());
recorder.setSampleRate(grabber2.getSampleRate());
recorder.start();
Frame frame1;
Frame frame2 ;
//先录入视频
while ((frame1 = grabber1.grabFrame()) != null ){
recorder.record(frame1);
}
//然后录入音频
while ((frame2 = grabber2.grabFrame()) != null) {
recorder.record(frame2);
}
grabber1.stop();
grabber2.stop();
recorder.stop();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (recorder != null) {
recorder.release();
}
if (grabber1 != null) {
grabber1.release();
}
if (grabber2 != null) {
grabber2.release();
}
} catch (FrameRecorder.Exception e) {
e.printStackTrace();
}
}
return isCreated;
}
3.开始测试
我根据网上的demo开始写了一个demo,然而不出意外的话就要出意外了…
图片确实转为MP4视频了,也是1秒一张,但是生成的视频像是加了红色滤镜一样…
一通百度加源码(当然看不懂)操作下来,最终在StackOverflow找到了解决方法,更改解析模式就可以了,一次不行就n次
(附上我写方法)
//生成MP4视频
private String createMp4(String mp4SavePath, List<File> list, int width, int height) throws Exception {
String mp3Name = "";
//视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, width, height);
// 文件格式
recorder.setFormat("mp4");
// 帧率与抓取器一致
recorder.setFrameRate(25);
// 编码器类型
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
try {
recorder.start();
Java2DFrameConverter converter = new Java2DFrameConverter();
for (File file : list) {
if (file.getName().contains(".mp3")) {
mp3Name = file.getName();
continue;
}
BufferedImage read = null;
try {
read = ImageIO.read(file);
//循环的数量等于帧率 保证每秒一张图片。
for (int i = 0; i < recorder.getFrameRate(); i++) {
//编码格式 ##这里解决生成视频颜色不对问题
recorder.record(converter.getFrame(read), avutil.AV_PIX_FMT_RGB32_1);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定要结束并释放资源
recorder.stop();
recorder.release();
}
return mp3Name;
}
网上的demo中是这样写的 recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
我借鉴(抄的)StackOverflow的大神在每张图片解析的时候在指定编码格式:
recorder.record(converter.getFrame(read), avutil.AV_PIX_FMT_RGB32_1);
4.融合音乐
好了进行下一步,配音…
然而根据这个文章的提示:
1 视频长度和音频长度尽量保持一致,如果不一致,合成的视频长度会以最长的为准,音频短,后面就自然缺失音频,视频短,后面的视频会呈现视频的最后一帧。
2 不建议录一帧视频然后录一帧音频,音频的后半段会丢失,比例差不多是1:1.6!!!
确实如这位大哥所写的一样,视频和音频时长不一致会导致生成的MP4出问题,所以我用我聪明的脑袋瓜迅速解决了这个问题…
5.完整示例
5.1 控制层
//上传图片转MP4
@PostMapping("/upload/mp4/transfer")
public ResponseEntity<InputStreamResource> uploadMP4(@RequestParam("files") MultipartFile[] files) throws Exception {
return imgManagerService.uploadMP4(files);
}
5.2业务层
//将图片转为MP4图片
@Override
public ResponseEntity<InputStreamResource> uploadMP4(MultipartFile[] files) throws Exception {
log.info("=======生成MP4开始========");
String uuid = IdGen.uuid();
for (MultipartFile file : files) {
getPathAndSaveFile(file, uuid);//上传到服务器的指定目录
}
// 图片集合的目录
String imagesPath = rootPath + uuid + "/";
String saveMp4name = imagesPath + "source.mp4"; //保存的视频名称
List<File> list = FileUtil.readFile(imagesPath);
int width = 1849;
int height = 932;
//返回MP3名字
String mp3 = createMp4(saveMp4name, list, width, height);
String outPutName = imagesPath + uuid + ".mp4";
mergeAudioAndVideo(saveMp4name, StringUtils.isBlank(mp3) ? null : imagesPath + mp3, outPutName);
log.info("=======生成MP4结束========");
File file = new File(outPutName);
InputStreamResource inputStreamResource = new InputStreamResource(new FileInputStream(file));
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"")
.contentType(MediaType.valueOf("audio/mp4"))
.body(inputStreamResource);
}
其中涉及的方法:
1.getPathAndSaveFile(file, uuid) #file为上传的文件 #uuid为单次请求的标识
private String getPathAndSaveFile(MultipartFile file, String taskId) throws IOException {
String path = rootPath + taskId + "/" + file.getOriginalFilename();
File newFile = new File(path);
FileUtils.copyInputStreamToFile(file.getInputStream(), newFile);
return path;
}
2.FileUtil.readFile(imagesPath) #imagesPath 保存到服务器的图片路径
/**
* 读取文件
*
* @param imgUrl 文件路径
* @throws IOException
*/
public static List<File> readFile(String imgUrl) throws IOException {
//读取所有图片
File file = new File(imgUrl);
return Arrays.stream(Objects.requireNonNull(file.listFiles())).collect(Collectors.toList());
}
3.生成MP4视频的方法为 createMp4(saveMp4name, list, width, height)
#saveMp4name 保存的视频名称
#list 文件列表
#width height生成的视频宽高 最好和图片一致
//生成MP4视频
private String createMp4(String mp4SavePath, List<File> list, int width, int height) throws Exception {
String mp3Name = "";
//视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, width, height);
// 文件格式
recorder.setFormat("mp4");
// 帧率与抓取器一致
recorder.setFrameRate(25);
// 编码器类型
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
try {
recorder.start();
Java2DFrameConverter converter = new Java2DFrameConverter();
for (File file : list) {
if (file.getName().contains(".mp3")) {
mp3Name = file.getName();
continue;
}
BufferedImage read = null;
try {
read = ImageIO.read(file);
//循环的数量等于帧率 保证每秒一张图片。
for (int i = 0; i < recorder.getFrameRate(); i++) {
//编码格式
recorder.record(converter.getFrame(read), avutil.AV_PIX_FMT_RGB32_1);
}
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定要结束并释放资源
recorder.stop();
recorder.release();
}
return mp3Name;
}
4.配音…
mergeAudioAndVideo(saveMp4name, StringUtils.isBlank(mp3) ? null : imagesPath + mp3, outPutName);
saveMp4name #要操作的MP4视频
StringUtils.isBlank(mp3) ? null : imagesPath + mp3 #要操作的MP3音乐 没有MP3就传null
outPutName #输出的目录
//配音
public void mergeAudioAndVideo(String videoPath, String audioPath, String outPut) throws Exception {
//没有音频文件 直接返回原文件
if (null == audioPath){
new File(videoPath).renameTo(new File(outPut));
return;
}
FFmpegFrameRecorder recorder = null;
FrameGrabber grabber1 = null;
FrameGrabber grabber2 = null;
try {
//抓取视频帧
grabber1 = new FFmpegFrameGrabber(videoPath);
//抓取音频帧
grabber2 = new FFmpegFrameGrabber(audioPath);
grabber1.start();
grabber2.start();
//创建录制
recorder = new FFmpegFrameRecorder(outPut,
grabber1.getImageWidth(), grabber1.getImageHeight(),
grabber2.getAudioChannels());
recorder.setFormat("mp4");
recorder.setFrameRate(grabber1.getFrameRate());
recorder.setSampleRate(grabber2.getSampleRate());
// 视频质量,0表示无损
recorder.setVideoQuality(0);
recorder.start();
Frame frame1;
Frame frame2 = null;
//先录入视频
while ((frame1 = grabber1.grabFrame()) != null) {
recorder.record(frame1);
}
//然后录入音频
audioEntry(frame2, grabber1, grabber2, recorder);
grabber1.stop();
grabber2.stop();
recorder.stop();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (recorder != null) {
recorder.release();
}
if (grabber1 != null) {
grabber1.release();
}
if (grabber2 != null) {
grabber2.release();
}
} catch (FrameRecorder.Exception e) {
e.printStackTrace();
}
}
}
5.录入音频的方法
audioEntry(frame2, grabber1, grabber2, recorder);
#frame2 音频帧
#grabber1 视频的信息
#grabber2 音乐的信息
#recorder 操作工具
private void audioEntry(Frame frame, FrameGrabber grabber1, FrameGrabber grabber2, FrameRecorder recorder) throws Exception {
long grabber1Timestamp = grabber1.getTimestamp();
while ((frame = grabber2.grabFrame()) != null) {
//如果视频时长小于音频时长 则截断音频帧
if (grabber1Timestamp <= grabber2.getTimestamp()) break;
recorder.record(frame);
}
long differ = grabber1Timestamp - grabber2.getTimestamp();
//如果视频时长大于音频时长 则循环录入
if (differ > 0) {
grabber1.setTimestamp(differ);
audioEntry(frame, grabber1, grabber2, recorder);
}
}
6.写在最后
这样这个接口写出来,前端小伙伴直接夸我666,当然后这里代码还优化的空间…
1.生成MP4视频和配音的解码器是否可以共用(当然我没想出来)
2.如果存在多个音频文件则需要再次修改代码
3.不管了,摸鱼去了文章来源:https://www.toymoban.com/news/detail-559687.html
7.记录
用以前的方法生成视频会导致视频无法播放所以更新了一下代码文章来源地址https://www.toymoban.com/news/detail-559687.html
//生成MP4视频
private void createMp4(String mp4SavePath, List<File> list, int width, int height) throws Exception {
String mp3Name = "";
//视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, width, height);
//MP4格式设置成H264,才能在html上播放
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
// 设置格式mp4
recorder.setFormat("mp4");
// 此处说明每一秒多少帧,即说明1秒会录多少张照片
recorder.setFrameRate(1); //0.01 代表100秒一张图
// 8000kb/s 这个说明视频每秒大小,值越大图片转过来的压缩率就越小质量就会越高
recorder.setVideoBitrate(80000);//80000000
// yuv420p
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
// 先默认吧,这个应该属于设置视频的处理模式 不可变(固定)音频比特率
recorder.setAudioOption("crf", "0");
// 最高质量
recorder.setAudioQuality(0);
// 音频比特率
recorder.setAudioBitrate(192000);
// 音频采样率
recorder.setSampleRate(44100);
// 双通道(立体声)
recorder.setAudioChannels(2);
// ------------------->end 初始化视频录制器
try {
recorder.start();// 开始录制
// ------------------->begin 图片处理开始
OpenCVFrameConverter.ToIplImage conveter = new OpenCVFrameConverter.ToIplImage(); // 申明一个图片处理的变量
for (File file : list) {
if (file.getName().contains(".mp3")) {
mp3Name = file.getPath();
continue;
}
IplImage image = opencv_imgcodecs.cvLoadImage(file.getPath()); // 非常吃内存!!
Frame frame = conveter.convert(image);
recorder.record(frame); // 录制
// 释放内存 非常吃内存!!
opencv_core.cvReleaseImage(image);
}
// ------------------->end 图片处理开始
// ------------------->begin 开始录制音频
if (StringUtils.isNotBlank(mp3Name)) {
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(mp3Name);
grabber.start();// 开始录制音频
audioEntry(null, grabber, recorder, recorder.getTimestamp());
// ------------------->end 开始录制音频
grabber.stop();
grabber.release();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定要结束并释放资源
recorder.stop();
recorder.release();
}
}
//录入音频 mp3Grabber 音频帧 frame音频源 recorder 解码器 recorderTimestamp 视频总长
private void audioEntry(Frame frame, FFmpegFrameGrabber mp3Grabber, FFmpegFrameRecorder recorder, long recorderTimestamp) throws Exception {
while ((frame = mp3Grabber.grabFrame()) != null) {
//如果视频时长小于音频时长 则截断音频帧
long timestamp = mp3Grabber.getTimestamp();
if (recorderTimestamp <= mp3Grabber.getTimestamp()) break;
recorder.record(frame);
}
long differ = recorderTimestamp - mp3Grabber.getTimestamp();
mp3Grabber.setTimestamp(0);
//如果视频时长大于音频时长 则循环录入
if (differ > 0) {
audioEntry(frame, mp3Grabber, recorder, differ);
}
}
到了这里,关于java如何将图片转为MP4视频并配音的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!