SpringBoot 整合 FFmpeg 的工具类

以下是一个完整的 SpringBoot 整合 FFmpeg 的工具类:

1. 配置类

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "ffmpeg")
public class FfmpegProperties {
    
    private String path = "ffmpeg";  // 默认使用系统PATH中的ffmpeg
    private int timeout = 300;       // 命令执行超时时间(秒)
    private int maxThreads = 4;      // 最大线程数
    
    // getters and setters
    public String getPath() {
        return path;
    }
    
    public void setPath(String path) {
        this.path = path;
    }
    
    public int getTimeout() {
        return timeout;
    }
    
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    
    public int getMaxThreads() {
        return maxThreads;
    }
    
    public void setMaxThreads(int maxThreads) {
        this.maxThreads = maxThreads;
    }
}

2. 完整的 FFmpeg 工具类

package com.example.util;

import com.example.config.FfmpegProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class FfmpegUtil {
    
    private static final Logger logger = LoggerFactory.getLogger(FfmpegUtil.class);
    
    @Autowired
    private FfmpegProperties ffmpegProperties;
    
    /**
     * 执行FFmpeg命令
     */
    public boolean execCmd(String[] cmd) {
        return execCmd(cmd, null);
    }
    
    /**
     * 执行FFmpeg命令(带输出处理)
     */
    public boolean execCmd(String[] cmd, OutputHandler outputHandler) {
        ProcessBuilder processBuilder = new ProcessBuilder(cmd);
        processBuilder.redirectErrorStream(true);  // 合并标准错误和标准输出
        
        try {
            Process process = processBuilder.start();
            
            // 处理输出流
            if (outputHandler != null) {
                handleOutput(process, outputHandler);
            }
            
            // 等待进程完成
            boolean finished = process.waitFor(ffmpegProperties.getTimeout(), TimeUnit.SECONDS);
            if (!finished) {
                process.destroyForcibly();
                logger.error("FFmpeg命令执行超时: {}", String.join(" ", cmd));
                return false;
            }
            
            int exitValue = process.exitValue();
            if (exitValue == 0) {
                logger.info("FFmpeg命令执行成功: {}", String.join(" ", cmd));
                return true;
            } else {
                logger.error("FFmpeg命令执行失败,退出码: {}, 命令: {}", exitValue, String.join(" ", cmd));
                return false;
            }
            
        } catch (IOException | InterruptedException e) {
            logger.error("FFmpeg命令执行异常: {}", e.getMessage(), e);
            return false;
        }
    }
    
    /**
     * 处理输出流
     */
    private void handleOutput(Process process, OutputHandler outputHandler) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                outputHandler.handle(line);
            }
        } catch (IOException e) {
            logger.error("处理FFmpeg输出流异常: {}", e.getMessage());
        }
    }
    
    /**
     * 音频转码为WAV格式
     */
    public boolean audioToWav(String inputPath, String outputPath) {
        return audioToWav(inputPath, outputPath, 44100, 2, 16);
    }
    
    /**
     * 音频转码为WAV格式(自定义参数)
     */
    public boolean audioToWav(String inputPath, String outputPath, 
                             Integer sampleRate, Integer channels, Integer bitDepth) {
        // 输出文件预处理
        File outputFile = new File(outputPath);
        prepareOutputFile(outputFile);
        
        // 设置默认参数
        if (sampleRate == null) sampleRate = 44100;
        if (channels == null) channels = 2;
        if (bitDepth == null) bitDepth = 16;
        
        // 根据位深度选择PCM格式
        String pcmFormat = getPcmFormat(bitDepth);
        
        // 构建FFmpeg命令
        String[] cmd = new String[]{
            ffmpegProperties.getPath(),
            "-i", inputPath,
            "-c:a", pcmFormat,
            "-ac", channels.toString(),
            "-ar", sampleRate.toString(),
            "-f", "wav",
            "-y",
            outputPath
        };
        
        logger.info("音频转码WAV: {} -> {}", inputPath, outputPath);
        return execCmd(cmd);
    }
    
    /**
     * 视频转码为指定编码格式
     */
    public boolean videoTranscoding(String inputPath, String outputPath) {
        return videoTranscoding(inputPath, outputPath, "libx264", "medium", 23);
    }
    
    /**
     * 视频转码(自定义参数)
     */
    public boolean videoTranscoding(String inputPath, String outputPath, 
                                   String codec, String preset, Integer crf) {
        File outputFile = new File(outputPath);
        prepareOutputFile(outputFile);
        
        if (preset == null) preset = "medium";
        if (crf == null) crf = 23;
        
        String[] cmd = new String[]{
            ffmpegProperties.getPath(),
            "-threads", String.valueOf(ffmpegProperties.getMaxThreads()),
            "-i", inputPath,
            "-c:v", codec,
            "-preset", preset,
            "-crf", crf.toString(),
            "-c:a", "aac",
            "-b:a", "128k",
            "-movflags", "+faststart",  // 优化网络播放
            "-f", "mp4",
            "-y",
            outputPath
        };
        
        logger.info("视频转码: {} -> {}", inputPath, outputPath);
        return execCmd(cmd);
    }
    
    /**
     * 提取视频中的音频
     */
    public boolean extractAudio(String videoPath, String audioPath) {
        File outputFile = new File(audioPath);
        prepareOutputFile(outputFile);
        
        String[] cmd = new String[]{
            ffmpegProperties.getPath(),
            "-i", videoPath,
            "-vn",           // 禁用视频流
            "-acodec", "copy", // 直接复制音频流
            "-y",
            audioPath
        };
        
        logger.info("提取音频: {} -> {}", videoPath, audioPath);
        return execCmd(cmd);
    }
    
    /**
     * 获取媒体文件信息
     */
    public MediaInfo getMediaInfo(String filePath) {
        List<String> outputLines = new ArrayList<>();
        OutputHandler handler = outputLines::add;
        
        String[] cmd = new String[]{
            ffmpegProperties.getPath(),
            "-i", filePath
        };
        
        // 这个命令会失败(因为缺少输出文件),但我们可以从错误输出中获取信息
        execCmd(cmd, handler);
        
        return parseMediaInfo(outputLines, filePath);
    }
    
    /**
     * 解析媒体信息
     */
    private MediaInfo parseMediaInfo(List<String> outputLines, String filePath) {
        MediaInfo info = new MediaInfo();
        info.setFilePath(filePath);
        
        for (String line : outputLines) {
            // 解析时长
            if (line.contains("Duration:")) {
                String duration = line.split("Duration:")[1].split(",")[0].trim();
                info.setDuration(duration);
            }
            // 解析视频流信息
            else if (line.contains("Video:")) {
                info.setVideoStream(true);
                if (line.contains("h264")) info.setVideoCodec("H.264");
                else if (line.contains("hevc")) info.setVideoCodec("H.265");
                // 可以添加更多解析逻辑
            }
            // 解析音频流信息
            else if (line.contains("Audio:")) {
                info.setAudioStream(true);
                if (line.contains("aac")) info.setAudioCodec("AAC");
                else if (line.contains("mp3")) info.setAudioCodec("MP3");
            }
        }
        
        return info;
    }
    
    /**
     * 获取PCM格式
     */
    private String getPcmFormat(int bitDepth) {
        switch (bitDepth) {
            case 8: return "pcm_u8";
            case 24: return "pcm_s24le";
            case 32: return "pcm_s32le";
            default: return "pcm_s16le";
        }
    }
    
    /**
     * 准备输出文件
     */
    private void prepareOutputFile(File outputFile) {
        if (outputFile.exists()) {
            outputFile.delete();
        }
        File parentDir = outputFile.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            parentDir.mkdirs();
        }
    }
    
    /**
     * 输出处理器接口
     */
    public interface OutputHandler {
        void handle(String line);
    }
    
    /**
     * 媒体信息类
     */
    public static class MediaInfo {
        private String filePath;
        private String duration;
        private boolean videoStream;
        private boolean audioStream;
        private String videoCodec;
        private String audioCodec;
        
        // getters and setters
        public String getFilePath() { return filePath; }
        public void setFilePath(String filePath) { this.filePath = filePath; }
        public String getDuration() { return duration; }
        public void setDuration(String duration) { this.duration = duration; }
        public boolean isVideoStream() { return videoStream; }
        public void setVideoStream(boolean videoStream) { this.videoStream = videoStream; }
        public boolean isAudioStream() { return audioStream; }
        public void setAudioStream(boolean audioStream) { this.audioStream = audioStream; }
        public String getVideoCodec() { return videoCodec; }
        public void setVideoCodec(String videoCodec) { this.videoCodec = videoCodec; }
        public String getAudioCodec() { return audioCodec; }
        public void setAudioCodec(String audioCodec) { this.audioCodec = audioCodec; }
        
        @Override
        public String toString() {
            return String.format("MediaInfo{filePath='%s', duration='%s', videoStream=%s, audioStream=%s, videoCodec='%s', audioCodec='%s'}",
                    filePath, duration, videoStream, audioStream, videoCodec, audioCodec);
        }
    }
}

