使用ffmpeg剪辑视频【删除视频头部,尾部,中间,视频拼接,获取视频指定时间截图】

使用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;
    }
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值