找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2306

积分

0

好友

323

主题
发表于 6 小时前 | 查看: 1| 回复: 0

第一部分:认识 FFmpeg —— 音视频处理的“全能工具集”

FFmpeg 是一套开源的音视频处理工具库与开发库,能够一站式解决音视频领域的绝大多数处理需求:

  • 格式转换:轻松实现 MP4、AVI、MOV 等格式的互转,如同文件格式的“翻译官”;
  • 精准剪辑:毫秒级裁剪视频片段,比专业剪辑软件更灵活;
  • 音频提取:无损分离视频中的音频轨道,效率远超手动操作;
  • 视频压缩:在保证画质的前提下大幅降低文件体积,节省存储和传输成本。
# FFmpeg 核心执行逻辑:
# 基础命令范式:ffmpeg -i 输入文件 [处理参数] 输出文件
# 核心思想:通过参数组合,实现任意音视频处理需求

第二部分:整合步骤 —— 标准化搭建视频处理服务

03.1 步骤1:引入核心依赖(Maven配置)

首先在 pom.xml 中添加 SpringBoot 基础依赖,为视频处理服务搭建基础运行环境:

<!-- pom.xml -->
<dependencies>
    <!-- SpringBoot Web核心依赖:提供HTTP接口能力 -->
    <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>

03.2 步骤2:FFmpeg配置类(参数标准化)

通过配置类统一管理 FFmpeg 的核心参数,支持多环境适配:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * FFmpeg核心配置类
 * 作用:统一管理FFmpeg执行路径、超时时间、线程数等核心参数
 */
@Configuration
@ConfigurationProperties(prefix = "ffmpeg") // 绑定配置文件中ffmpeg前缀的配置项
@Data // Lombok注解:自动生成get/set方法
public class FFmpegConfig {
    /**
     * FFmpeg可执行文件路径
     * Windows示例: "C:/ffmpeg/bin/ffmpeg.exe"
     * Linux/Mac示例: "/usr/bin/ffmpeg"
     */
    private String path;

    /**
     * 处理超时时间(秒)
     * 作用:防止单个视频处理耗时过长,占用服务器资源
     */
    private Long timeout = 3600L;

    /**
     * 处理线程数
     * 作用:多线程提升处理效率,建议与CPU核心数匹配
     */
    private Integer threads = 4;
}

application.yml 中配置具体参数:

ffmpeg:
  path: /usr/local/bin/ffmpeg # 替换为实际FFmpeg安装路径
  timeout: 3600                # 超时时间:1小时
  threads: 4                   # 处理线程数:4核CPU适配值

03.3 步骤3:FFmpeg命令执行器(核心调度层)

封装 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;

/**
 * FFmpeg命令执行器
 * 作用:封装FFmpeg进程启动、命令执行、结果返回的核心逻辑
 */
@Slf4j // 日志注解:简化日志输出
@Component // 交给Spring容器管理
public class FFmpegCommander {

    @Autowired
    private FFmpegConfig ffmpegConfig; // 注入FFmpeg配置类

    /**
     * 执行FFmpeg命令
     * @param commands 命令参数列表(不含FFmpeg路径)
     * @return 执行结果:true成功 / false失败
     */
    public boolean execute(List<String> commands) {
        // 组装完整命令:FFmpeg路径 + 业务参数
        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();

            // 读取进程输出:防止缓冲区溢出导致进程阻塞
            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; // 0表示执行成功

            if (success) {
                log.info("FFmpeg命令执行成功!");
            } else {
                log.error("FFmpeg命令执行失败,退出码:{}", exitCode);
            }

            return success;

        } catch (Exception e) {
            log.error("FFmpeg进程启动/执行异常", e);
            return false;
        }
    }

    /**
     * 获取FFmpeg版本信息
     * 作用:校验FFmpeg是否安装成功、版本是否兼容
     * @return 版本信息字符串
     */
    public String getVersion() {
        try {
            // 执行ffmpeg -version命令
            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();
        }
    }
}

