java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot FFmpeg视频压缩

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

作者: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视频压缩内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文