自定义ffmpeg工具类,实现java输入/出流对接ffmpeg进程

本文介绍了如何在Java项目中使用FFmpeg处理音频和视频文件,重点讲述了如何通过JAVE库实现FFmpeg的定制化调用,包括通过反射获取FFMPEGLocator的可执行文件路径,以及处理输入流和输出流的异步对接,以解决项目中AMR转MP3和网络流处理的需求。
摘要由CSDN通过智能技术生成


前言

基于jave的FFMPEGLocator实现自定义ffmpeg工具类

项目中遇到的问题,大致如下:
1.前端页面获取到微信录音保存的amr文件,但是 <audio> 标签不支持直接播放amr文件。
2.采用openCV和jave转成mp3文件,但这两个jar提供的功能过于丰富,且引入后项目打包偏大。
3.引用其他jar包提供的功能不支持输入流指定到ffmpeg进程,并用输出流接收ffmpeg进程的返回,而是必须指定到本地文件,除非使用临时文件暂存,但这样增加了临时文件读写的消耗。
4.网上没看见其他的java输入/出流方式对接ffmpeg。
由于以上的问题,被安排来写个ffmpeg工具类。

一、ffmpeg介绍

FFmpeg是一个开源的跨平台多媒体处理框架,用于录制、转换、流化数字音频和视频,并能够处理这些媒体的后期处理。

它包含了先进的音频/视频编解码库libavcodec,这个库中的许多编解码器都是从头开始开发的,以确保高可移植性和编解码质量。FFmpeg支持几乎所有视频和音频格式,包括mkv、flv、mov等,并且它的功能非常强大,甚至可以播放一些VLC媒体播放器不支持的流媒体格式。

此外,FFmpeg还支持将媒体文件转化为流,使其能够在网络上进行传输。它可以用命令行或编程语言(如C、Java、Python等)调用,使得自动化处理视频和音频成为可能。

二、jave介绍

JAVE (Java Audio Video Encoder) 类库是一个 ffmpeg 项目的 Java 语言封装。开发人员可以使用 JAVE 在不同的格式间转换视频和音频。例如将 AVI 转成 MPEG 动画,等等 ffmpeg 中可以完成的在 JAVE 都有对应的方法。

三、代码使用

依赖引入:

<dependency>
      <groupId>com.github.dadiyang</groupId>
      <artifactId>jave</artifactId>
      <version>1.0.5</version>
</dependency>

java代码

记录一些比较重要的代码,相关实体类、配置、整体工具类代码可以看最后的案例仓库

反射获取jave的locator中可执行文件的地址

import it.sauronsoftware.jave.DefaultFFMPEGLocator;
import it.sauronsoftware.jave.FFMPEGLocator;

import java.lang.reflect.Field;

/**
 * ffmpeg执行文件相关参数
 *
 * @author suix
 * @date 2024-01-31
 */
public class CustomFfmpegExecAttributes {

    /**
     * ffmpeg定位
     */
    private FFMPEGLocator locator;

    /**
     * ffmpeg执行文件路径
     */
    private String execPath;

    public CustomFfmpegExecAttributes() {
        this.locator = new DefaultFFMPEGLocator();
        try {
            // 通过反射获取jave的locator中可执行文件的地址,该jar包未提供get方法
            Field field = this.locator.getClass().getDeclaredField("path");
            field.setAccessible(true);
            this.execPath = String.valueOf(field.get(locator));
        } catch (Exception e) {
            throw new RuntimeException("获取ffmpeg可执行文件异常: " + e.getMessage());
        }
    }

    public FFMPEGLocator getLocator() {
        return locator;
    }

    public String getExecPath() {
        return execPath;
    }
}

