当前位置: 首页 > article >正文

libilibi项目总结(18)FFmpeg 的使用

FFmpeg工具类

import com.easylive.entity.config.AppConfig;
import com.easylive.entity.constants.Constants;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.File;
import java.math.BigDecimal;

@Component
public class FFmpegUtils {

    @Resource
    private AppConfig appConfig;


    /**
     * 生成图片缩略图
     *
     * @param filePath
     * @return
     */
    public void createImageThumbnail(String filePath) {
        final String CMD_CREATE_IMAGE_THUMBNAIL = "ffmpeg -i \"%s\" -vf scale=200:-1 \"%s\"";
        String cmd = String.format(CMD_CREATE_IMAGE_THUMBNAIL, filePath, filePath + Constants.IMAGE_THUMBNAIL_SUFFIX);
        ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
    }


    /**
     * 获取视频编码
     *
     * @param videoFilePath
     * @return
     */
    public String getVideoCodec(String videoFilePath) {
        final String CMD_GET_CODE = "ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \"%s\"";
        String cmd = String.format(CMD_GET_CODE, videoFilePath);
        String result = ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
        result = result.replace("\n", "");
        result = result.substring(result.indexOf("=") + 1);
        String codec = result.substring(0, result.indexOf("["));
        return codec;
    }

    public void convertHevc2Mp4(String newFileName, String videoFilePath) {
        String CMD_HEVC_264 = "ffmpeg -i %s -c:v libx264 -crf 20 %s";
        String cmd = String.format(CMD_HEVC_264, newFileName, videoFilePath);
        ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
    }

    public void convertVideo2Ts(File tsFolder, String videoFilePath) {
        final String CMD_TRANSFER_2TS = "ffmpeg -y -i \"%s\"  -vcodec copy -acodec copy -vbsf h264_mp4toannexb \"%s\"";
        final String CMD_CUT_TS = "ffmpeg -i \"%s\" -c copy -map 0 -f segment -segment_list \"%s\" -segment_time 10 %s/%%4d.ts";
        String tsPath = tsFolder + "/" + Constants.TS_NAME;
        //生成.ts
        String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
        ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
        //生成索引文件.m3u8 和切片.ts
        cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath());
        ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
        //删除index.ts
        new File(tsPath).delete();
    }


    public Integer getVideoInfoDuration(String completeVideo) {
        final String CMD_GET_CODE = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%s\"";
        String cmd = String.format(CMD_GET_CODE, completeVideo);
        String result = ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
        if (StringTools.isEmpty(result)) {
            return 0;
        }
        result = result.replace("\n", "");
        return new BigDecimal(result).intValue();
    }
}

这段代码展示了一个Java类 FFmpegUtils,该类主要提供了一些关于视频和图片处理的功能,利用了FFmpeg工具进行视频转码、生成视频缩略图、获取视频编码格式等操作。下面我将逐个解释每个方法的功能及其实现。

1. 类级别注解和成员

@Component
public class FFmpegUtils {

    @Resource
    private AppConfig appConfig;
}
  • @Component 注解表示该类是一个Spring Bean,Spring框架会自动扫描并注册它为一个Bean,允许依赖注入等Spring特性。
  • @Resource 注解表示 appConfig 将被自动注入。这通常用于从配置类中注入配置数据,如 appConfig 中可能存储了一些关于FFmpeg日志的配置信息。

2. createImageThumbnail 方法:生成图片缩略图

public void createImageThumbnail(String filePath) {
    final String CMD_CREATE_IMAGE_THUMBNAIL = "ffmpeg -i \"%s\" -vf scale=200:-1 \"%s\"";
    String cmd = String.format(CMD_CREATE_IMAGE_THUMBNAIL, filePath, filePath + Constants.IMAGE_THUMBNAIL_SUFFIX);
    ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
}
  • 功能:根据输入的图片路径,生成一个宽度为200像素的缩略图,保持比例。
  • 实现
    • 使用 FFmpeg 的 scale 滤镜来调整图片大小。
    • 通过 String.format 格式化命令字符串,%s 会被 filePath 和目标缩略图的文件路径替换(缩略图路径使用 Constants.IMAGE_THUMBNAIL_SUFFIX 后缀)。
    • ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog()) 执行命令,通过外部工具运行FFmpeg命令。appConfig.getShowFFmpegLog() 可能是一个布尔值,指示是否在日志中显示FFmpeg的执行输出。