03.4 步骤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() {
        File tempDirFile = new File(TEMP_DIR);
        if (!tempDirFile.exists()) {
            tempDirFile.mkdirs(); // 递归创建目录
        }
    }

    /**
     * 视频格式转换
     * @param inputFile 上传的视频文件
     * @param targetFormat 目标格式(如mp4、avi、mov)
     * @return 转换后的视频文件
     * @throws IOException 文件操作异常
     */
    public File convertFormat(MultipartFile inputFile, String targetFormat) throws IOException {
        log.info("开始视频格式转换:原格式{} → 目标格式{}", 
                 getFileExtension(inputFile.getOriginalFilename()), 
                 targetFormat);

        // 1. 保存上传文件到临时目录
        File inputTempFile = saveTempFile(inputFile);

        // 2. 生成输出文件(UUID命名避免重复)
        String outputFileName = UUID.randomUUID() + "." + targetFormat;
        File outputTempFile = new File(TEMP_DIR + outputFileName);

        // 3. 构建FFmpeg转换命令
        List<String> commands = Arrays.asList(
            "-i", inputTempFile.getAbsolutePath(),     // 输入文件路径
            "-threads", ffmpegConfig.getThreads().toString(), // 处理线程数(从配置读取)
            "-preset", "fast",                 // 编码预设:快速模式(平衡速度和质量)
            "-c:v", "libx264",                 // 视频编码器:H.264(兼容性最好)
            "-c:a", "aac",                     // 音频编码器:AAC(主流音频格式)
            "-y",                              // 覆盖已存在的输出文件
            outputTempFile.getAbsolutePath()   // 输出文件路径
        );

        // 4. 执行转换命令
        boolean success = ffmpegCommander.execute(commands);

        // 5. 清理临时输入文件
        inputTempFile.delete();

        // 6. 校验结果并返回
        if (success && outputTempFile.exists()) {
            long fileSizeMB = outputTempFile.length() / (1024 * 1024);
            log.info("格式转换成功,文件大小:{} MB", fileSizeMB);
            return outputTempFile;
        } else {
            throw new RuntimeException("视频格式转换失败,请检查FFmpeg配置或文件格式");
        }
    }

    /**
     * 提取视频缩略图
     * @param videoFile 视频文件
     * @param second 提取帧的时间点(秒)
     * @return 缩略图文件
     * @throws IOException 文件操作异常
     */
    public File extractThumbnail(MultipartFile videoFile, int second) throws IOException {
        log.info("开始提取视频缩略图:时间点{}秒", second);

        // 1. 保存临时文件
        File inputTempFile = saveTempFile(videoFile);
        String outputFileName = UUID.randomUUID() + ".jpg";
        File outputTempFile = new File(TEMP_DIR + outputFileName);

        // 2. 构建缩略图提取命令
        List<String> commands = Arrays.asList(
            "-i", inputTempFile.getAbsolutePath(),
            "-ss", String.valueOf(second),    // 跳转到指定时间点
            "-vframes", "1",                  // 只提取1帧
            "-vf", "scale=320:-1",           // 缩放:宽度320,高度按比例自动计算
            "-y",                             // 覆盖输出文件
            outputTempFile.getAbsolutePath()
        );

        // 3. 执行命令并清理临时文件
        boolean success = ffmpegCommander.execute(commands);
        inputTempFile.delete();

        // 4. 校验结果
        if (success && outputTempFile.exists()) {
            log.info("视频缩略图提取成功");
            return outputTempFile;
        }
        throw new RuntimeException("视频缩略图提取失败");
    }

    /**
     * 视频压缩(按比特率)
     * @param videoFile 待压缩视频
     * @param targetBitrate 目标视频比特率(kbps)
     * @return 压缩后的视频文件
     * @throws IOException 文件操作异常
     */
    public File compressVideo(MultipartFile videoFile, int targetBitrate) throws IOException {
        log.info("开始视频压缩:目标比特率{} kbps", targetBitrate);

        // 1. 保存临时文件并记录原大小
        File inputTempFile = saveTempFile(videoFile);
        long originalSize = inputTempFile.length();

        // 2. 生成输出文件
        String outputFileName = UUID.randomUUID() + "_compressed.mp4";
        File outputTempFile = new File(TEMP_DIR + outputFileName);

        // 3. 构建压缩命令
        List<String> commands = Arrays.asList(
            "-i", inputTempFile.getAbsolutePath(),
            "-threads", ffmpegConfig.getThreads().toString(),
            "-b:v", targetBitrate + "k",      // 视频比特率(核心压缩参数)
            "-b:a", "128k",                   // 音频比特率(固定128kbps保证音质)
            "-y",
            outputTempFile.getAbsolutePath()
        );

        // 4. 执行命令并清理临时文件
        boolean success = ffmpegCommander.execute(commands);
        inputTempFile.delete();

        // 5. 校验结果并计算压缩率
        if (success && outputTempFile.exists()) {
            long compressedSize = outputTempFile.length();
            double compressionRatio = (1.0 - (double)compressedSize/originalSize) * 100;
            log.info("视频压缩成功!原大小:{}MB,压缩后:{}MB,压缩率:{:.1f}%",
                     originalSize/(1024*1024),
                     compressedSize/(1024*1024),
                     compressionRatio);
            return outputTempFile;
        }
        throw new RuntimeException("视频压缩失败");
    }

    /**
     * 音视频合并(替换视频音频轨道)
     * @param videoFile 视频文件
     * @param audioFile 音频文件
     * @return 合并后的视频文件
     * @throws IOException 文件操作异常
     */
    public File mergeVideoAudio(MultipartFile videoFile, 
                                MultipartFile audioFile) throws IOException {
        log.info("开始音视频合并操作");

        // 1. 保存临时文件
        File videoTempFile = saveTempFile(videoFile);
        File audioTempFile = saveTempFile(audioFile);

        // 2. 生成输出文件
        String outputFileName = UUID.randomUUID() + "_merged.mp4";
        File outputTempFile = new File(TEMP_DIR + outputFileName);

        // 3. 构建合并命令
        List<String> commands = Arrays.asList(
            "-i", videoTempFile.getAbsolutePath(),  // 视频源
            "-i", audioTempFile.getAbsolutePath(),  // 音频源
            "-c:v", "copy",                  // 视频流直接复制(不重新编码,提升速度)
            "-c:a", "aac",                   // 音频流重新编码(保证兼容性)
            "-map", "0:v:0",                 // 映射:取第一个文件的视频轨道
            "-map", "1:a:0",                 // 映射:取第二个文件的音频轨道
            "-shortest",                     // 时长以最短的流为准
            "-y",
            outputTempFile.getAbsolutePath()
        );

        // 4. 执行命令并清理临时文件
        boolean success = ffmpegCommander.execute(commands);
        videoTempFile.delete();
        audioTempFile.delete();

        // 5. 校验结果
        if (success && outputTempFile.exists()) {
            log.info("音视频合并成功");
            return outputTempFile;
        }
        throw new RuntimeException("音视频合并失败");
    }

    /**
     * 保存上传文件到临时目录
     * @param file 上传的MultipartFile文件
     * @return 临时文件对象
     * @throws IOException 文件写入异常
     */
    private File saveTempFile(MultipartFile file) throws IOException {
        // 生成唯一文件名:UUID + 原文件名(避免冲突)
        String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
        Path tempPath = Paths.get(TEMP_DIR + fileName);
        // 写入文件到临时目录
        Files.copy(file.getInputStream(), tempPath);
        return tempPath.toFile();
    }

    /**
     * 获取文件扩展名
     * @param filename 文件名
     * @return 扩展名(小写)
     */
    private String getFileExtension(String filename) {
        if (filename == null || filename.isEmpty()) {
            return "unknown";
        }
        int dotIndex = filename.lastIndexOf('.');
        return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1).toLowerCase();
    }
}

