SpringBoot集成FFmpeg实现视频缩略图与gif生成

0 阅读1分钟

一、导入pom依赖

    <profiles>
        <profile>
            <id>local</id>
            <properties>
                <profiles.active>local</profiles.active>
                <!--根据环境进行切换-->
                <ffmpeg.classifier>windows-x86_64</ffmpeg.classifier>
<!--                <ffmpeg.classifier>linux-x86_64</ffmpeg.classifier>-->
            </properties>
        </profile>
    </profiles> 

        <!-- javacv+javacpp核心库-->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>${javacv.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacpp-platform</artifactId>
            <version>${javacv.version}</version>
        </dependency>

        <!-- ffmpeg最小依赖包,必须包含上面的javacv+javacpp核心库 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>ffmpeg</artifactId>
            <version>5.1.2-${javacv.version}</version>
            <classifier>${ffmpeg.classifier}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacpp</artifactId>
            <version>${javacv.version}</version>
            <classifier>${ffmpeg.classifier}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>openblas</artifactId>
            <version>0.3.21-${javacv.version}</version>
            <classifier>${ffmpeg.classifier}</classifier>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>opencv</artifactId>
            <version>4.6.0-${javacv.version}</version>
            <classifier>${ffmpeg.classifier}</classifier>
        </dependency>

二、预览图生成代码

package com.czi.dcp.biz.eventquickprocess.service;

import cn.hutool.core.img.gif.AnimatedGifEncoder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

import java.awt.image.BufferedImage;
import java.io.*;
import java.util.UUID;

/**
 * 视频缩略图处理器
 *
 * @author god_cvz
 */
@Component
@Slf4j
public class VideoPreviewProcessor {

    public void asyncProcess(String videoUrl) {
        new Thread(() -> process(videoUrl)).start();
    }

    public void process(String videoUrl) {
        log.info("开始处理视频缩略图 videoUrl: {}", videoUrl);
        try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoUrl)) {
            grabber.start();
            Java2DFrameConverter converter = new Java2DFrameConverter();
            // 生成webp,动图
            generateWebp(grabber, converter);
            // 生成缩略图
            generateThumbnails(grabber, converter);
        } catch (Exception | Error e) {
            log.error("处理视频缩略图失败: {}", e.getMessage());
        }
    }

    private void generateThumbnails(FFmpegFrameGrabber grabber, Java2DFrameConverter converter) {
        // 缩略图宽度
        int thumbnailWith = 200;
        // 缩略图高度
        int thumbnailHeight = 150;
        // gif动图时长
        int gifDuration = 10;
        // gif动图质量
        int gifQuality = 10;
        String thumbnailsName = UUID.randomUUID() + ".gif";
        String thumbnailsObjectKey = "/bucket/gif" + thumbnailsName;
        uploadThumbnails(grabber, converter, thumbnailWith, thumbnailHeight, gifDuration, gifQuality, thumbnailsObjectKey);
    }

    private void uploadThumbnails(FFmpegFrameGrabber grabber, Java2DFrameConverter converter,
                                  Integer targetWidth, Integer targetHeight,
                                  Integer gifDuration, Integer gifQuality,
                                  String objectKey) {
        ByteArrayInputStream inputStream = null;
        try (ByteArrayOutputStream gifOutPutStream = new ByteArrayOutputStream()) {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            grabber.setFrameNumber(0);
            int targetFrameCount = (int) Math.min(gifDuration * grabber.getFrameRate(), grabber.getLengthInFrames());
            // 设置缩放比例
            int[] targetSize = getChangeSize(grabber.getImageWidth(), grabber.getImageHeight(), targetWidth, targetHeight);
            // 设置gif图像宽高
            grabber.setImageWidth(targetSize[0]);
            grabber.setImageHeight(targetSize[1]);
            // 使用AnimatedGifEncoder转换视频流
            AnimatedGifEncoder encoder = new AnimatedGifEncoder();
            encoder.start(gifOutPutStream);
            encoder.setDelay((int) (1000 / grabber.getFrameRate()));
            encoder.setFrameRate((float) grabber.getFrameRate());
            encoder.setQuality(gifQuality);
            encoder.setRepeat(0);

            for (int i = 0; i < targetFrameCount; i++) {
                Frame frame = grabber.grabImage();
                BufferedImage image = converter.convert(frame);
                encoder.addFrame(image);
            }

            encoder.finish();
            stopWatch.stop();
            inputStream = new ByteArrayInputStream(gifOutPutStream.toByteArray());
            // 上传oss
            this.uploadFile(objectKey, inputStream);
            log.info("生成缩略图成功: objectKey: {}, 处理耗时: {}s", objectKey, stopWatch.getTotalTimeSeconds());
        } catch (Exception e) {
            log.error("生成缩略图失败: objectKey: {} - 原因:{}", objectKey, e.getMessage());
        } finally {
            IoUtil.closeIfPosible(inputStream);
        }
    }

    private void generateWebp(FFmpegFrameGrabber grabber, Java2DFrameConverter converter) {
        // webp缩略图宽度
        int webpWidth = 720;
        // webp缩略图高度
        int webpHeight = 540;
        // gif动图时长
        int gifDuration = 10;
        // gif动图质量
        int gifQuality = 10;
        String webpFileName = UUID.randomUUID() + ".webp";
        String webpObjectKey = "/bucket/webp/" + webpFileName;
        uploadWebp(grabber, webpWidth, webpHeight, gifDuration, gifQuality, webpObjectKey, webpFileName);
    }

    private void uploadWebp(FFmpegFrameGrabber grabber, Integer targetWidth, Integer targetHeight, Integer gifDuration, Integer gifQuality, String objectKey, String filename) {
        File outputFile = new File(filename);
        FileInputStream fileInputStream = null;
        try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, targetWidth, targetHeight)) {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            grabber.setFrameNumber(0);
            int targetFrameCount = (int) Math.min(gifDuration * grabber.getFrameRate(), grabber.getLengthInFrames());
            // 设置缩放比例
            int[] targetSize = getChangeSize(grabber.getImageWidth(), grabber.getImageHeight(), targetWidth, targetHeight);
            // 设置gif图像宽高
            grabber.setImageWidth(targetSize[0]);
            grabber.setImageHeight(targetSize[1]);

            recorder.setFormat("webp");
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_WEBP);
            recorder.setOption("loop", "0");
            recorder.setVideoQuality(gifQuality);
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.start();

            for (int i = 0; i < targetFrameCount; i++) {
                Frame frame = grabber.grabImage();
                recorder.record(frame);
            }
            recorder.close();
            stopWatch.stop();
            fileInputStream = new FileInputStream(outputFile);
            // Webp动图文件 上传云盘
            this.uploadFile(objectKey, fileInputStream);
            log.info("生成webp成功: objectKey: {}, 处理耗时: {}s", objectKey, stopWatch.getTotalTimeSeconds());
        } catch (Exception e) {
            log.error("生成webp失败: objectKey: {} - 原因: {}", objectKey, e.getMessage());
        } finally {
            IoUtil.closeIfPosible(fileInputStream);
            FileUtil.del(outputFile);
        }
    }

    /**
     * 计算图像 按比例缩放后的宽高
     * 选择宽高缩放后较大的一边,固定其长度,另一边按原图比例缩放
     *
     * @param originalWidth  图像原宽度
     * @param originalHeight 图像原长度
     * @param targetWidth    目标宽度
     * @param targetHeight   目标高度
     * @return int[]
     */
    public static int[] getChangeSize(int originalWidth, int originalHeight, int targetWidth, int targetHeight) {
        // 设置缩放比例
        double scale = Math.min(targetWidth / (double) originalWidth, targetHeight / (double) originalHeight);
        // 计算缩放后的宽高
        return new int[]{(int) (originalWidth * scale), (int) (originalHeight * scale)};
    }

    /**
     * todo:自定义上传
     *
     * @param objectKey   OSS存储key
     * @param inputStream 流数据
     */
    private void uploadFile(String objectKey, InputStream inputStream) {

    }
}

tips:

1.建议使用线程池进行异步处理

2.代码中可自行修改预览图缩略图宽高

3.文件上传逻辑各自处理