3. getVideoCodec 方法:获取视频编码格式

public String getVideoCodec(String videoFilePath) {
    final String CMD_GET_CODE = "ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \"%s\"";
    String cmd = String.format(CMD_GET_CODE, videoFilePath);
    String result = ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
    result = result.replace("\n", "");
    result = result.substring(result.indexOf("=") + 1);
    String codec = result.substring(0, result.indexOf("["));
    return codec;
}
  • 功能:通过 ffprobe 工具获取视频文件的编码格式(例如 H.264, HEVC 等)。
  • 实现
    • 使用 ffprobe 命令(FFmpeg的一个工具)分析视频文件,提取视频流的编码名称。
    • 通过 ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog()) 执行命令,得到执行结果。
    • 对结果进行字符串处理:
      • 移除换行符。
      • = 之后提取编码名称。
      • 去掉编码名称中可能的多余字符(如 [)。
    • 返回编码格式(如 h264)。

4. convertHevc2Mp4 方法:将HEVC格式视频转换为MP4格式

public void convertHevc2Mp4(String newFileName, String videoFilePath) {
    String CMD_HEVC_264 = "ffmpeg -i %s -c:v libx264 -crf 20 %s";
    String cmd = String.format(CMD_HEVC_264, newFileName, videoFilePath);
    ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
}
  • 功能:将HEVC(H.265)格式的视频转换为H.264编码的MP4格式。
  • 实现
    • 使用 ffmpeg 命令,将输入的视频(videoFilePath)转换为 libx264 编码,并输出为新的文件(newFileName)。
    • -crf 20 表示视频的质量控制参数,值越低质量越高(20-25是常见的范围)。
    • 执行转换命令。

5. convertVideo2Ts 方法:将视频转换为.ts格式,并分割为多个片段

public void convertVideo2Ts(File tsFolder, String videoFilePath) {
    final String CMD_TRANSFER_2TS = "ffmpeg -y -i \"%s\"  -vcodec copy -acodec copy -vbsf h264_mp4toannexb \"%s\"";
    final String CMD_CUT_TS = "ffmpeg -i \"%s\" -c copy -map 0 -f segment -segment_list \"%s\" -segment_time 10 %s/%%4d.ts";
    String tsPath = tsFolder + "/" + Constants.TS_NAME;
    //生成.ts
    String cmd = String.format(CMD_TRANSFER_2TS, videoFilePath, tsPath);
    ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
    //生成索引文件.m3u8 和切片.ts
    cmd = String.format(CMD_CUT_TS, tsPath, tsFolder.getPath() + "/" + Constants.M3U8_NAME, tsFolder.getPath());
    ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
    //删除index.ts
    new File(tsPath).delete();
}
  • 功能:将视频文件转换为 .ts 格式并分割为多个片段,生成 .m3u8 索引文件,通常用于直播或点播视频流。
  • 实现
    • 生成 .ts 文件:使用 ffmpeg 命令将输入视频转为 .ts 格式,并使用 -vcodec copy -acodec copy 保持视频和音频流的原始格式,不重新编码。
    • 分割为多个片段:使用 ffmpeg-f segment 选项将 .ts 文件切割为多个片段,每片段的时长为 10 秒。
    • 删除临时文件:删除中间生成的 index.ts 文件,只保留最终的切片和 .m3u8 文件。

6. getVideoInfoDuration 方法:获取视频的时长

public Integer getVideoInfoDuration(String completeVideo) {
    final String CMD_GET_CODE = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%s\"";
    String cmd = String.format(CMD_GET_CODE, completeVideo);
    String result = ProcessUtils.executeCommand(cmd, appConfig.getShowFFmpegLog());
    if (StringTools.isEmpty(result)) {
        return 0;
    }
    result = result.replace("\n", "");
    return new BigDecimal(result).intValue();
}
  • 功能:获取视频文件的时长(秒数)。
  • 实现
    • 使用 ffprobe 工具来获取视频的时长,命令中的 -show_entries format=duration 表示只获取时长信息。
    • 执行命令后,处理返回结果:
      • 去掉换行符。
      • 使用 BigDecimal 转换为整数返回时长(秒)。

总结

这段代码封装了一些FFmpeg工具的常用操作,如生成视频缩略图、转换视频格式、获取视频编码信息、将视频分割成.ts文件等。每个方法都通过 ProcessUtils.executeCommand 执行外部命令,调用FFmpeg和FFprobe工具来完成相应的功能。appConfig 用于控制FFmpeg命令的日志输出等配置。

ProcessUtils

import com.easylive.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class ProcessUtils {
    private static final Logger logger = LoggerFactory.getLogger(ProcessUtils.class);

    private static final String osName = System.getProperty("os.name").toLowerCase();

    public static String executeCommand(String cmd, Boolean showLog) throws BusinessException {
        if (StringTools.isEmpty(cmd)) {
            return null;
        }

        Runtime runtime = Runtime.getRuntime();
        Process process = null;
        try {
            //判断操作系统
            if (osName.contains("win")) {
                process = Runtime.getRuntime().exec(cmd);
            } else {
                process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
            }
            // 执行ffmpeg指令
            // 取出输出流和错误流的信息
            // 注意:必须要取出ffmpeg在执行命令过程中产生的输出信息,如果不取的话当输出流信息填满jvm存储输出留信息的缓冲区时,线程就回阻塞住
            PrintStream errorStream = new PrintStream(process.getErrorStream());
            PrintStream inputStream = new PrintStream(process.getInputStream());
            errorStream.start();
            inputStream.start();
            // 等待ffmpeg命令执行完
            process.waitFor();
            // 获取执行结果字符串
            String result = errorStream.stringBuffer.append(inputStream.stringBuffer + "\n").toString();
            // 输出执行的命令信息
            if (showLog) {
                logger.info("执行命令{}结果{}", cmd, result);
            }
            return result;
        } catch (Exception e) {
            logger.error("执行命令失败cmd{}失败:{} ", cmd, e.getMessage());
            throw new BusinessException("视频转换失败");
        } finally {
            if (null != process) {
                ProcessKiller ffmpegKiller = new ProcessKiller(process);
                runtime.addShutdownHook(ffmpegKiller);
            }
        }
    }

    /**
     * 在程序退出前结束已有的FFmpeg进程
     */
    private static class ProcessKiller extends Thread {
        private Process process;

        public ProcessKiller(Process process) {
            this.process = process;
        }

        @Override
        public void run() {
            this.process.destroy();
        }
    }


    /**
     * 用于取出ffmpeg线程执行过程中产生的各种输出和错误流的信息
     */
    static class PrintStream extends Thread {
        InputStream inputStream = null;
        BufferedReader bufferedReader = null;
        StringBuffer stringBuffer = new StringBuffer();

        public PrintStream(InputStream inputStream) {
            this.inputStream = inputStream;
        }

        @Override
        public void run() {
            try {
                if (null == inputStream) {
                    return;
                }
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                String line = null;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuffer.append(line);
                }
            } catch (Exception e) {
                logger.error("读取输入流出错了!错误信息:" + e.getMessage());
            } finally {
                try {
                    if (null != bufferedReader) {
                        bufferedReader.close();
                    }
                    if (null != inputStream) {
                        inputStream.close();
                    }
                } catch (IOException e) {
                    logger.error("调用PrintStream读取输出流后,关闭流时出错!");
                }
            }
        }
    }
}

