SpringBoot基于FFmpeg实现压缩视频切片为m3u8

 更新时间:2026年02月13日 09:44:36   作者:god_cvz  
本文介绍了一个使用FFmpeg将MP4视频压缩切片为HLS格式M3U8文件的Java工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

分享一个关于使用ffmpeg对mp4文件进行压缩切片为hls格式m3u8文件的命令行调用程序。

前提是已经安装了ffmpeg,安装过程就不再赘述了。

直接上代码

package xxxxx;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import com.czi.uavcloud.common.utils.StringUtils;
import com.czi.uavcloud.fileprocess.utils.FileUtils;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;

/**
 * 压缩切片处理器
 *
 * @Author god_cvz
 */
@Slf4j
public class VideoCompressHandle {

    protected static final boolean WINDOWS = System.getProperty("os.name").startsWith("Windows");
    protected static final String SLASH = WINDOWS ? "\\" : "/";
    // 输出文件路径,可以自行修改
    public static final String TEMP_FOLDER_PATH = (WINDOWS ? "D:" : SLASH + "var") + SLASH + "tempVideoCompress" + SLASH;

    // 切片时长,默认5秒一个切片
    private static final String DEFAULT_HLS_TIME = "5";


    /**
     * 从指定URL下载视频并压缩切片为m3u8
     *
     * @param downloadUrl 视频下载地址
     */
    public static void compressWithUrl(String downloadUrl) {
        String videoFilePath = TEMP_FOLDER_PATH + UUID.randomUUID() + SLASH + FileUtil.getName(downloadUrl);
        // 下载视频至指定路径
        try (BufferedInputStream bufferInput = new BufferedInputStream(getInputStreamFromUrl(downloadUrl));
             FileOutputStream out = new FileOutputStream(videoFilePath)) {
            IoUtil.copy(bufferInput, out);
        } catch (Exception e) {
            e.printStackTrace();
        }

        compressWithLocal(videoFilePath);
    }