编码(输入/出流)参数构造

    /**
     * 编码(输入/出流)参数构造
     *
     * @param attributes  自定义命令参数
     * @param inputStream 输入流
     * @return cmd命令参数列表
     * @author suix
     */
    public static List<String> encode(CustomAttributes attributes, InputStream inputStream) throws Exception {
        List<String> argList = new ArrayList<>();

        if (null == attributes) {
            throw new RuntimeException("参数不能为空");
        }
        if (null == inputStream || inputStream.available() == 0) {
            throw new RuntimeException("输入流不可用");
        }

        // 指定执行器
        CustomFfmpegExecAttributes execAttributes = new CustomFfmpegExecAttributes();
        argList.add(execAttributes.getExecPath());

        // -i -    输入流指定至ffmpeg入参
        argList.add(CustomArgs.INPUT);
        argList.add("pipe:0");

        // 构造输入/出路径之外的参数
        generateArgsWithoutPath(attributes, argList);

        // 输出流接收ffmpeg返回
        argList.add("pipe:1");

        // 打印命令
        for (String arg : argList) {
            System.out.print(arg + " ");
        }

        return argList;
    }

构造输入/出路径之外的参数

    /**
     * 构造不包含输入/出文件路径的ffmpeg参数命令
     *
     * @param attributes 自定义命令参数
     * @param argList    ffmpeg命令行参数
     * @author suix
     */
    private static void generateArgsWithoutPath(CustomAttributes attributes, List<String> argList) {
        CustomVideoAttributes videoAttributes = attributes.getVideoAttributes();
        CustomAudioAttributes audioAttributes = attributes.getAudioAttributes();
        // 视频流相关
        if (null == videoAttributes) {
            // 不输出视频
            argList.add(CustomArgs.UN_VIDEO);
        } else {
            // 视频流编码器参数
            generateVideoArgs(videoAttributes, attributes.isSubtitles(), argList);
        }

        // 音频流相关
        if (null == audioAttributes) {
            // 不输出音频
            argList.add(CustomArgs.UN_AUDIO);
        } else {
            // 音频流编码器参数
            generateAudioArgs(audioAttributes, argList);
        }

        // 指定输出文件编码格式
        argList.add(CustomArgs.FORMAT);
        String format = attributes.getFormat();
        if (null == format || format.isBlank()) {
            // 文件编码格式默认为mp4/mp3
            format = attributes.isVideo() ? "mp4" : "mp3";
        }
        argList.add(format);

        // 指定编码速度和质量的预设值
        String presetName = attributes.getPresetName();
        if (null != presetName && !presetName.isBlank()) {
            argList.add(CustomArgs.PRESET);
            argList.add(presetName);
        }

        // 指定编码器的调整参数
        String tuningParameter = attributes.getTuningParameter();
        if (null != tuningParameter && !tuningParameter.isBlank()) {
            argList.add(CustomArgs.TUNE);
            argList.add(tuningParameter);
        }

        // 指定线程数
        String threadsCount = attributes.getThreadsCount();
        if (null != threadsCount && !threadsCount.isBlank()) {
            argList.add(CustomArgs.THREADS_COUNT);
            argList.add(threadsCount);
        }

        // 指定从输入文件的哪个时间点开始转码
        String startTime = attributes.getStartTime();
        if (null != startTime && !startTime.isBlank()) {
            argList.add(CustomArgs.START_TIME);
            argList.add(startTime);
        }

        // 指定转码的时长,默认单位为秒
        String duration = attributes.getDuration();
        if (null != duration && !duration.isBlank()) {
            argList.add(CustomArgs.DURATION);
            argList.add(duration);
        }
    }