3. 应用配置文件

# application.yml
ffmpeg:
  path: /usr/local/bin/ffmpeg  # Windows: C:/ffmpeg/bin/ffmpeg.exe
  timeout: 300                 # 5分钟超时
  max-threads: 4               # 最大线程数

4. 控制器示例

package com.example.controller;

import com.example.util.FfmpegUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/media")
public class MediaController {
    
    @Autowired
    private FfmpegUtil ffmpegUtil;
    
    @PostMapping("/convert-to-wav")
    public Map<String, Object> convertToWav(@RequestParam("file") MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 保存上传文件
            String originalFileName = file.getOriginalFilename();
            String tempDir = System.getProperty("java.io.tmpdir");
            String inputPath = tempDir + File.separator + originalFileName;
            String outputPath = tempDir + File.separator + 
                originalFileName.replaceFirst("\\.[^.]+$", "") + ".wav";
            
            file.transferTo(new File(inputPath));
            
            // 执行转换
            boolean success = ffmpegUtil.audioToWav(inputPath, outputPath);
            
            result.put("success", success);
            result.put("outputFile", outputPath);
            if (success) {
                result.put("message", "转换成功");
            } else {
                result.put("message", "转换失败");
            }
            
            // 清理临时文件
            new File(inputPath).delete();
            
        } catch (IOException e) {
            result.put("success", false);
            result.put("message", "文件处理异常: " + e.getMessage());
        }
        