    /**
     * 从本地路径读取视频并压缩切片为m3u8
     *
     * @param videoFilePath 本地视频地址
     */
    public static void compressWithLocal(String videoFilePath) {
        // hutool工具返回的名称是带后缀的,入http://abc/123.mp4,返回的是123.mp4
        String videoName = FileUtil.getName(videoFilePath);
        // 创建临时文件夹目录
        String tempFolderPath = TEMP_FOLDER_PATH + UUID.randomUUID();
        FileUtil.mkdir(tempFolderPath);
        if (!WINDOWS) {
            try {
                // 文件夹授权
                asyncExecute(new String[]{"chmod", "777", "-R", tempFolderPath});
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        // m3u8文件输出地址:/var/tempVideoCompress/业务id/文件名/文件名.m3u8
        String outputFolderPath = tempFolderPath + SLASH + videoName.split("\\.")[0];
        FileUtil.mkdir(outputFolderPath);
        String m3u8FilePath = outputFolderPath + SLASH + videoName.split("\\.")[0] + ".m3u8";

        handle(tempFolderPath, videoFilePath, m3u8FilePath);
    }

    /**
     * 从指定 URL 下载资源并返回 InputStream
     *
     * @param urlString 资源的 URL 地址
     * @return InputStream (需要调用方手动关闭)
     * @throws IOException 下载或连接失败时抛出异常
     */
    public static InputStream getInputStreamFromUrl(String urlString) throws Exception {
        if (urlString == null || urlString.isEmpty()) {
            throw new IllegalArgumentException("URL 不能为空");
        }
        log.info("[Download] 开始下载文件:{}", urlString);
        long startTime = System.currentTimeMillis();

        try {
            URL url = new URL(urlString);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(10_000);
            connection.setReadTimeout(15_000);
            connection.setDoInput(true);
            // 检查响应码
            int responseCode = connection.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                throw new IOException("下载失败,HTTP 响应码:" + responseCode);
            }
            log.info("[Download] 连接成功[{}],开始接收数据...", url);
            InputStream inputStream = connection.getInputStream();
            long endTime = System.currentTimeMillis();
            log.info("[Download] 下载完成,用时 {} ms", (endTime - startTime));
            return inputStream;
        } catch (Exception e) {
            log.error("[Download] 下载文件失败[{}], error:{}", urlString, e.getMessage());
            throw new Exception("[Download] 下载文件失败: " + e.getMessage());
        }
    }

    public static void handle(String tempFolderPath, String videoFilePath, String m3u8FilePath) {
        long startTime = System.currentTimeMillis();
        try {
            // 压缩切片
            log.info("[视频压缩切片]压缩切片文件");
            ffmpegCompress(videoFilePath, m3u8FilePath);
            // 需要上传的可以自己处理
//            log.info("[视频压缩切片]上传切片目录文件:{}", videoFilePath);
//            uploadFolder(outputFolder);
        } catch (Exception e) {
            log.info("[视频压缩切片]处理失败:{}", e.getMessage());
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            log.info("[视频压缩切片]总耗时:{}", costTime);
//            log.info("[视频压缩切片]删除本地压缩包,释放空间:{}", tempFolderPath);
//            FileUtil.del(tempFolderPath);
        }
    }

    /**
     * 将mp4视频文件转码压缩为m3u8并切片
     *
     * @param inputFile
     * @param outputFile
     */
    public static void ffmpegCompress(String inputFile, String outputFile) {
        ffmpegCompress(inputFile, outputFile, DEFAULT_HLS_TIME);
    }

    /**
     * 将mp4视频文件转码压缩为m3u8并切片
     *
     * @param inputFile  待处理文件
     * @param outputFile 输出的具体文件路径
     */
    public static void ffmpegCompress(String inputFile, String outputFile, String hlsTime) {
        // 上级目录绝对路径
        String absoluteFolderPath = outputFile.substring(0, outputFile.indexOf(FileUtil.getName(outputFile)));
        if (!FileUtil.exist(absoluteFolderPath)) {
            FileUtils.createDirIfAbsent(absoluteFolderPath);
        }
        log.info("开始压缩转码文件:in:{},out:{},hlsTime:{}", inputFile, outputFile, hlsTime);
        long startTime = System.currentTimeMillis();
        // linux的ffmpeg可执行文件放在/usr/bin/ffmpeg,可以自行修改
        String cmd = WINDOWS ? "D:\\ffmpeg\\bin\\ffmpeg.exe" : "/usr/bin/ffmpeg";
        String[] command = new String[]{cmd,
                "-i", inputFile,
                //多线程数
                "-threads", "2",
                //帧数
                "-r", "25",
                //码率
                "-b:v", "3000k",
                //分辨率
                "-s", "1920x1080",
                //ultrafast(转码速度最快,视频往往也最模糊)、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo这10个选项,从快到慢
                "-preset", "ultrafast",
                //视频画质级别 1-5
                "-level", "3.0",
                //从0开始
                "-start_number", "0",
                "-g", "50",
                //设置编码器
                "-codec:v", "h264",
                //转码
                "-f", "hls",
                //每5秒切一个
                "-hls_time", StringUtils.isNotBlank(hlsTime) ? hlsTime : DEFAULT_HLS_TIME,
                //设置播放列表保存的最多条目,设置为0会保存所有切片信息,默认值为5
                "-hls_list_size", "0",
                outputFile};
        try {
            asyncExecute(command);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        log.info("压缩转码文件完成:{},耗时:{}", outputFile, System.currentTimeMillis() - startTime);
    }

    /**
     * 执行shell命令
     *
     * @param cmd
     */
    public static Integer asyncExecute(String[] cmd) throws IOException {
        return asyncExecute(cmd, null, null);
    }

    /**
     * 执行shell命令(支持任务管理器)
     *
     * @param cmd         命令数组
     * @param taskManager 任务管理器,用于注册进程和检查取消状态
     * @param taskId      任务ID
     */
    public static Integer asyncExecute(String[] cmd, Object taskManager, Long taskId) throws IOException {
        log.info("执行命令:{}", String.join(" ", cmd));
        Process process = new ProcessBuilder(cmd)
                // 合并错误流和标准流
                .redirectErrorStream(true)
                .start();

        // 如果有任务管理器,注册进程
        if (taskManager != null && taskId != null) {
            try {
                // 使用反射调用 registerChildProcess 方法
                taskManager.getClass().getMethod("registerChildProcess", Long.class, Process.class)
                        .invoke(taskManager, taskId, process);
            } catch (Exception e) {
                log.warn("注册子进程失败: {}", e.getMessage());
            }
        }

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
            // 异步读取输出(防止阻塞)
            Thread outputThread = new Thread(() -> {
                try {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        System.out.println("[PROCESS] " + line);

                        // 检查任务是否被取消
                        if (taskManager != null && taskId != null) {
                            try {
                                Object context = taskManager.getClass().getMethod("getTaskContext", Long.class)
                                        .invoke(taskManager, taskId);
                                if (context != null) {
                                    Boolean shouldStop = (Boolean) context.getClass().getMethod("shouldStop")
                                            .invoke(context);
                                    if (shouldStop != null && shouldStop) {
                                        log.info("检测到任务取消,停止读取进程输出,任务ID: {}", taskId);
                                        break;
                                    }
                                }
                            } catch (Exception e) {
                                // 忽略反射调用异常
                            }
                        }
                    }
                } catch (IOException e) {
                    System.err.println("输出流读取异常: " + e.getMessage());
                }
            });
            outputThread.start();

            // 等待进程完成,支持中断检查
            int exitCode = waitForProcessWithCancellationCheck(process, taskManager, taskId);

            // 等待输出线程结束
            outputThread.join(1000);
            return exitCode;
        } catch (InterruptedException e) {
            log.info("进程执行被中断");
            process.destroyForcibly();
            throw new RuntimeException("进程执行被取消", e);
        }
    }