java输入/出流对接ffmpeg进程

	/**
     * 输入/出流方式,执行ffmpeg转码amr文件为mp3文件,并获取ffmpeg返回输出流
     *
     * @param attributes  CustomAttributes 自定义ffmpeg参数
     * @param inputStream 文件输入流
     * @return ByteArrayOutputStream
     * @author suix
     */
    public static ByteArrayOutputStream execFfmpegCommand(CustomAttributes attributes,
                                                          InputStream inputStream) throws Exception {
        // 命令参数
        List<String> commands = CustomEncoder.encode(attributes, inputStream);
        ProcessBuilder processBuilder = new ProcessBuilder(commands);
        Process process = processBuilder.start();

        /*
           当同时自定义了ffmpeg的输入和输出为管道时,需要确保两边的读写操作都是异步且适当处理缓冲,否则会出现堵塞现象
           采用多线程的方式实现
         */
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 线程1 执行文件流指定至ffmpeg进程的输入流
        Future<?> stdinWriterFuture = executor.submit(() -> {
            try (OutputStream ffmpegStdin = process.getOutputStream()) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    ffmpegStdin.write(buffer, 0, bytesRead);
                    ffmpegStdin.flush(); // 防止缓冲区满造成阻塞
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

        // 线程2 指定输出流接取ffmpeg进程的输出流
        Future<?> stdoutReaderFuture = executor.submit(() -> {
            try (InputStream ffmpegStdout = process.getInputStream();
                 InputStream ffmpegStderr = process.getErrorStream()) {

                // 合并stdout和stderr的流以同时处理
                InputStream combinedStream = new SequenceInputStream(ffmpegStdout, ffmpegStderr);
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = combinedStream.read(buffer)) != -1) {
                    byteArrayOutputStream.write(buffer, 0, bytesRead);
                    byteArrayOutputStream.flush();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        // 等待所有任务完成
        stdinWriterFuture.get();
        stdoutReaderFuture.get();

        // 关闭线程池
        executor.shutdown();

        return byteArrayOutputStream;
    }

其中有个坑,本地测试的时候最初采用单线程的方式。
当只有输入流或只有输出流对接ffmpeg进程的时候,可以正常使用,符合预期。
但当输入流和输出流同时对接ffmpeg进程的时候,输入流读取中途线程会堵塞。
看见一篇c++对接ffmpeg的文章,看见使用多线程对接,突然有了灵感改为异步操作。

原因大致如下:
问题可能出在Java进程与FFmpeg子进程之间的输入输出流同步上。当同时自定义了ffmpeg的输入和输出为管道时,需要确保两边的读写操作都是异步且适当处理缓冲。
这里的一个常见问题是,在向ffmpeg的stdin(标准输入)写入数据的同时,如果没有正确处理stdout(标准输出)和stderr(标准错误输出),可能会导致缓冲区满进而阻塞ffmpeg进程或者Java进程。

四、工具类代码仓库

工具类案例demo

五、参考文章

修复com.github.dadiyang-jave-1.0.5获取部分mp4视频高度宽度出现NPM异常
FFmpeg 的使用
使用ffmpeg实现管道输入输出,并连接在代码中


总结

几步比较重要的地方就在于,java反射获取执行器地址,构造ffmpeg命令行参数列表,输入/出流异步对接ffmpeg进程。

第一次在csdn写博客,有点小麻烦 0.0

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Java使用FFmpeg来进行音视频处理十分常见。下面是三个测试代码的例子,展示了在Java中使用FFmpeg的一些常见功能: 1. 视频转换:使用`FfmpegUtil.videoConvert()`方法来将一个视频文件转换为另一种格式的视频文件。传入参数包括FFmpeg的路径、原视频文件路径和目标视频文件路径。示例代码如下: 2. 音视频合并:使用`FfmpegUtil.audioVideoMerge()`方法来将一个音频文件和一个视频文件合并为一个新的视频文件。传入参数包括FFmpeg的路径、音频文件路径、视频文件路径、合并时间(以秒为单位)和目标视频文件路径。示例代码如下: 3. 获取视频封面:使用`FfmpegUtil.getVideoCover()`方法来从一个视频文件中提取封面图片。传入参数包括FFmpeg的路径、视频文件路径和目标图片文件路径。示例代码如下: 请注意,示例中使用到的`FfmpegProperties`和`SpringContextHolder`是自定义的类和方法,用于获取FFmpeg的配置和Spring容器中的bean。你需要根据你的项目具体情况进行相应的修改。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [音视频处理工具FFmpegJava结合的简单使用](https://blog.csdn.net/yinshipin007/article/details/130870582)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值