springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率

用到的maven依赖:

lombok依赖就不贴出来了

java 复制代码
  <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>ffmpeg-platform</artifactId>
            <version>4.3.2-1.5.5</version>
  </dependency>
   <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
      </dependency>
 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.2</version>
  </dependency>

工具类:

java 复制代码
import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.AVCodec;
import org.bytedeco.ffmpeg.avcodec.AVCodecContext;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avformat;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacpp.PointerPointer;

import java.io.*;
import java.util.concurrent.TimeUnit;

@Slf4j
public class VideoUtils {

    static class LazyFfmpeg {
        private static final String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
    }

    public static String ffmpeg() {
        return LazyFfmpeg.ffmpeg;
    }

    /**
     * 压缩视频
     * @param inputFilePath 压缩前视频地址
     * @param outputFilePath 压缩后视频地址
     */
    public static void compressVideo(String inputFilePath, String outputFilePath) {

        if (StringUtils.isAnyBlank(inputFilePath, outputFilePath)) {
            throw new RuntimeException("输入视频路径或输出视频文件路径不能为空");
        }

        if (StringUtils.equals(inputFilePath, outputFilePath)) {
            throw new RuntimeException("outputFilePath不能和inputFilePath相同");
        }

        validIsFile(new File(inputFilePath));

        ProcessBuilder processBuilder = new ProcessBuilder(
                ffmpeg(),
                "-y",                     // 自动覆盖输出文件
                "-i", inputFilePath,      // 输入文件路径
                "-crf","30",
                "-c:v","h264",
                "-preset", "slow",            // 使用较慢的预设来提高压缩效率
//                "-b:v", "1000",  // 设置视频比特率为 1000 kbps
//                "-vf", String.format("scale=%s:%s", 1920, 1080),
//                "-c:a", "copy",           // 保持音频编码不变
                "-c:a", "aac",                  // 使用 AAC 音频编码
                "-b:a", "2k",                 // 设置音频比特率为 128 kbps
                outputFilePath            // 输出文件路径

        );

        StringBuilder stringBuilder = new StringBuilder();

        int exitCode;
        try {
            Process process = processBuilder.start();

            // 捕获错误输出
            processErrorMsg(process, stringBuilder);

            // 等待 FFmpeg 进程完成
            exitCode = process.waitFor();

        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        if (exitCode != 0) {
            throw new RuntimeException(stringBuilder.toString());
        }


    }

    /**
     * 提取图片
     *
     * @param videoPath 视频路径
     * @param second    提取指定时间图片
     * @param timeout   等待的最长时间
     * @param unit      参数的时间 timeout 单位
     * @return 图片
     */
    public static byte[] ffmpegExtractImage(String videoPath, Number second, long timeout, TimeUnit unit) {

        if (timeout <= 0) {
            throw new IllegalArgumentException("timeout不能小于等于0");
        }

        if (second == null) {
            second = 0;
        }

        if (unit == null) {
            unit = TimeUnit.MINUTES;
        }

        File videoFile = new File(videoPath);
        validIsFile(videoFile);
        ProcessBuilder extractBuilder = new ProcessBuilder(
                ffmpeg(),
                "-ss", second.toString(),
                "-i", videoPath,
                "-f", "image2pipe",
                "-vframes", "1",
//                "-vcodec", "png",//如果觉得照片不清晰,就启用此选项,但是照片会变大
                "-"
        );

        try {

            Process process = extractBuilder.start();

            try (InputStream inputStream = process.getInputStream()) {

                byte[] bytes = IoUtil.readBytes(inputStream);

                boolean result = process.waitFor(timeout, unit);
                if (!result) {
                    throw new RuntimeException("子进程退出之前已超过等待时间");
                }

                return bytes;
            }

        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 获取视频分辨率
     *
     * @param videoFilePath 视频路径
     */
    public static int[] getVideoResolution(String videoFilePath) {

        validIsFile(new File(videoFilePath));

        AVFormatContext formatContext = avformat.avformat_alloc_context();
        AVCodecContext codecContext = avcodec.avcodec_alloc_context3(null);

        // 打开视频文件
        if (avformat.avformat_open_input(formatContext, videoFilePath, null, null) != 0) {
            throw new RuntimeException("无法打开视频文件");
        }

        // 获取视频流信息
        if (avformat.avformat_find_stream_info(formatContext, (PointerPointer) null) < 0) {
            throw new RuntimeException("无法获取视频流信息");
        }

        // 查找视频流
        int videoStreamIndex = -1;
        for (int i = 0; i < formatContext.nb_streams(); i++) {
            if (formatContext.streams(i).codecpar().codec_type() == avutil.AVMEDIA_TYPE_VIDEO) {
                videoStreamIndex = i;
                break;
            }
        }

        if (videoStreamIndex == -1) {
            throw new RuntimeException("视频流未找到");
        }

        // 获取视频解码器上下文
        avcodec.avcodec_parameters_to_context(codecContext, formatContext.streams(videoStreamIndex).codecpar());

        // 查找解码器
        AVCodec codec = avcodec.avcodec_find_decoder(codecContext.codec_id());
        if (codec == null) {
            throw new RuntimeException("无法找到解码器");
        }

        // 打开解码器
        if (avcodec.avcodec_open2(codecContext, codec, (PointerPointer) null) < 0) {
            throw new RuntimeException("无法打开解码器");
        }

        // 获取视频分辨率
        int width = codecContext.width();
        int height = codecContext.height();

        // 清理资源
        codecContext.close();

        return new int[]{width, height};
    }
    private static void processErrorMsg(Process process, StringBuilder stringBuilder) {

        new Thread(() -> {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    stringBuilder.append(line);
                }
            } catch (IOException e) {
                log.error("打印命令行错误日志出现异常  errMsg:{}", e.getMessage(), e);
            }
        }).start();
    }

    public static void validIsFile(File file) {
        validExists(file);
        if (!file.isFile()) {
            throw new IllegalArgumentException("不是文件");
        }
    }

    public static void validExists(File file) {
        if (!file.exists()) {
            throw new IllegalArgumentException("videoPath不存在");
        }
    }
}
相关推荐
小羊没烦恼!5 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里5 天前
java中的继承和多态的区别
java
小羊没烦恼!5 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕5 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码5 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖5 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时5 天前
01-06-A-JVM排查实战详解
java·jvm
vipxieliang5 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
ba_pi5 天前
mysql 查询所有表名并授权
java
ProcessOn官方账号5 天前
java函数式编程--入门基础
java·编程·函数式编程