    /**
     * 等待进程完成,支持取消检查
     */
    private static int waitForProcessWithCancellationCheck(Process process, Object taskManager, Long taskId) throws InterruptedException {
        while (true) {
            try {
                // 检查线程是否被中断
                if (Thread.currentThread().isInterrupted()) {
                    log.info("检测到线程中断,正在终止进程...");
                    process.destroyForcibly();
                    throw new InterruptedException("任务被取消");
                }

                // 检查任务管理器中的任务状态
                if (taskManager != null && taskId != null) {
                    try {
                        Object context = taskManager.getClass().getMethod("getTaskContext", Long.class)
                                .invoke(taskManager, taskId);
                        if (context != null) {
                            Boolean shouldStop = (Boolean) context.getClass().getMethod("shouldStop")
                                    .invoke(context);
                            if (shouldStop != null && shouldStop) {
                                log.info("检测到任务取消请求,正在终止进程,任务ID: {}", taskId);
                                process.destroyForcibly();
                                throw new InterruptedException("任务被取消");
                            }
                        }
                    } catch (Exception e) {
                        // 忽略反射调用异常
                    }
                }

                // 非阻塞检查进程是否完成
                if (process.isAlive()) {
                    // 进程还在运行,等待一小段时间后再检查
                    Thread.sleep(1000);
                } else {
                    // 进程已完成,返回退出码
                    return process.exitValue();
                }
            } catch (InterruptedException e) {
                log.info("等待进程时被中断,正在强制终止进程...");
                process.destroyForcibly();
                throw e;
            }
        }
    }

    /**
     * 上传切片文件目录
     *
     * @param folder
     * @return
     */
    public static void uploadFolder(String folder) {
        // 切片文件上传至源文件同级目录下的一个同名文件夹
        File[] files = FileUtil.ls(folder);
        if (files == null) {
            return;
        }
        for (File file : files) {
            uploadFile(file);
        }
    }

    /**
     * todo:自定义OSS上传
     *
     * @param file
     */
    private static void uploadFile(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {

        } catch (FileNotFoundException e) {
            log.error("读取文件报错");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

原视频文件:

切片后文件:

到此这篇关于SpringBoot基于FFmpeg实现压缩视频切片为m3u8的文章就介绍到这了,更多相关SpringBoot FFmpeg视频压缩内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决Java中基于GeoTools的Shapefile读取乱码的问题

    解决Java中基于GeoTools的Shapefile读取乱码的问题

    本文主要讨论了在使用Java编程语言进行地理信息数据解析时遇到的Shapefile属性信息乱码问题,以及根据不同的编码设置进行属性信息解析的方法,感兴趣的朋友跟随小编一起看看吧
    2025-03-03
  • Java集合类之Map集合的特点及使用详解

    Java集合类之Map集合的特点及使用详解

    这篇文章主要为大家详细介绍一下Java集合类中Map的特点及使用,文中的示例代码讲解详细,对我们学习Java有一定帮助,感兴趣的可以了解一下
    2022-08-08
  • Java Swing JToggleButton开关按钮的实现

    Java Swing JToggleButton开关按钮的实现

    这篇文章主要介绍了Java Swing JToggleButton开关按钮的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • java乐观锁原理与实现案例分析

    java乐观锁原理与实现案例分析

    这篇文章主要介绍了java乐观锁原理与实现,结合具体案例形式分析了乐观锁的原理及java使用乐观锁实现自动派单功能的相关操作技巧,需要的朋友可以参考下
    2019-10-10
  • java中concat()方法的使用说明

    java中concat()方法的使用说明

    这篇文章主要介绍了java中concat()方法的使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • 基于LinkedHashMap实现LRU缓存

    基于LinkedHashMap实现LRU缓存

    LinkedHashMap是Java集合中一个常用的容器,它继承了HashMap, 是一个有序的Hash表。那么该如何基于LinkedHashMap实现一个LRU缓存呢?本文将介绍LinkedHashMap的实现原理,感兴趣的同学可以参考一下
    2023-05-05
  • java 实现下压栈的操作(能动态调整数组大小)

    java 实现下压栈的操作(能动态调整数组大小)

    这篇文章主要介绍了java 实现下压栈的操作(能动态调整数组大小),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Java中的ReentrantLock、ReentrantReadWriteLock、StampedLock详解

    Java中的ReentrantLock、ReentrantReadWriteLock、StampedLock详解

    这篇文章主要介绍了Java中的ReentrantLock、ReentrantReadWriteLock、StampedLock详解,读写锁:一个资源能够被多个读线程访问,或者被一个写线程访问但是不能同时存在读写线程,需要的朋友可以参考下
    2024-01-01
  • Spring Boot 3.2.5集成mysql的详细步骤记录

    Spring Boot 3.2.5集成mysql的详细步骤记录

    作为一名Java开发者,我们经常需要在我们的应用程序中使用数据库,在Spring Boot中集成数据库是非常容易的,下面这篇文章主要给大家介绍了关于Spring Boot 3.2.5集成mysql的详细步骤,需要的朋友可以参考下
    2024-04-04
  • JDK1.8使用的垃圾回收器和执行GC的时长以及GC的频率方式

    JDK1.8使用的垃圾回收器和执行GC的时长以及GC的频率方式

    这篇文章主要介绍了JDK1.8使用的垃圾回收器和执行GC的时长以及GC的频率方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05

最新评论