```在Spring Boot应用程序中,`FFmpeg` 是通过 `ProcessUtils` 类执行的,该类负责执行系统命令(如 `ffmpeg` 或 `ffprobe`)并处理其输出流和错误流。下面我会详细解释 `ProcessUtils` 类的工作原理以及它如何在后台运行 FFmpeg。

### 1. **`executeCommand` 方法概述**
`executeCommand` 是 `ProcessUtils` 类的核心方法,它用于执行操作系统命令(比如 FFmpeg 命令),并返回执行的结果。

```java
public static String executeCommand(String cmd, Boolean showLog) throws BusinessException {
    // 检查输入的命令是否为空
    if (StringTools.isEmpty(cmd)) {
        return null;
    }

    // 获取当前操作系统名称
    Runtime runtime = Runtime.getRuntime();
    Process process = null;
    try {
        // 根据操作系统类型执行不同的命令
        if (osName.contains("win")) {
            process = Runtime.getRuntime().exec(cmd); // Windows 系统
        } else {
            process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd}); // Linux 或 macOS
        }

        // 获取并处理输出流和错误流
        PrintStream errorStream = new PrintStream(process.getErrorStream());
        PrintStream inputStream = new PrintStream(process.getInputStream());

        // 启动两个线程,分别读取标准输出流和错误输出流
        errorStream.start();
        inputStream.start();

        // 等待命令执行完毕
        process.waitFor();

        // 获取命令的输出结果
        String result = errorStream.stringBuffer.append(inputStream.stringBuffer + "\n").toString();

        // 如果 `showLog` 为 true,则记录日志
        if (showLog) {
            logger.info("执行命令{}结果{}", cmd, result);
        }

        // 返回执行的结果
        return result;
    } catch (Exception e) {
        logger.error("执行命令失败cmd{}失败:{} ", cmd, e.getMessage());
        throw new BusinessException("视频转换失败"); // 捕获异常并抛出自定义异常
    } finally {
        // 执行完毕后,注册一个退出钩子,确保进程在 JVM 退出时被销毁
        if (null != process) {
            ProcessKiller ffmpegKiller = new ProcessKiller(process);
            runtime.addShutdownHook(ffmpegKiller);
        }
    }
}

