定时获取FTP中视频的中间帧

77 阅读2分钟

业务场景:设备上传视频FTP服务器中,因为有需求获取视频的中间帧作为视频播放的封面,如果实时的进行获取的话效率 受带宽问题,也可以接受一定的延时,所以采取的定时获取的方式。 话不多说直接上代码: 1.导入相关的依赖包,由于是直接将整个解码器,所以依赖包会有点大

<!--add 用于avi 格式转 mp4-->
       <dependency>
          <groupId>ws.schild</groupId>
          <artifactId>jave-core</artifactId>
          <version>2.4.5</version>
       </dependency>
       <dependency>
          <groupId>ws.schild</groupId>
          <artifactId>jave-native-win64</artifactId>
          <version>2.4.5</version>
       </dependency>
<!--ffmpeg抽帧-->
       <dependency>
          <groupId>org.bytedeco</groupId>
          <artifactId>javacv</artifactId>
          <version>1.4.3</version>
       </dependency>
       <dependency>
          <groupId>org.bytedeco.javacpp-presets</groupId>
          <artifactId>ffmpeg-platform</artifactId>
          <version>4.0.2-1.4.3</version>
       </dependency>

首先创建FTP的连接工具,ip、账号、密码这些写到相关的配置文件中

@Slf4j
@Component
public class FTPConnectionUtils {
```
@Value("${ftpvideo.host}")
private  String ftpVideoHost;

@Value("${ftpvideo.username}")
private  String ftpVideoUser;

@Value("${ftpvideo.password}")
private String ftpVideoPassword;
```
  /**
     * VIDEO FTP连接
     * @return
     */
    public FTPClient getFtpVideoClient() {
        FTPClient ftpVideoClient = new FTPClient();
        try {
            ftpVideoClient.setControlEncoding("UTF-8");
            ftpVideoClient.connect(ftpVideoHost, 21);
            FTPClientConfig config = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
            ftpVideoClient.setCharset(Charset.forName("GBK"));
            ftpVideoClient.configure(config);
            //登录
            ftpVideoClient.login(ftpVideoUser, ftpVideoPassword);
            ftpVideoClient.setFileType(FTPClient.BINARY_FILE_TYPE);
            ftpVideoClient.enterLocalPassiveMode();
//            ftpVideoClient.enterRemotePassiveMode();
            ftpVideoClient.setConnectTimeout(3000);
        } catch (Exception e) {
            throw new JeecgBootException("FTP连接失败");
        }
        return ftpVideoClient;
    }
    ```
/**
 * 获取FTP指定目录的文件列表
 */
public FTPFile[] getFTPVideoFileList(String ftpPath) {
    //日志FTP服务器
    FTPClient ftpVideoClient = getFtpVideoClient();
    //查询ftp文件列表
    FTPFile[] listFile;
    try {
        //进入到指定目录
        if (!ftpVideoClient.changeWorkingDirectory(ftpPath)) {
            throw new JeecgBootException("目录不存在,请重新选择");
        }
        listFile = ftpVideoClient.listFiles();
        log.info(ftpVideoClient.getRemoteAddress()+"文件路径"+ftpPath);
        log.info("file length "+listFile.length);
        //读取ftp中的文件
        if (listFile == null || listFile.length == 0) {
            return new FTPFile[0];
        }
    } catch (IOException e) {
        log.error("获取日志文件列表失败");
        throw new RuntimeException(e);
    }finally {
        try {
            //判断ftp连接是否在传输
            if (ftpVideoClient.isConnected() && ftpVideoClient.isAvailable()) {
                ftpVideoClient.logout();
                ftpVideoClient.disconnect();
            }
        } catch (Exception e) {
            log.error("ftp服务器关闭失败");
        }
    }
    return listFile;
}

```
/**
 * 视频截取中间帧作为图片并且进行上传返回url
 * @param videoInputStream 视频流
 * @param bizPath FTP视频文件对应的上一级目录
 * @return
 */
public static String videoCutFrame(InputStream videoInputStream, String bizPath){
    InputStream inputStream = null;
    FFmpegFrameGrabber grabber = null;
    if (videoInputStream == null) {
        return null;
    }
    try {
    grabber = new FFmpegFrameGrabber(videoInputStream);
    grabber.start();
    //获取帧数
    int length = grabber.getLengthInFrames();
    //获取中间帧
    int i = length >> 1;
    //跳到指定帧中
    grabber.setFrameNumber(i);
    //捕获帧
    Frame frame = grabber.grabImage();
    if ( null != frame && frame.image != null){
        //将frame帧转成一张图流
        inputStream = writeToFile(frame);
    }
    grabber.stop();
    if (inputStream != null) {
        //将图片的内容读取到byte数组中
        String fileName = UUID.randomUUID().toString().replace("-", "")+"."+CommonConstant.IMAGE_PNG;
        String relativePath = CommonConstant.FTP_VIDEO_IMAGE+"/"+bizPath+"/"+fileName;
        return  OssBootUtil.upload(inputStream,relativePath);
    }
 } catch (Exception e) {
    log.error("[{}]文件流读取失败",bizPath);
}finally {
        if (grabber != null) {
            try {
                grabber.stop();
                grabber.close();
            } catch (FrameGrabber.Exception e) {
                log.error("文件流读取失败",e);
            }
        }
}

    return null;
}
```

开启定时任务在启动类中添加 @EnableScheduling @EnableAsync 注解 然后创建定时任务类

```
package org.jeecg.modules.buz.tasks;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.DateUtils;
import org.jeecg.modules.buz.entity.BuzAppconstructVideoImage;
import org.jeecg.modules.buz.service.IBuzAppconstructVideoImageService;
import org.jeecg.modules.system.util.FTPConnectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.io.*;
import java.nio.file.Files;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @Description:
 * @Author: HWJ
 * @Date: 2024年06月27日 18:10
 */
@Slf4j
@Component
public class VideoImageTask {

