千千梦丶琪 发表于 2024-12-23 20:29:13

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 进程在应用退出时被精确烧毁。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: libilibi项目总结(18)FFmpeg 的利用