2. 方法执行过程解释

  • 检查输入命令是否为空:首先,方法检查传入的命令字符串(cmd)是否为空,如果为空则直接返回 null,避免执行空命令。

  • 获取系统信息:通过 System.getProperty("os.name").toLowerCase() 获取操作系统的名称,并将其转换为小写。这是为了决定在不同的操作系统(如 Windows 或 Linux)上如何执行命令。

  • 执行命令

    • Windows:如果操作系统是 Windows,直接调用 Runtime.getRuntime().exec(cmd) 执行命令。
    • Unix 系统(Linux/macOS):如果操作系统是 Linux 或 macOS,则使用 /bin/sh 来执行命令,/bin/sh -c 使得传入的字符串命令能够在 shell 中执行。
  • 输出和错误流处理:FFmpeg 执行命令时,会产生标准输出(stdout)和错误输出(stderr)。这些输出流需要被读取并处理,否则在输出流缓冲区满时,进程会阻塞。

    • 使用两个 PrintStream 线程分别读取 process.getErrorStream()process.getInputStream() 流。
  • 等待命令执行完毕:通过 process.waitFor() 阻塞当前线程,直到命令执行完毕。

  • 收集命令结果:命令执行完毕后,输出流中的数据会被拼接起来形成完整的执行结果。

  • 记录日志:如果 showLog 参数为 true,会将命令及其执行结果记录到日志中。

  • 异常处理:如果在执行命令过程中发生任何异常,会被捕获并记录错误日志,并抛出一个 BusinessException 异常,提示 “视频转换失败”。

  • 进程销毁:通过 ProcessKiller 类注册一个 JVM 退出钩子,在应用退出时确保 FFmpeg 进程被销毁,以防止后台进程继续运行。

3. 流的读取和关闭:PrintStream