03.5 步骤5:控制器层(HTTP接口暴露)

封装 RESTful 接口,对外提供视频处理能力:

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;

/**
 * 视频处理接口控制器
 * 作用:暴露HTTP接口,接收前端请求并调用服务层处理
 */
@Slf4j
@RestController
@RequestMapping("/api/video") // 接口统一前缀
public class VideoController {

    @Autowired
    private VideoService videoService;

    @Autowired
    private FFmpegCommander ffmpegCommander;

    /**
     * 获取FFmpeg版本信息接口
     * @return 版本信息JSON
     */
    @GetMapping("/version")
    public String getFFmpegVersion() {
        String version = ffmpegCommander.getVersion();
        return "{\"version\": \"" + version + "\"}";
    }

    /**
     * 视频格式转换接口
     * @param file 上传的视频文件
     * @param format 目标格式
     * @return 转换后的文件下载响应
     * @throws IOException 文件操作异常
     */
    @PostMapping("/convert")
    public ResponseEntity<Resource> convertFormat(
            @RequestParam("file") MultipartFile file,
            @RequestParam("format") String format) throws IOException {

        log.info("接收视频格式转换请求:文件名{},目标格式{}", file.getOriginalFilename(), format);

        // 调用服务层转换方法
        File convertedFile = videoService.convertFormat(file, format);

        // 构建文件下载响应
        return buildFileResponse(convertedFile,
                "converted." + format, 
                MediaType.APPLICATION_OCTET_STREAM);
    }

