开发者

SpringBoot整合FFmpeg进行视频处理的详细教学

开发者 https://www.devze.com 2026-01-06 10:22 出处:网络 作者: 悟空码字
价值2999元 Java视频教程限时免费下载
专为Java开发者设计,涵盖核心技术、架构设计、性能优化等
立即下载
目录第一部分:认识 FFmpeg 第二部分:整合步骤 —— 像组装乐高一样简单步骤1:先给项目来点“开胃菜”—— Maven依赖步骤2:配置FFmpeg —— 像教AI用筷子步骤3:创
目录
  • 第一部分:认识 FFmpeg 
  • 第二部分:整合步骤 —— 像组装乐高一样简单
    • 步骤1:先给项目来点“开胃菜”—— Maven依赖
    • 步骤2:配置FFmpeg —— 像教AI用筷子
    • 步骤3:创建FFmpeg指挥官
    • 步骤4:创建视频处理服务 —— 你的私人视频管家
    • 步骤5:创建控制器 —&mdasphph; 视频处理的接待处
    • 步骤6:添加异常处理 —— 给程序买份保险
  • 第三部分:使用示例 —— 让我们来实际操练一下
    • 1. 启动应用程序
    • 2. 测试API
  • 第四部分:高级技巧 —— 让FFmpeg更懂你
    • 1. 添加进度监听(给视频处理加个进度条)
    • 2. 批量处理(一次处理多个文件)
    • 3. 视频信息提取(给视频做体检)
  • 第五部分:总结与注意事项
    • 成功整合的秘诀:
    • 可能遇到的坑
    • 为什么选择这个方案?
  • 最后

    第一部分:认识 FFmpeg 

    FFmpeg 是什么?想象一下,如果你有一个朋友,他能:

    • 把 MP4 变成 AVI,就像把咖啡变成奶茶
    • 裁剪视频,比理发师剪头发还精准
    • 提取音频,比从披萨上分离芝士还干净
    • 压缩视频,比你把行李箱塞满时还高效

    这个“万能朋友”就是 FFmpeg!它是一个开源的声音/影像处理工具,功能强大到能让好莱坞特效师失业(开玩笑的)。

    # FFmpeg 的基本心态:

    # "给我一个视频,我能还你一个世界"

    # 实际上它想说的是:"ffmpeg -i input.mp4 [一堆参数] output.mp4"

    第二部分:整合步骤 —— 像组装乐高一样简单

    步骤1:先给项目来点“开胃菜”—— Maven依赖

    <!-- pom.XML -->
    <dependencies>
        <!-- SpringBoot 标准配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- 让我们记录FFmpeg的“精彩表演” -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        
        <!-- 视频处理时的“后悔药”——异常处理 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>
    

    步骤2:配置FFmpeg —— 像教AI用筷子

    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ConfigurationProperties(prefix = "ffmpeg")
    @Data
    public class FFmpegConfig {
        /**
         * FFmpeg可执行文件路径
         * Windows: "C:/ffmpeg/bin/ffmpeg.exe"
         * linux/MAC: "/usr/bin/ffmpeg"
         */
        private String path;
        
        /**
         * 超时时间(秒)
         * 防止视频处理变成“永恒等待”
         */
        private Long timeout = 3600L;
        
        /**
         * 线程数
         * 多线程就像多双手,干活更快!
         */
        private Integer threads = 4;
    }
    
    # application.yml
    ffmpeg:
      path: /usr/local/bin/ffmpeg  # 你的FFmpeg安装路径
      timeout: 3600                # 1小时,足够看一集电视剧了
      threads: 4                   # 4个线程,四核处理器的最爱
    

    步骤3:创建FFmpeg指挥官

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import Java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.List;
    
    @Slf4j
    @Component
    public class FFmpegCommander {
        
        @Autowired
        private FFmpegConfig ffmpegConfig;
        
        /**
         * 执行FFmpeg命令
         * @param commands 命令参数(像给厨师递菜单)
         * @return 是否成功(厨子有没有把菜做糊)
         */
        public boolean execute(List<String> commands) {
            List<String> fullCommand = new ArrayList<>();
            fullCommand.add(ffmpegConfig.getPath());
            fullCommand.addAll(commands);
            
            log.info("FFmpeg开始干活啦!命令:{}", String.join(" ", fullCommand));
            
            ProcessBuilder processBuilder = new ProcessBuilder(fullCommand);
            processBuilder.redirectErrorStream(true); // 错误输出也给我看看
            
            try {
                Process process = processBuilder.start();
                
                // 读取输出,防止FFmpeg“自言自语”没人听
                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(process.getInputStream()))) {
                    String line;
                    while ((line = reader.readLine()) != null) {
                        log.debug("FFmpeg悄悄说:{}", line);
                    }
                }
                
                // 等待处理完成,别急着催
                int exitCode = process.waitFor();
                boolean success = exitCode == 0;
                
                if (success) {
                    log.info("FFmpeg完美收工!");
                } else {
                    log.error("FFmpeg罢工了!退出码:{}", exitCode);
                }
                
                return success;
                
            } catch (Exception e) {
                log.error("FFmpeg崩溃了,原因:{}", e.getMessage(), e);
                return false;
            }
        }
        
        /**
         * 获取FFmpeg版本(验明正身)
         */
        public String getVersion() {
            try {
                Process process = new ProcessBuilder(ffmpegConfig.getPath(), "-version").start();
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(process.getInputStream()));
                return reader.readLine(); // 第一行就是版本信息
            } catch (Exception e) {
                return "FFmpeg可能去度假了:" + e.getMessage();
            }
        }
    }
    

    步骤4:创建视频处理服务 —— 你的私人视频管家

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.Arrays;
    import java.util.List;
    import java.util.UUID;
    
    @Slf4j
    @Service
    public class VideoService {
        
        @Autowired
        private FFmpegCommander ffmpegCommander;
        
        // 临时文件存放目录(像快递的临时存放点)
        private final String TEMP_DIR = System.getProperty("java.io.tmpdir") + "/video-process/";
        
        public VideoService() {
            // 确保临时目录存在
            new File(TEMP_DIR).mkdirs();
        }
        
        /**
         * 转换视频格式(像把中文翻译成英文)
         * @param inputFile 输入文件
         * @param targetFormat 目标格式(mp4, avi, mov...)
         */
        public File convertFormat(MultipartFile inputFile, String targetFormat) throws IOException {
            log.info("开始格式转换:{} → {}", 
                     getFileExtension(inputFile.getOriginalFilename()), 
                     targetFormat);
            
            // 1. 保存上传的文件(像把食材先放到厨房)
            File input = saveTempFile(inputFile);
            
            // 2. 准备输出文件(准备好盘子)
            String outputFileName = UUID.randomUUID() + "." + targetFormat;
            File output = new File(TEMP_DIR + outputFileName);
            
            // 3. 构建FFmpeg命令菜单
            List<String> commands = Arrays.asList(
                "-i", input.getAbsolutePath(),     // 输入文件
                "-threads", "4",                   // 用4个线程
                "-preset", "fast",                 // 快速预设
                "-c:v", "libx264",                 // 视频编码
                "-c:a", "aac",                     // 音频编码
                "-y",                              // 覆盖输出文件(别问我是否确定)
                output.getAbsolutePath()           // 输出文件
            );
            
            // 4. 让FFmpeg大厨开始烹饪
            boolean success = ffmpegCommander.execute(commands);
            
            // 5. 清理临时文件(洗盘子)
            input.delete();
            
            if (success && output.exists()) {
                log.info("格式转换成功!文件大小:{} MB", 
                         output.length() / (1024 * 1024));
                return output;
            } else {
                throw new RuntimeException("转换失败,FFmpeg可能去做美甲了");
            }
        }
        
        /**
         * 提取视频缩略图(给视频拍证件照)
         */
        public File extractThumbnail(MultipartFile videoFile, int second) throws IOException {
            log.info("正在给视频拍第{}秒的证件照...", second);
            
            File input = saveTempFile(videoFile);
            String outputFileName = UUID.randomUUID() + ".jpg";
            File output = new File(TEMP_DIR + outputFileName);
            
            List<String> commands = Arrays.asList(
                "-i", input.getAbsolutePath(),
                "-ss", String.valueOf(second),    // 跳转到指定秒数
                "-vframes", "1",                  // 只要1帧
                "-vf", "scale=320:-1",           // 缩放到宽度320,高度自动
                "-y",
                output.getAbsolutePath()
            );
            
            boolean success = ffmpegCommander.execute(commands);
            input.delete();
            
            if (success && output.exists()) {
                log.info("缩略图生成成功!");
                return output;
            }
            throw new RuntimeException("拍照失败,视频可能害羞了");
        }
        
        /**
         * 压缩视频(给视频减肥)
         */
        public File compressVideo(MultipartFile videoFile, int targetBitrate) throws IOException {
            log.info("开始给视频减肥,目标比特率:{}k", targetBitrate);
            
            File input = saveTempFile(videoFile);
            long originalSize = input.length();
            
            String outputFileName = UUID.randomUUID() + "_compressed.mp4";
            File output = new File(TEMP_DIR + outputFileName);
            
            List<String> commands = Arrays.asList(
                "-i", input.getAbsolutePath(),
                "-threads", "4",
                "-b:v", targetBitrate + "k",      // 目标视频比特率
                "-b:a", "128k",                   // 音频比特率
                "-y",
                output.getAbsolutePath()
            );
            
            boolean success = ffmpegCommander.execute(commands);
            input.delete();
            
            if (success && output.exists()) {
                long compressedSize = output.length();
                double ratio = (1.0 - (double)compressedSize/originalSize) * 100;
                log.info("减肥成功!原大小:{}MB,现大小:{}MB,瘦身:{:.1f}%",
                         originalSize/(1024*1024),
                         compressedSize/(1024*1024),
                         ratio);
                return output;
            }
            throw new RuntimeException("减肥失败,视频可能偷吃宵夜了");
        }
        
        /**
         * 合并视频和音频(像给电影配音)
         */
        public File mergeVideoAudio(MultipartFile videoFile, 
                                    MultipartFile audioFile) throws IOException {
            log.info("开始给视频配音...");
            
            File video = saveTempFile(videoFile);
            File audio = saveTempFile(audioFile);
            
            String outputFileName = UUID.randomUUID() + "_merged.mp4";
            File output = new File(TEMP_DIR + outputFileName);
            
            List<String> commands = Arrays.asList(
                "-i", video.getAbsolutePath(),
                "-i", audio.getAbsolutePath(),
                "-c:v", "copy",                   // 视频流直接复制(不重新编码)
                "-c:a", "aac",                    // 音频重新编码
                "-map", "0:v:0",                  // 取第一个文件的视频
                "-map", "1:a:0",                  // 取第二个文件的音频
                "-shortest",                      // 以最短的流为准
                "-y",
                output.getAbsolutePath()
            );
            
            boolean success = ffmpegCommander.execute(commands);
            video.delete();
            audio.delete();
            
            if (success && output.exists()) {
                log.info("配音成功!新视频诞生了");
                return output;
            }
            throw new RuntimeException("合并失败,可能视频和音频在闹离婚");
        }
        
        private File saveTempFile(MultipartFile file) throws IOException {
            String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
            Path path = Paths.get(TEMP_DIR + fileName);
            Files.copy(file.getInputStream(), path);
            return path.toFile();
        }
        
        private String getFileExtension(String filename) {
            if (filename == null) return "unknown";
            int dotIndex = filename.lastIndexOf('.');
            return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1);
        }
    }
    

    步骤5:创建控制器 —— 视频处理的接待处

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.FileSystemResource;
    import org.springframework.core.io.Resource;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.File;
    import java.io.IOException;
    
    @Slf4j
    @RestController
    @RequestMapping("/api/video")
    public class VideoController {
        
        @Autowired
        private VideoService videoService;
        
        @Autowired
        private FFmpegCommander ffmpegCommander;
        
        @GetMapping("/version")
        public String getFFmpegVersion() {
            String version = ffmpegCommander.getVersion();
            return "{\"version\": \"" + version + "\"}";
        }
        
        @PostMapping("/convert")
        public ResponseEntity<Resource> convertFormat(
                @RequestParam("file") MultipartFile file,
                @RequestParam("format") String format,
                HttpServletResponse response) throws IOException {
            
            log.info("收到转换请求:{} → {}", file.getOriginalFilename(), format);
            
            File converted = videoService.convertFormat(file, format);
            
            return buildFileResponse(converted, 
                    "converted." + format, 
                    MediaType.APPLICATION_OCTET_STREAM);
        }
        
        @PostMapping("/thumbnail")
        public ResponseEntity<Resource> extractThumbnail(
                @RequestParam("file") MultipartFile file,
                @RequestParam(value = "second", defaultValue = "5") int second) throws IOException {
            
            File thumbnail = videoService.extractThumbnail(file, second);
            
            return buildFileResponse(thumbnail,
                    "thumbnail.jpg",
                    MediaType.IMAGE_JPEG);
        }
        
        @PostMapping("/compress")
        public ResponseEntity<Resource> compressVideo(
                @RequestParam("file") MultipartFile file,
                @RequestParam(value = "bitrate", defaultValue = "1000") int bitrate) throws IOException {
            
            File compressed = videoService.compressVideo(file, bitrate);
            
            return buildFileResponse(compressed,
                    "compressed.mp4",
                    MediaType.APPLICATION_OCTET_STREAM);
        }
        
        @PostMapping("/merge")
        public ResponseEntity<Resource> mergeVideoAudio(
                @RequestParam("video") MultipartFile video,
                @RequestParam("audio") MultipartFile audio) throws IOException {
            
            File merged = videoService.mergeVideoAudio(video, audio);
            
            return buildFileResponse(merged,
                    "merged.mp4",
                    MediaType.APPLICATION_OCTET_STREAM);
        }
        
        private ResponseEntity<Resource> buildFileResponse(File file, 
                                                           String filename,
                                                           MediaType mediaType) {
            if (!file.exists()) {
                return ResponseEntity.notFound().build();
            }
            
            Resource resource = new FileSystemResource(file);
            
            // 文件下载完成后自动删除(深藏功与名)
            file.deleteOnExit();
            
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "attachment; filename=\"" + filename + "\"")
                    .contentType(mediaType)
                    .contentLength(file.length())
                    .body(resource);
        }
    }
    

    步骤6:添加异常处理 —— 给程序买份保险

    import lombok.extern.slf4j.Slf4j;
    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 org.springframework.web.multipart.MaxUploadSizeExceededException;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @Slf4j
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        
        @ExceptionHandler(Exception.class)
        public ResponseEntity<Map<String, Object>> handleException(Exception e) {
            log.error("系统闹情绪了:{}", e.getMessage(), e);
            
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "服务器开小差了,可能是FFmpeg在偷懒");
            response.put("error", e.getMessage());
            response.put("timestamp", System.currentTimeMillis());
            
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(response);
        }
        
        @ExceptionHandler(MaxUploadSizeExceededException.class)
        public ResponseEntity<Map<String, Object>> handleMaxSizeException() {
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "文件太大了,服务器拿不动了");
            response.put("suggestion", "请尝试压缩视频或上传小一点的文件");
            
            return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                    .body(response);
        }
        
        @ExceptionHandler(IOException.class)
        public ResponseEntity<Map<String, Object>> handleIOException(IOException e) {
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "文件读写出了问题,可能是磁盘在闹脾气");
            response.put("error", e.getMessage());
            
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(response);
        }
    }
    

    第三部分:使用示例 —— 让我们来实际操练一下

    1. 启动应用程序

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class VideoProcessingApplication {
        public static void main(String[] args) {
            SpringApplication.run(VideoProcessingApplication.class, args);
            System.out.println("视频处理服务启动成功!");
            System.out.println("FFmpeg整装待发,随时准备处理你的视频");
        }
    }
    

    2. 测试APIjavascript

    使用Postman或curl测试:

    # 查看FFmpeg版本
    curl http://localhost:8080/api/video/version
    
    # 转换视频格式
    curl -X POST -F "file=@input.avi" -F "format=mp4"android \
         http://localhost:8080/api/video/convert --output output.mp4
    
    # 提取缩略图
    curl -X POST -F "file=@video.mp4" -F "second=10" \
         http://localhost:8080/api/video/thumbnail --output thumbnail.jpg
    
    # 压缩视频
    curl -X POST -F "file=@large_video.mp4" -F "bitrate=500" \
         http://localhost:8080/api/video/compress --output compressed.mp4
    

    第四部分:高级技巧 —— 让FFmpeg更android懂你

    1. 添加进度监听(给视频处理加个进度条)

    public interface ProgressListener {
        void onProgress(double percentage, String message);
        void onComplete(File outputFile);
        void onError(String error);
    }
    
    // 在FFmpegCommander中添加进度解析
    private void parseProgress(String line, ProgressListener listener) {
        // 解析FFmpeg的输出,提取进度信息
        // 示例输出:frame=  123 fps=25.1 time=00:00:04.92 bitrate= 512.0kbits/s
        if (line.contains("time=")) {
            // 这里可以解析时间,计算进度百分比
            // 实际实现需要根据视频总时长计算
        }
    }
    

    2. 批量处理(一次处理多个文件)

    public List<File> BATchConvert(List<MultipartFile> files, String format) {
       js return files.parallelStream()  // 并行处理,更快!
                .map(file -> {
                    try {
                        return videoService.convertFormat(file, format);
                    } catch (IOException e) {
                        log.error("转换失败:{}", file.getOriginalFilename(), e);
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
    

    3. 视频信息提取(给视频做体检)

    public Map<String, Object> getVideoInfo(File videoFile) {
        // 使用FFprobe(FFmpeg的小伙伴)获取视频信息
        List<String> commands = Arrays.asList(
            "-v", "error",
            "-select_streams", "v:0",
            "-show_entries", "stream=width,height,duration,bit_rate,codec_name",
            "-of", "json",
            videoFile.getAbsolutePath()
        );
        
        // 执行命令并解析JSON结果
        // 返回包含分辨率、时长、码率、编码格式等信息
    }
    

    第五部分:总结与注意事项

    成功整合的秘诀:

    正确安装FFmpeg:确保系统PATH中有FFmpeg,或者配置正确的路径

    # 检查安装
    ffmpeg -version
    

    资源管理

    java -Xmx2g -jar your-application.jar
    
    • 视频处理很吃内存,记得给JVM足够的内存
    • 及时清理临时文件,防止磁盘被撑爆

    错误处理

    • FFmpeg可能会因为各种原因失败(不支持的格式、损坏的文件等)
    • 添加重试机制和详细的日志记录

    安全性

    • 限制上传文件类型和大小
    • 对用户输入进行严格验证
    • 防止命令注入攻击

    可能遇到的坑

    1. 跨平台问题:Windows和Linux下的路径差异
    2. 编码问题:中文字符在命令中可能需要特殊处理
    3. 权限问题:确保应用有执行FFmpeg的权限
    4. 性能问题:大文件处理可能需要很长时间,考虑异步处理

    为什么选择这个方案?

    1. 灵活性强:可以执行任何FFmpeg支持的操作
    2. 功能全面:视频处理界的"瑞士军刀"
    3. 社区支持好:遇到问题容易找到解决方案
    4. 免费开源:省钱又省心

    最后

    FFmpeg就像一把强大的电锯——功能强大但需要小心使用。不要在生产环境直接运行未经验证的命令,否则可能会:

    • 把服务器CPU烧得像烤红薯
    • 让磁盘空间消失得比钱包里的钱还快
    • 产生一堆让你怀疑人生的临时文件

    但只要你按照本文的步骤,像对待一只温顺的猫一样对待FFmpeg,它就会成为你在视频处理领域最得力的助手!

    以上就是SpringBoot整合FFmpeg进行视频处理的详细教学的详细内容,更多关于SpringBoot FFmpeg视频处理的资料请关注编程客栈(www.devze.com)其它相关文章!

    0
    价值2999元 Java视频教程限时免费下载
    专为Java开发者设计,涵盖核心技术、架构设计、性能优化等
    立即下载

    精彩评论

    暂无评论...
    验证码 换一张
    取 消