当前位置: 代码网 > it编程>编程语言>Java > SpringBoot整合FFmpeg进行视频处理的详细教学

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

2026年01月05日 Java 我要评论
第一部分:认识 ffmpegffmpeg 是什么?想象一下,如果你有一个朋友,他能:把 mp4 变成 avi,就像把咖啡变成奶茶裁剪视频,比理发师剪头发还精准提取音频,比从披萨上分离芝士还干净压缩视频

第一部分:认识 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. 测试api

使用postman或curl测试:

# 查看ffmpeg版本
curl http://localhost:8080/api/video/version

# 转换视频格式
curl -x post -f "file=@input.avi" -f "format=mp4" \
     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更懂你

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) {
    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视频处理的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com