    private static Boolean status = true;

    @Value("${spring.profiles.active}")
    private String env;
    @Autowired
    private IBuzAppconstructVideoImageService videoImageService;

    @Autowired
    private FTPConnectionUtils ftpConnectionUtils;

    @Value("${ftpvideo.videoPath}")
    private String videoPath;

    //每三十秒执行一次
   @Scheduled(cron = "0/30 * * * * ?")
   @Async
   public void run(){
       if ("dev".equals(env)) {
           return;
       }
       if(!status){
           return ;
       }else{
           status = false;
       }
       log.info("VideoImageTask  datetime:" + DateUtils.getTimestamp());
       FTPClient ftpClient = ftpConnectionUtils.getFtpVideoClient();
        try {
            getVideoFile(videoPath,ftpClient);
        } catch (IOException e) {
            log.error("数据流异常");
        }finally {
            try {
                //关闭ftp连接
                if (ftpClient.isConnected()){
                    ftpClient.logout();
                    ftpClient.disconnect();
                }
            } catch (Exception e) {
                log.error("ftp logout fail");
            }finally {
                status = true;
            }
        }
    }
    //获取到文件返回文件地址
    private void getVideoFile(String filePath,FTPClient ftpClient) throws IOException {
        if (!ftpClient.changeWorkingDirectory(filePath)) {
            log.error(filePath + "目录不存在!");
            return;
        }
        //获取该目录的所有文件(文件+目录)
        FTPFile[] ftpFiles = ftpClient.listFiles();
        if (ftpFiles == null) {
            //目录没有文件---结束
            return;
        }
        for (FTPFile ftpFile : ftpFiles) {
            if (ftpFile.isDirectory()) {
                //文件夹递归
                getVideoFile(filePath + "/" + ftpFile.getName(),ftpClient);
            } else {
                String fileName = ftpFile.getName();
                //获取文件后缀
                String suffix = FileUtil.getSuffix(fileName);
                if ("avi".equalsIgnoreCase(suffix)){
                    //avi格式视频--处理
                    videoProcessing(filePath, fileName);
                }
            }
        }
    }

    /**
     * 视频处理
     * @param filePath
     * @param fileName
     */
    private void videoProcessing(String filePath, String fileName) {
        try {
            //参数校验
            if (StrUtil.isBlank(filePath) || StrUtil.isBlank(fileName)) {
                return;
            }
            //校验视频
            int count = videoImageService.count(new LambdaQueryWrapper<BuzAppconstructVideoImage>()
                    .eq(BuzAppconstructVideoImage::getVideoName, fileName));
            if (count > 0) {
                return;
            }
            //获取文件的视频流
            FTPClient ftpClient = ftpConnectionUtils.getFtpVideoClient();
            //设置数据传输过期时间
            ftpClient.setDataTimeout(Duration.ofMinutes(15));
            InputStream videoInputStream = new BOMInputStream(ftpClient.retrieveFileStream(filePath + "/" + fileName));
            String imageUrl = CommonUtils.videoCutFrame(videoInputStream, filePath);
            if (imageUrl != null){
                //保存文件对应的图片地址
                BuzAppconstructVideoImage videoImage =
                        BuzAppconstructVideoImage.builder()
                                .videoDir(filePath)
                                .videoName(fileName)
                                .videoMiddleFrame(imageUrl)
                                .build();
                //保存
                videoImageService.save(videoImage);
                //防止连接超时
                ftpClient.completePendingCommand();
            }
            videoInputStream.close();
        } catch (Exception e) {
            log.error(filePath + "/" + fileName+"获取文件流失败", e);
        }
    }
}

这样定时的获取到中间帧的图片,并且上传到oss,用户访问也是通过数据库给到封面,效率提高。 本人应届刚上班不久,有不足 支出各位大佬多多指点,谢谢。