业务场景:设备上传视频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,用户访问也是通过数据库给到封面,效率提高。 本人应届刚上班不久,有不足 支出各位大佬多多指点,谢谢。