        return result;
    }
    
    @GetMapping("/media-info")
    public FfmpegUtil.MediaInfo getMediaInfo(@RequestParam String filePath) {
        return ffmpegUtil.getMediaInfo(filePath);
    }
}

5. 异常处理类

package com.example.handler;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(Exception e) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", false);
        result.put("message", "处理失败: " + e.getMessage());
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
    }
}

6. 使用示例

@Service
public class MediaService {
    
    @Autowired
    private FfmpegUtil ffmpegUtil;
    
    public void processMedia() {
        // 音频转WAV
        boolean success = ffmpegUtil.audioToWav(
            "/path/to/input.mp3", 
            "/path/to/output.wav",
            48000, 2, 16
        );
        
        // 获取媒体信息
        FfmpegUtil.MediaInfo info = ffmpegUtil.getMediaInfo("/path/to/video.mp4");
        System.out.println("视频时长: " + info.getDuration());
        
        // 视频转码
        ffmpegUtil.videoTranscoding(
            "/path/to/input.avi",
            "/path/to/output.mp4",
            "libx264", "medium", 23
        );
    }
}

主要特性

  1. 配置化:通过配置文件管理 FFmpeg 路径和参数
  2. 异常处理:完善的异常处理和日志记录
  3. 灵活的输出处理:支持实时处理 FFmpeg 输出
  4. 超时控制:防止长时间运行的命令阻塞系统
  5. 多种媒体操作:支持音视频转码、信息提取等
  6. RESTful API:提供 Web 接口方便调用

这个工具类可以直接集成到 SpringBoot 项目中使用,提供了完整的媒体处理能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BirdMan98

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值