赞
踩
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 依赖来实现解析音视频文件的功能。
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 系统的依赖,可根据软件运行环境进行删减。
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; } }
音视频文件公共信息参数
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; }
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); } }
# 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
示例文件下载: https://zh.getsamplefiles.com
测试文件:
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
}
测试文件:
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"
}
在实际项目中,一般会将文件保存到专门的文件服务器,但是 FFmpeg 解析文件必须是本地文件,因此在上传至文件服务器之前需要将文件在本地服务器做中转,解析完毕后再上传,然后删除本地文件。
Convert video to Another Format in Spring Boot(Java-based apps)
JAVE2 官方文档 Getting informations about a multimedia file
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo/tree/master/demo-ffmpeg-media
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。