文章目录
1 摘要
FFmpeg 是最常用的跨平台的音频、视频处理软件,但是其通过命令行的方式进行操作对于普通用户而言上手难度大,同时 FFmpeg 只是函数库,对于不同的编程语言,需要自行适配API接口,以便于操作文件。本文介绍SpringBoot 集成 FFmpeg 实现对音视频文件的解析。
FFmpeg 官网: https://ffmpeg.org
FFmpeg Java 平台常用适配仓库:
JavaCV : https://github.com/bytedeco/javacv
JavaCV 是一个集成第三方函数库的平台,包括 OpenCV、FFmpeg 等知名函数库,提供统一的 API 操作,应用广泛。在不考虑应用程序体积大小的情况下推荐使用 JavaCV 作为集成方案。
JAVE2: https://github.com/a-schild/jave2
JAVE2 是将 FFmpeg 进行封装,并提供 Java API 以供用户操作音视频文件的依赖库。用户可以根据软件运行的操作系统来自由选择所需平台的依赖。
本文是基于 JAVE2 依赖来实现解析音视频文件的功能。
2 核心 Maven 依赖
demo-ffmpeg-media/pom.xml
<!-- ffmpeg 音视频处理 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>${schild-ffmpeg.version}</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win64</artifactId>
<version>${schild-ffmpeg.version}</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
<version>${schild-ffmpeg.version}</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osx64</artifactId>
<version>${schild-ffmpeg.version}</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osxm1</artifactId>
<version>${schild-ffmpeg.version}</version>
</dependency>
其中 schild-ffmpeg
版本为:
<schild-ffmpeg.version>3.5.0</schild-ffmpeg.version>
这里分别引入了 Windows、Linux、macOS 系统的依赖,可根据软件运行环境进行删减。
3 核心代码
3.1 FFmpeg 解析音视频工具类
demo-ffmpeg-media/src/main/java/com/ljq/demo/springboot/ffmpeg/common/util/FFmpegMediaUtil.java
package com.ljq.demo.springboot.ffmpeg.common.util;
import com.ljq.demo.springboot.ffmpeg.model.response.AudioInfoResponse;
import com.ljq.demo.springboot.ffmpeg.model.response.VideoInfoResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.info.AudioInfo;
import ws.schild.jave.info.MultimediaInfo;
import ws.schild.jave.info.VideoInfo;
import java.io.File;
import java.nio.file.Files;
import java.util.Objects;
/**
* @Description: FFmpeg 音视频工具类
* @Author: junqiang.lu
* @Date: 2024/5/10
*/
@Slf4j
public class FFmpegMediaUtil {
/**
* 获取视频信息
*
* @param videoPath 视频路径
* @return 视频信息
*/
public static VideoInfoResponse getVideoInfo(String videoPath) {
VideoInfoResponse response = null;
try {
// 解析文件
File videoFile = new File(videoPath);
MultimediaObject multimediaObject = new MultimediaObject(videoFile);
MultimediaInfo multimediaInfo = multimediaObject.getInfo();
VideoInfo videoInfo = multimediaInfo.getVideo();
// 判断是否为视频
if (Objects.isNull(videoInfo) || videoInfo.getBitRate() < 0) {
return null;
}
response = new VideoInfoResponse();
response.setFormat(multimediaInfo.getFormat())
.setDuration(multimediaInfo.getDuration() / 1000)
.setSize(videoFile.length())
.setMd5(DigestUtils.md5DigestAsHex(Files.readAllBytes(videoFile.toPath())));
response.setBitRate(videoInfo.getBitRate())
.setFrameRate(videoInfo.getFrameRate())
.setWidth(videoInfo.getSize().getWidth())
.setHeight(videoInfo.getSize().getHeight());
return response;
} catch (Exception e) {
log.warn("Error processing video file", e);
}
return response;
}
/**
* 获取音频信息
*
* @param audioPath 音频路径
* @return 音频信息
*/
public static AudioInfoResponse getAudioInfo(String audioPath) {
AudioInfoResponse response = null;
try {
// 解析文件
File videoFile = new File(audioPath);
MultimediaObject multimediaObject = new MultimediaObject(videoFile);
MultimediaInfo multimediaInfo = multimediaObject.getInfo();
AudioInfo audioInfo = multimediaInfo.getAudio();
// 判断是否为音频
if (Objects.isNull(audioInfo) || Objects.nonNull(multimediaInfo.getVideo())) {
return null;
}
response = new AudioInfoResponse();
response.setFormat(multimediaInfo.getFormat())
.setDuration(multimediaInfo.getDuration() / 1000)
.setSize(videoFile.length())
.setMd5(DigestUtils.md5DigestAsHex(Files.readAllBytes(videoFile.toPath())));
response.setSamplingRate(audioInfo.getSamplingRate())
.setBitRate(audioInfo.getBitRate())
.setChannels(audioInfo.getChannels())
.setBitDepth(audioInfo.getBitDepth());
return response;
} catch (Exception e) {
log.warn("Error processing audio file", e);
}
return response;
}
}
3.2 音视频文件信息参数
音视频文件公共信息参数
demo-ffmpeg-media/src/main/java/com/ljq/demo/springboot/ffmpeg/model/response/MediaInfoResponse.java
package com.ljq.demo.springboot.ffmpeg.model.response;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @Description: 多媒体信息
* @Author: junqiang.lu
* @Date: 2024/5/10
*/
@Data
@Accessors(chain = true)
public class MediaInfoResponse implements Serializable {
private static final long serialVersionUID = -326368230008457941L;
/**
* 文件格式
*/
private String format;
/**
* 时长,单位:秒
*/
private Long duration;
/**
* 文件大小,单位:字节数
*/
private Long size;
/**
* 文件md5值
*/
private String md5;
}
视频文件参数信息对象
demo-ffmpeg-media/src/main/java/com/ljq/demo/springboot/ffmpeg/model/response/VideoInfoResponse.java
package com.ljq.demo.springboot.ffmpeg.model.response;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* @Description: 视频文件信息返回对象
* @Author: junqiang.lu
* @Date: 2024/5/10
*/
@Data
@Accessors(chain = true)
@ToString(callSuper = true)
public class VideoInfoResponse extends MediaInfoResponse {
private static final long serialVersionUID = -9016123624628502571L;
/**
* 比特率,单位: bps
*/
private Integer bitRate;
/**
* 帧率,单位: FPS
*/
private Float frameRate;
/**
* 宽度,单位: px
*/
private Integer width;
/**
* 高度,单位: px
*/
private Integer height;
}
音频文件参数信息对象
demo-ffmpeg-media/src/main/java/com/ljq/demo/springboot/ffmpeg/model/response/AudioInfoResponse.java
package com.ljq.demo.springboot.ffmpeg.model.response;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* @Description: 音频文件信息返回对象
* @Author: junqiang.lu
* @Date: 2024/5/10
*/
@Data
@Accessors(chain = true)
@ToString(callSuper = true)
public class AudioInfoResponse extends MediaInfoResponse {
private static final long serialVersionUID = 3573655613715240188L;
/**
* 采样率
*/
private Integer samplingRate;
/**
* 音频通道数量,1-单声道,2-立体声
*/
private Integer channels;
/**
* 比特率,单位: bps
*/
private Integer bitRate;
/**
* 位深度
*/
private String bitDepth;
}
3.3 音视频文件上传Controller
demo-ffmpeg-media/src/main/java/com/ljq/demo/springboot/ffmpeg/controller/FFmpegMediaController.java
package com.ljq.demo.springboot.ffmpeg.controller;
import com.ljq.demo.springboot.ffmpeg.common.config.UploadConfig;
import com.ljq.demo.springboot.ffmpeg.common.util.FFmpegMediaUtil;
import com.ljq.demo.springboot.ffmpeg.model.response.AudioInfoResponse;
import com.ljq.demo.springboot.ffmpeg.model.response.VideoInfoResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
/**
* @Description: FFmpeg 媒体文件处理控制层
* @Author: junqiang.lu
* @Date: 2024/5/10
*/
@Slf4j
@RestController
@RequestMapping(value = "/api/ffmpeg/media")
public class FFmpegMediaController {
@Resource
private UploadConfig uploadConfig;
/**
* 视频上传
*
* @param file
* @return
* @throws IOException
*/
@PostMapping(value = "/upload/video", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<VideoInfoResponse> uploadVideo(MultipartFile file) throws IOException {
// 文件上传保存
String videoFilePath = uploadConfig.getUploadPath() + File.separator + file.getOriginalFilename();
log.info("videoFilePath: {}", videoFilePath);
File videoFile = new File(videoFilePath);
if (!videoFile.getParentFile().exists()) {
videoFile.getParentFile().mkdirs();
}
file.transferTo(videoFile);
// 获取视频信息
VideoInfoResponse videoInfoResponse = FFmpegMediaUtil.getVideoInfo(videoFilePath);
return ResponseEntity.ok(videoInfoResponse);
}
/**
* 音频上传
*
* @param file
* @return
* @throws IOException
*/
@PostMapping(value = "/upload/audio", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<AudioInfoResponse> uploadAudio(MultipartFile file) throws IOException {
// 文件上传保存
String audioFilePath = uploadConfig.getUploadPath() + File.separator + file.getOriginalFilename();
log.info("audioFilePath: {}", audioFilePath);
File audioFile = new File(audioFilePath);
if (!audioFile.getParentFile().exists()) {
audioFile.getParentFile().mkdirs();
}
file.transferTo(audioFile);
// 获取音频信息
AudioInfoResponse audioInfoResponse = FFmpegMediaUtil.getAudioInfo(audioFilePath);
return ResponseEntity.ok(audioInfoResponse);
}
}
3.4 application 配置文件
# config
server:
port: 9250
# spring
spring:
application:
name: demo-ffmpeg-media
servlet:
multipart:
max-file-size: 1000MB
max-request-size: 1000MB
# uploadConfig
upload:
path: D:\\upload # linux/macOS 路径 /opt/upload
4 测试数据
示例文件下载: https://zh.getsamplefiles.com
4.1 视频文件解析
测试文件:
https://zh.getsamplefiles.com/download/mp4/sample-4.mp4
测试结果:
{
"format": "mov",
"duration": 30,
"size": 7588608,
"md5": "d7b5155ee54ee9c7dcd8cbb5395823dc",
"bitRate": 2015000,
"frameRate": 25.0,
"width": 1280,
"height": 720
}
4.2 音频文件解析
测试文件:
https://zh.getsamplefiles.com/download/mp3/sample-5.mp3
测试结果:
{
"format": "mp3",
"duration": 45,
"size": 1830660,
"md5": "843e2916b1c552fb5e8ee3d83faddb8c",
"samplingRate": 44100,
"channels": 2,
"bitRate": 320000,
"bitDepth": "fltp"
}
5 注意事项
5.1 文件必须在本地
在实际项目中,一般会将文件保存到专门的文件服务器,但是 FFmpeg 解析文件必须是本地文件,因此在上传至文件服务器之前需要将文件在本地服务器做中转,解析完毕后再上传,然后删除本地文件。
6 推荐参考文档
Convert video to Another Format in Spring Boot(Java-based apps)
JAVE2 官方文档 Getting informations about a multimedia file
7 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo/tree/master/demo-ffmpeg-media
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.