    /**
     * 视频缩略图提取接口
     * @param file 视频文件
     * @param second 提取帧的时间点(默认5秒)
     * @return 缩略图文件下载响应
     * @throws IOException 文件操作异常
     */
    @PostMapping("/thumbnail")
    public ResponseEntity<Resource> extractThumbnail(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "second", defaultValue = "5") int second) throws IOException {

        File thumbnailFile = videoService.extractThumbnail(file, second);

        return buildFileResponse(thumbnailFile,
                "thumbnail.jpg",
                MediaType.IMAGE_JPEG);
    }

    /**
     * 视频压缩接口
     * @param file 视频文件
     * @param bitrate 目标比特率(默认1000kbps)
     * @return 压缩后的文件下载响应
     * @throws IOException 文件操作异常
     */
    @PostMapping("/compress")
    public ResponseEntity<Resource> compressVideo(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "bitrate", defaultValue = "1000") int bitrate) throws IOException {

        File compressedFile = videoService.compressVideo(file, bitrate);

        return buildFileResponse(compressedFile,
                "compressed.mp4",
                MediaType.APPLICATION_OCTET_STREAM);
    }

    /**
     * 音视频合并接口
     * @param video 视频文件
     * @param audio 音频文件
     * @return 合并后的文件下载响应
     * @throws IOException 文件操作异常
     */
    @PostMapping("/merge")
    public ResponseEntity<Resource> mergeVideoAudio(
            @RequestParam("video") MultipartFile video,
            @RequestParam("audio") MultipartFile audio) throws IOException {

        File mergedFile = videoService.mergeVideoAudio(video, audio);

        return buildFileResponse(mergedFile,
                "merged.mp4",
                MediaType.APPLICATION_OCTET_STREAM);
    }

    /**
     * 构建文件下载响应
     * @param file 待下载文件
     * @param filename 下载文件名
     * @param mediaType 文件媒体类型
     * @return ResponseEntity<Resource> 下载响应
     */
    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);
    }
}

03.6 步骤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;

/**
 * 全局异常处理器
 * 作用:统一捕获控制器层异常,返回标准化JSON响应
 */
@Slf4j
@RestControllerAdvice // 全局控制器异常处理
public class GlobalExceptionHandler {