PrintStream 是一个线程类,用于读取 FFmpeg 命令执行过程中的标准输出流和错误输出流。它是一个内部类,负责从 InputStream(标准输出流或错误输出流)中读取数据并将其存储到 StringBuffer 中。

static class PrintStream extends Thread {
    InputStream inputStream = null;
    BufferedReader bufferedReader = null;
    StringBuffer stringBuffer = new StringBuffer();

    public PrintStream(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    @Override
    public void run() {
        try {
            if (null == inputStream) {
                return;
            }
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            String line = null;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuffer.append(line); // 将每一行的输出内容追加到 stringBuffer
            }
        } catch (Exception e) {
            logger.error("读取输入流出错了!错误信息:" + e.getMessage());
        } finally {
            try {
                if (null != bufferedReader) {
                    bufferedReader.close();
                }
                if (null != inputStream) {
                    inputStream.close();
                }
            } catch (IOException e) {
                logger.error("调用PrintStream读取输出流后,关闭流时出错!");
            }
        }
    }
}
  • 读取输出流PrintStream 通过 BufferedReader 逐行读取输入流(标准输出流或错误输出流),并将读取的内容存储到 StringBuffer 中。
  • 关闭流:在读取完毕后,它会关闭输入流和缓冲流,确保资源被正确释放。

4. 进程终止:ProcessKiller

ProcessKiller 类是一个自定义线程,它负责在应用程序退出时终止 FFmpeg 进程。

private static class ProcessKiller extends Thread {
    private Process process;

    public ProcessKiller(Process process) {
        this.process = process;
    }

    @Override
    public void run() {
        this.process.destroy(); // 销毁进程
    }
}
  • 作用:当 JVM 退出时,ProcessKiller 会被触发,调用 process.destroy() 方法强制终止 FFmpeg 进程,避免进程僵死或长时间占用资源。

总结

通过 ProcessUtils.executeCommand 方法,Spring Boot 后端应用可以在运行时启动一个外部进程(如 FFmpeg),执行视频转换或其他处理任务。命令执行过程中产生的标准输出和错误输出流会被独立的线程读取并保存,最后将执行结果返回给调用者。为了保证系统的稳定性,ProcessUtils 还通过 ProcessKiller 线程确保 FFmpeg 进程在应用退出时被正确销毁。


http://www.kler.cn/a/442246.html

相关文章:

  • HTML5 Canvas实现的跨年烟花源代码
  • 【经济学通识——国债】
  • C++实现设计模式---外观模式 (Facade)
  • Linux:认识Shell、Linux用户和权限
  • 2025第3周 | json-server的基本使用
  • postgresql分区表相关问题处理
  • SSM 与 Vue 双剑合璧:新锐台球厅管理系统的匠心设计与完美实现
  • javaEE--计算机是如何工作的-1
  • AI技术在演示文稿制作中的应用一键生成PPT
  • zlmediakit搭建直播推流服务
  • Visual studio中C/C++连接mysql
  • (笔记)lib:no such lib的另一种错误可能:/etc/ld.so.conf没增加
  • Java中ArrayList和LinkedList的区别?
  • 富途证券C++面试题及参考答案
  • 先进的多模态专家需要掌握哪些知识和技能课程
  • 单片机STM32、GD32、ESP32开发板的差异和应用场景
  • Java全栈项目:学生请假管理系统
  • C++并发与多线程(高级函数async)
  • [每周一更]-(第127期):Go新项目-Gin中使用超时中间件实战(11)
  • 【深度学习基础】Windows实时查看GPU显存占用、功耗、进程状态
  • USB-A/C 2in1接口的未来应用前景分析
  • JAVA入门:使用IDE开发
  • 多模态检索增强生成
  • HarmonyOS 实时监听与获取 Wi-Fi 信息
  • 解锁Vue组件的奇妙世界
  • 【YashanDB知识库】数据库一主一备部署及一主两备部署时,主备手动切换方法及自动切换配置