java如何将图片转为MP4视频并配音

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 [email protected]
 * @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.不管了,摸鱼去了

7.记录

用以前的方法生成视频会导致视频无法播放所以更新了一下代码

//生成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);
        }
    }

猜你喜欢

转载自blog.csdn.net/weixin_52016779/article/details/129837415