    /**
     * 通用异常处理
     * @param e 异常对象
     * @return 标准化错误响应
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(Exception e) {
        log.error("系统异常", e);

        Map<String, Object> response = new HashMap<>();
        response.put("success", false);
        response.put("message", "视频处理失败,请稍后重试");
        response.put("error", e.getMessage());
        response.put("timestamp", System.currentTimeMillis());

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(response);
    }

    /**
     * 文件大小超限异常处理
     * @return 标准化错误响应
     */
    @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);
    }

    /**
     * 文件IO异常处理
     * @param e IO异常对象
     * @return 标准化错误响应
     */
    @ExceptionHandler(IOException.class)
    public ResponseEntity<Map<String, Object>> handleIOException(IOException e) {
        log.error("文件IO异常", 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);
    }
}

第三部分:服务启动与接口测试

04.1 应用启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 视频处理服务启动类
 * 作用:启动SpringBoot应用,加载所有配置和组件
 */
@SpringBootApplication // 包含@ComponentScan、@EnableAutoConfiguration、@Configuration
public class VideoProcessingApplication {
    public static void main(String[] args) {
        SpringApplication.run(VideoProcessingApplication.class, args);
        System.out.println("=====================================");
        System.out.println("视频处理服务启动成功!");
        System.out.println("FFmpeg已就绪,可通过/api/video接口调用");
        System.out.println("=====================================");
    }
}

04.2 接口测试示例

使用 curl 命令快速测试接口(也可使用 Postman、Apifox 等工具):

# 1. 查看FFmpeg版本
curl http://localhost:8080/api/video/version

# 2. 视频格式转换:avi → mp4
curl -X POST -F "file=@input.avi" -F "format=mp4" \
     http://localhost:8080/api/video/convert --output output.mp4

# 3. 提取第10秒缩略图
curl -X POST -F "file=@video.mp4" -F "second=10" \
     http://localhost:8080/api/video/thumbnail --output thumbnail.jpg

# 4. 压缩视频(目标比特率500kbps)
curl -X POST -F "file=@large_video.mp4" -F "bitrate=500" \
     http://localhost:8080/api/video/compress --output compressed.mp4

第四部分:高级扩展技巧

05.1 进度监听(实时反馈处理进度)

实现进度监听接口,实时返回视频处理进度:

/**
 * 视频处理进度监听接口
 * 作用:实时反馈处理进度、完成状态、错误信息
 */
public interface ProgressListener {
    /**
     * 进度更新回调
     * @param percentage 处理进度(0-100)
     * @param message 进度描述
     */
    void onProgress(double percentage, String message);

    /**
     * 处理完成回调
     * @param outputFile 处理后的文件
     */
    void onComplete(File outputFile);

    /**
     * 处理失败回调
     * @param error 错误信息
     */
    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=")) {
        // 1. 提取time字段(已处理时长)
        // 2. 结合视频总时长计算进度百分比
        // 3. 调用listener.onProgress()反馈进度
    }
}

05.2 批量处理(提升多文件处理效率)

并行处理多个视频文件,提升整体处理效率:

import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 批量视频格式转换
 * @param files 待处理文件列表
 * @param format 目标格式
 * @return 处理完成的文件列表
 */
public List<File> batchConvert(List<MultipartFile> files, String format) {
    // 空值校验
    if (CollectionUtils.isEmpty(files)) {
        return List.of();
    }

    // 并行流处理:提升多文件处理效率
    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());
}

05.3 视频信息解析(获取文件元数据)

通过 FFprobe(FFmpeg 配套工具)解析视频元数据:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 解析视频元数据
 * @param videoFile 视频文件
 * @return 包含分辨率、时长、码率等信息的Map
 */
