使用ffmpeg剪辑视频【删除视频头部,尾部,中间,视频拼接,获取视频指定时间截图】
引入pom
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.5</version>
</dependency>
相关java实现 【部分私有类未添加,时间格式转换类未添加,不影响主题逻辑】
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bytedeco.ffmpeg.ffmpeg;
import org.bytedeco.javacpp.Loader;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 类描述:视频剪辑工具类
* @date 2022-09-29 14:27:41
* @link https://zhuanlan.zhihu.com/p/145312133 参考文档
* @link https://ffmpeg.org/ffmpeg.html#Video-Options 参考文档【官网参数解释】
*/
public class VideoUtils {
private static final Logger logger = LogManager.getLogger(VideoUtils.class);
/**
* builder ffmpeg构建封装
*/
private ProcessBuilder builder;
/**
* 源文件路径
*/
private String originFilePath;
/**
* 源文件后缀
*/
private String suffix;
/**
* 文件点
*/
public static final String SPOT = ".";
/**
* 文件点
*/
public static final String TEMP_FOLDER = Constants.RESOURCE + File.separator + "temp" + File.separator;
/**
* 获取时间
*/
public static final Pattern TIME_PATTERN = Pattern.compile("^\\s*Duration: (\\d\\d):(\\d\\d):(\\d\\d)\\.(\\d).*$", Pattern.CASE_INSENSITIVE);
/**
* 方法描述:获取指定视频指定秒数截图
* @param path 指定视频路径
* @param shotTime 指定截图时间
* @return {@link String} 图片地址
* @throws
* @date 2022-09-30 17:43:28
*/
public static File screenshot(String path, int shotTime) {
File tempFile = FileUtil.createNewFile(TEMP_FOLDER + UUIDUtil.generateUUID() + SPOT + "png");
try {
new ProcessBuilder(Loader.load(ffmpeg.class), "-y", "-i", path, "-ss", String.valueOf(shotTime), "-vframes", "1", tempFile.getAbsolutePath()).start().waitFor();
} catch (Exception e) {
logger.info("获取指定视频指定秒数截图异常, path:{}, shotTime:{}", path, shotTime);
}
return tempFile;
}
/**
* 方法描述:获取视频播放时长
* @param path 视频路径
* @return {@link int} 返回播放时长
* @throws
* @date 2022-09-30 17:41:53
*/
public static int getVideoplayTimes(String path) {
try {
InputStream errorStream = new ProcessBuilder(Loader.load(ffmpeg.class), "-y", "-i", path).start().getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream));
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = TIME_PATTERN.matcher(line);
if (matcher.matches()) {
int hours = Integer.parseInt(matcher.group(1));
int minutes = Integer.parseInt(matcher.group(2));
int seconds = Integer.parseInt(matcher.group(3));
return hours * 60 * 60 + minutes * 60 + seconds;
}
}
} catch (Exception e) {
logger.error("获取视频时长异常, path:{}", path, e);
}
return 0;
}
/**
* 方法描述:剪切视频头,删除视频头
* @param cutTime 开始剪切视频的秒数 格式:00:03:00 表示从 3 分钟开始截取(也就是去除 3 分钟的开头)
* @return {@link VideoUtils}
* @date 2022-09-29 14:35:09
*/
public VideoUtils delHeadFrom(String cutTime) {
builder.command().add("-ss");
builder.command().add(cutTime);
return this;
}
/**
* 方法描述:剪切视频头,删除视频头
* @param cutTime 开始剪切视频的秒数
* @return {@link VideoUtils}
* @date 2022-09-29 14:35:09
*/
public VideoUtils delHeadFrom(int cutTime) {
builder.command().add("-ss");
builder.command().add(String.valueOf(cutTime));
return this;
}
/**
* 方法描述:剪切视频头尾,删除视频尾
* @param cutEndTime 剪切视频的秒数
* @return {@link VideoUtils}
* @date 2022-09-29 14:35:09
*/
public VideoUtils delEndFrom(int cutEndTime) {
saveTime(cutEndTime);
return this;
}
/**
* 方法描述:剪切视频头尾,删除视频尾
* @param cutEndTime 剪切视频的秒数 格式:00:03:00 表示从截取到3 分钟开始(也就是去除 3 分钟的之后视频)
* @return {@link VideoUtils}
* @date 2022-09-29 14:35:09
*/
public VideoUtils delEndFrom(String cutEndTime) {
saveTime(convertTime2Second(cutEndTime));
return this;
}
/**
* 方法描述:删除中间视频片段
* @param delFromTime 开始剪切视频删除的秒数 格式:00:03:00 表示从 3 分钟开始截取删除
* @param delEndTime 终止剪切视频删除的秒数 格式:00:05:00 表示从 5 分钟结束
* @return {@link VideoUtils}
* @date 2022-09-29 14:35:09
*/
public boolean delMidFrom(String delFromTime, String delEndTime, String savePath) {
File videoToStart = null, videoToEnd = null;
try {
if (!(DateUtil.isTimeStr(delFromTime, DateUtil.format7) && DateUtil.isTimeStr(delEndTime, DateUtil.format7))) {
throw new IllegalArgumentException("传入时间格式错误, delFromTime: " + delFromTime + "\t delEndTime: " + delEndTime);
}
videoToStart = FileUtil.createNewFile(TEMP_FOLDER + UUIDUtil.generateUUID() + SPOT + this.suffix);
videoToEnd = FileUtil.createNewFile(TEMP_FOLDER + UUIDUtil.generateUUID() + SPOT + this.suffix);
//首先保存片头 片尾
create(originFilePath).saveTime(convertTime2Second(delFromTime)).outFile(videoToStart.getAbsolutePath()).start().waitFor();
create(originFilePath).delHeadFrom(delEndTime).outFile(videoToEnd.getAbsolutePath()).start().waitFor();
//合并片头片尾
return create(videoToStart.getAbsolutePath()).mergeVideos(videoToEnd.getAbsolutePath()).outFile(savePath).start().waitFor() == 0;
} catch (Exception e) {
logger.error("合并视频失败 delFromTime:{}, delEndTime: {} savePath: {} ", delFromTime, delEndTime, savePath, e);
} finally {
if (videoToStart != null) {
videoToStart.deleteOnExit();
}
if (videoToEnd != null) {
videoToEnd.deleteOnExit();
}
}
return false;
}
/**
* 方法描述:删除中间视频片段
* @param delFromTime 开始剪切视频删除的秒数 格式:00:03:00 表示从 3 分钟开始截取删除
* @param delEndTime 终止剪切视频删除的秒数 格式:00:05:00 表示从 5 分钟结束
* @return {@link VideoUtils}
* @date 2022-09-29 14:35:09
*/
public boolean delMidFrom(int delFromTime, int delEndTime, String savePath) {
File videoToStart = null, videoToEnd = null;
try {
videoToStart = FileUtil.createNewFile(TEMP_FOLDER + UUIDUtil.generateUUID() + SPOT + this.suffix);
videoToEnd = FileUtil.createNewFile(TEMP_FOLDER + UUIDUtil.generateUUID() + SPOT + this.suffix);
//首先保存片头 片尾
create(originFilePath).saveTime(delFromTime).outFile(videoToStart.getAbsolutePath()).start().waitFor();
create(originFilePath).delHeadFrom(delEndTime).outFile(videoToEnd.getAbsolutePath()).start().waitFor();
//合并片头片尾
return create(videoToStart.getAbsolutePath()).mergeVideos(videoToEnd.getAbsolutePath()).outFile(savePath).start().waitFor() == 0;
} catch (Exception e) {
logger.error("合并视频失败 delFromTime:{}, delEndTime: {} savePath: {} ", delFromTime, delEndTime, savePath, e);
return false;
} finally {
if (videoToStart != null) {
videoToStart.deleteOnExit();
}
if (videoToEnd != null) {
videoToEnd.deleteOnExit();
}
}
}
public VideoUtils mergeVideos(String... originFiles) {
for (String filePath : originFiles) {
builder.command().add("-i");
builder.command().add(filePath);
}
builder.command().add("-filter_complex");
StringBuffer sb = new StringBuffer("[0:v:0][0:a:0]");
for (int i = 0; i < originFiles.length; i++) {
sb.append("[" + (i + 1) + ":v:0][" + (i + 1) + ":a:0]");
}
sb.append("concat=n=" + (originFiles.length + 1) + ":v=1:a=1[outv][outa]");
builder.command().add(sb.toString());
builder.command().add("-map");
builder.command().add("[outv]");
builder.command().add("-map");
builder.command().add("[outa]");
return this;
}
/**
* 方法描述:设置保存时长
* @param saveTime 保存时长 单位 s 从截取位置开始保存指定时长
* @return {@link VideoUtils}
* @date 2022-09-29 14:41:23
*/
public VideoUtils saveTime(int saveTime) {
if (saveTime < 1) {
throw new IllegalArgumentException("保存时长不能少于一秒");
}
builder.command().add("-t");
builder.command().add(String.valueOf(saveTime));
return this;
}
/**
* 方法描述:文件输出路径设置
* @param filePath 文件输出路径
* @return {@link VideoUtils} 视频工具类
* @date 2022-09-29 15:17:17
*/
public VideoUtils outFile(String filePath) {
if (StringUtils.isBlank(filePath)) {
throw new IllegalArgumentException("输出文件路径不存");
}
builder.command().add(filePath);
return this;
}
/**
* 方法描述:获取指定格式时间串的秒数
* @param time 时间串 格式为:00:00:00
* @return {@link int} 秒数
* @date 2022-09-29 16:05:04
*/
private int convertTime2Second(String time) {
if (!DateUtil.isTimeStr(time, DateUtil.format7)) {
throw new IllegalArgumentException("传入时间格式错误, time: " + time);
}
return LocalTime.parse(time, DateTimeFormatter.ofPattern(DateUtil.format7)).toSecondOfDay();
}
/**
* 方法描述:根据文件路径获取文件后缀
* @param filePath 文件路径
* @return {@link String} 文件后缀 不带点“.”
* @date 2022-09-29 15:43:19
*/
public static String getFileSuffix(String filePath) {
int lastSeparator = Math.max(filePath.lastIndexOf("\\"), filePath.lastIndexOf("/"));
int lastPoint = filePath.lastIndexOf(SPOT);
if (lastPoint < 1 || lastPoint < lastSeparator || filePath.length() <= lastPoint) {
throw new IllegalArgumentException("文件路径检测失败, filePath: " + filePath);
}
return filePath.substring(lastPoint + 1);
}
public Process start() {
try {
return builder.start();
} catch (IOException e) {
logger.error("处理视频失败,builder:{}", JSONObject.toJSONString(builder.command()), e);
}
return null;
}
/**
* 方法描述:处理视频构造类
* @param path 源视频路径
* @return {@link ProcessBuilder} 处理类
* @date 2022-09-29 14:25:04
*/
public static VideoUtils create(String path) {
return new VideoUtils()
.setOriginFilePath(path)
.setSuffix(getFileSuffix(path))
.setBuilder(new ProcessBuilder(Loader.load(org.bytedeco.ffmpeg.ffmpeg.class), "-y", "-i", path));
}
public ProcessBuilder getBuilder() {
return builder;
}
private VideoUtils setBuilder(ProcessBuilder builder) {
this.builder = builder;
return this;
}
public String getOriginFilePath() {
return originFilePath;
}
private VideoUtils setOriginFilePath(String originFilePath) {
this.originFilePath = originFilePath;
return this;
}
public String getSuffix() {
return suffix;
}
public VideoUtils setSuffix(String suffix) {
this.suffix = suffix;
return this;
}
}