最近在对之前写的一个 Spring Boot 的视频网站项目做功能完善,需要利用 FFmpeg 实现读取视频信息和自动截图的功能,查阅资料后发现网上这部分的内容非常少,于是就有了这篇文章。 视频网站项目地址 GitHub:https://github.com/PuZhiweizuishuai/PornTube 码云: https://gitee.com/puzhiweizuishuai/VideoWeb 本文将介绍如何利用Javacv实现在视频网站中常见的读取视频信息和自动获取封面图的功能。

最近在对之前写的一个 Spring Boot 的视频网站项目做功能完善,需要利用 FFmpeg 实现读取视频信息和自动截图的功能,查阅资料后发现网上这部分的内容非常少,于是就有了这篇文章。

视频网站项目地址 GitHub: https://github.com/PuZhiweizuishuai/PornTube
码云: https://gitee.com/puzhiweizuishuai/VideoWeb

本文将介绍如何利用Javacv实现在视频网站中常见的读取视频信息和自动获取封面图的功能。

javacv 介绍

javacv可以帮助我们在java中很方便的使用 OpenCV 以及 FFmpeg 相关的功能接口
项目地址: https://github.com/bytedeco/javacv

引入 javacv

        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacv.version}</version>
        </dependency>

读取视频信息

创建 VideoInfo 类

package com.buguagaoshu.porntube.vo;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
 * @author Pu Zhiwei {@literal puzhiweipuzhiwei@foxmail.com}
 * create          2022-06-06 19:15
@Getter
@Setter
public class VideoInfo {
     * 总帧数
    private int lengthInFrames;
    private double frameRate;
    private double duration;
     * 视频编码
    private String videoCode;
     * 音频编码
    private String audioCode;
    private int width;
    private int height;
    private int audioChannel;
    private String md5;
     * 音频采样率
    private Integer sampleRate;
    public String toJson() {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(this);
        } catch (Exception e) {
            return "";

使用 FFmpegFrameGrabber 读取视频信息

 public static VideoInfo getVideoInfo(File file) {
        VideoInfo videoInfo = new VideoInfo();
        FFmpegFrameGrabber grabber = null;
        try {
            grabber = new FFmpegFrameGrabber(file);
            // 启动 FFmpeg
            grabber.start();
            // 读取视频帧数
            videoInfo.setLengthInFrames(grabber.getLengthInVideoFrames());
			// 读取视频帧率
            videoInfo.setFrameRate(grabber.getVideoFrameRate());
            // 读取视频秒数
            videoInfo.setDuration(grabber.getLengthInTime() / 1000000.00);
            // 读取视频宽度
            videoInfo.setWidth(grabber.getImageWidth());
            // 读取视频高度
            videoInfo.setHeight(grabber.getImageHeight());
            videoInfo.setAudioChannel(grabber.getAudioChannels());
            videoInfo.setVideoCode(grabber.getVideoCodecName());
            videoInfo.setAudioCode(grabber.getAudioCodecName());
            // String md5 = MD5Util.getMD5ByInputStream(new FileInputStream(file));
            videoInfo.setSampleRate(grabber.getSampleRate());
            return videoInfo;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            try {
                if (grabber != null) {
                    // 此处代码非常重要,如果没有,可能造成 FFmpeg 无法关闭
                    grabber.stop();
                    grabber.release();
            } catch (FFmpegFrameGrabber.Exception e) {
                log.error("getVideoInfo grabber.release failed 获取文件信息失败:{}", e.getMessage());

读取信息没有什么难度,但是在对视频截图的过程中,出现了一些问题,在我查找截图实现的代码时,大多数的代码都是这么写的

* 获取视频缩略图 * @param filePath:视频路径 * @param mod:视频长度/mod获取第几帧 * @throws Exception public static String randomGrabberFFmpegImage(String filePath, int mod) { String targetFilePath = ""; FFmpegFrameGrabber ff = FFmpegFrameGrabber.createDefault(filePath); ff.start(); //图片位置是否正确 String rotate = ff.getVideoMetadata(ROTATE); //获取帧数 int ffLength = ff.getLengthInFrames(); Frame f; int i = 0; //设置截取帧数 int index = ffLength / mod; while (i < ffLength) { f = ff.grabImage(); if(i == index){ if (null != rotate && rotate.length() > 1) { OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage(); IplImage src = converter.convert(f); f = converter.convert(rotate(src, Integer.parseInt(rotate))); targetFilePath = getImagePath(filePath, i); doExecuteFrame(f, targetFilePath); break; ff.stop(); }catch (Exception e){ log.error("获取视频缩略图异常:" + e.getMessage()); return targetFilePath;

这样写本身没有什么问题,但是在获取需要截取帧数的部分,使用的是通过循环来一帧一帧的判断,这样在视频较短的时候没有什么问题,但是如果视频较长,就会出现严重的性能问题。

  while (i < ffLength) {
                f = ff.grabImage();
                if(i == index){
					......
                    break;

FFmpeg 的命令行参数有一个 -ss 的参数,使用 -ss 可以快速的帮助我们跳到视频的指定位置,完成操作,不用一帧一帧的判断。

所以现在的问题就是如何在 javacv 中实现 -ss 参数

我在 javacv 的 GitHub Issues 中发现了这个操作,即使用 setTimestamp() 方法,使用 setTimestamp() 方法可以使 FFmpeg 跳转到指定时间,完成截图,于是,最后的截图代码就变成了这样

* 随机获取视频截图 * @param videFile 视频文件 * @param count 输出截图数量 * @return 截图列表 public static List<FileTableEntity> randomGrabberFFmpegImage(File videFile, int count, long userId) { FFmpegFrameGrabber grabber = null; String path = FileTypeEnum.filePath(); try { List<FileTableEntity> images = new ArrayList<>(count); grabber = new FFmpegFrameGrabber(videFile); grabber.start(); // 获取视频总帧数 // int lengthInVideoFrames = grabber.getLengthInVideoFrames(); // 获取视频时长, / 1000000 将单位转换为秒 long delayedTime = grabber.getLengthInTime() / 1000000; Random random = new Random(); for (int i = 0; i < count; i++) { // 跳转到响应时间 grabber.setTimestamp((random.nextInt((int)delayedTime - 1) + 1) * 1000000L); Frame f = grabber.grabImage(); Java2DFrameConverter converter = new Java2DFrameConverter(); BufferedImage bi = converter.getBufferedImage(f); String imageName = FileTypeEnum.newFilename(SUFFIX); File out = Paths.get(path, imageName).toFile(); ImageIO.write(bi, "jpg", out); FileTableEntity fileTable = FileUtils.createFileTableEntity(imageName, SUFFIX, path, f.image.length, "系统生成截图", userId, FileTypeEnum.VIDEO_PHOTO.getCode()); images.add(fileTable); return images; } catch (Exception e) { return null; } finally { try { if (grabber != null) { grabber.stop(); grabber.release(); } catch (FFmpegFrameGrabber.Exception e) { log.error("getVideoInfo grabber.release failed 获取文件信息失败:{}", e.getMessage());

这样我们就能快速的实现截图了。

本文首发于 https://www.buguagaoshu.com/archives/shi-yong-javacv-shi-xian-du-qu-shi-pin-xin-xi-ji-zi-dong-jie-qu-feng-mian-tu

转载请注明来源