public Map<String, Object> getVideoInfo(File videoFile) {
    // 构建FFprobe命令(FFmpeg配套工具,专用于解析媒体信息)
    List<String> commands = Arrays.asList(
        ffmpegConfig.getPath().replace("ffmpeg", "ffprobe"), // 替换为ffprobe路径
        "-v", "error", // 只输出错误信息
        "-select_streams", "v:0", // 只选择视频流
        "-show_entries", "stream=width,height,duration,bit_rate,codec_name", // 要提取的字段
        "-of", "json", // 输出格式:JSON
        videoFile.getAbsolutePath()
    );

    // 执行命令并读取输出
    String jsonOutput = executeCommand(commands); // 需实现命令执行并返回字符串的方法

    // 解析JSON并封装结果
    JSONObject jsonObject = JSON.parseObject(jsonOutput);
    JSONObject stream = jsonObject.getJSONArray("streams").getJSONObject(0);

    Map<String, Object> videoInfo = new HashMap<>();
    videoInfo.put("width", stream.getInteger("width")); // 宽度
    videoInfo.put("height", stream.getInteger("height")); // 高度
    videoInfo.put("duration", stream.getDouble("duration")); // 时长(秒)
    videoInfo.put("bitRate", stream.getLong("bit_rate")); // 比特率(bps)
    videoInfo.put("codec", stream.getString("codec_name")); // 编码格式

    return videoInfo;
}

第五部分:生产环境注意事项

06.1 环境配置要点

  1. FFmpeg环境校验
    确保服务器已安装 FFmpeg,且配置路径正确:

    # 检查FFmpeg是否安装
    ffmpeg -version
    # 检查FFprobe是否安装(用于解析视频信息)
    ffprobe -version
  2. JVM资源配置
    视频处理消耗内存,需调整 JVM 参数:

    # 启动脚本示例:分配2G堆内存
    java -Xmx2g -jar video-processing.jar
  3. 临时文件清理
    定时清理临时目录,避免磁盘空间耗尽:

    # Linux定时任务示例:每天凌晨清理7天前的临时文件
    0 0 * * * find /tmp/video-process/ -mtime +7 -delete

06.2 常见问题与解决方案

问题类型 常见原因 解决方案
跨平台路径异常 Windows/Linux路径分隔符不同 使用 File.separator 或Paths类统一处理路径
编码兼容问题 部分格式编码不支持 统一使用libx264(视频)+aac(音频)编码
权限不足 FFmpeg执行权限/临时目录写入权限不足 给FFmpeg可执行文件添加执行权限(chmod +x),给临时目录添加写入权限
处理超时 大文件处理耗时过长 调整超时配置,采用异步处理(如MQ+异步线程)

06.3 安全防护措施

  1. 限制文件上传大小(在 application.yml 中配置):
    spring:
      servlet:
        multipart:
          max-file-size: 100MB # 单个文件最大100MB
          max-request-size: 200MB # 单次请求最大200MB
  2. 校验文件类型:仅允许上传视频/音频格式(如 mp4、avi、mp3 等);
  3. 防止命令注入:对用户输入的参数进行严格校验,避免拼接恶意命令;
  4. 异步处理:大文件处理采用异步模式,避免阻塞 HTTP 请求。

总结

通过 SpringBoot 整合 FFmpeg,可以快速搭建一套功能完善的视频处理服务,覆盖格式转换、缩略图提取、压缩、音视频合并等核心场景。此方案基于成熟的 Java 生态,实现成本低,可扩展性强。

该方案的核心优势:

  • 灵活性:支持 FFmpeg 所有原生命令,可扩展任意视频处理能力;
  • 标准化:基于 SpringBoot 生态,符合企业级开发规范;
  • 可维护性:分层设计(配置层→执行层→服务层→接口层),便于扩展和维护;
  • 低成本:基于开源工具,无商业授权成本。

在生产环境中,需重点关注资源占用、异常处理、安全防护三大核心点,确保服务稳定运行。对于更复杂的生产级需求,可以参考在云栈社区分享的分布式架构与持续集成部署方案,构建更健壮的视频处理中台。




上一篇:机器人落地应用的五道技术鸿沟:从VLA模型到工业部署的挑战
下一篇:B端产品经理权限设计避坑指南:为什么你做的系统没人用?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 16:50 , Processed in 0.294054 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表