获取人脸对应视频流程,自动删除源视频流程,自动创建任务渲染流程,自动删除人脸数据逻辑

This commit is contained in:
2024-12-11 15:38:18 +08:00
parent ba4c339660
commit 8c81a994c8
47 changed files with 1318 additions and 222 deletions

View File

@ -0,0 +1,142 @@
package com.ycwl.basic.task;
import com.ycwl.basic.mapper.pc.DeviceMapper;
import com.ycwl.basic.mapper.pc.FaceMapper;
import com.ycwl.basic.mapper.pc.FaceSampleMapper;
import com.ycwl.basic.mapper.pc.ScenicMapper;
import com.ycwl.basic.mapper.pc.TemplateMapper;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.model.pc.template.entity.TemplateConfigEntity;
import com.ycwl.basic.model.pc.template.req.TemplateReqQuery;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.service.task.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Component
@EnableScheduling
@Slf4j
public class DynamicTaskGenerator {
@Autowired
private ScenicMapper scenicMapper;
@Autowired
private TemplateMapper templateMapper;
@Autowired
private FaceMapper faceMapper;
@Autowired
private TaskFaceService faceService;
@Autowired
private FaceSampleMapper faceSampleMapper;
@Autowired
private TaskService taskService;
@Autowired
private DeviceMapper deviceMapper;
@Scheduled(cron = "0 0 * * * ?")
public void dynamicTask() {
List<ScenicRespVO> scenicList = scenicMapper.list(new ScenicReqQuery());
for (ScenicRespVO scenic : scenicList) {
log.info("定时任务执行,当前景区:{}", scenic.getName());
ScenicConfigEntity scenicConfig = scenicMapper.getConfig(scenic.getId());
if (scenicConfig == null || scenicConfig.getBookRoutine() == 2) {
log.info("当前景区{},未启用提前预约流程", scenic.getName());
continue;
}
log.info("当前景区{},启用了提前预约流程", scenic.getName());
TemplateReqQuery templateQuery = new TemplateReqQuery();
templateQuery.setScenicId(scenic.getId());
List<TemplateRespVO> templateList = templateMapper.list(templateQuery);
for (TemplateRespVO template : templateList) {
log.info("当前景区{},启用了提前预约流程,模板:{}", scenic.getName(), template.getName());
if (template.getStatus() == 0) {
log.info("当前模板{}未启用,跳过", template.getName());
continue;
}
TemplateConfigEntity templateConfig = templateMapper.getConfig(template.getId());
if (templateConfig == null) {
log.info("当前模板{}未配置,跳过", template.getName());
continue;
}
if (templateConfig.getIsDefault() == 0) {
if (scenicConfig.getBookRoutine() == 1) {
log.info("当前模板{}未启用默认,且景区启用预约流程,跳过", template.getName());
continue;
}
}
Integer minimalPlaceholderFill = templateConfig.getMinimalPlaceholderFill();
List<String> placeholderList = new ArrayList<>();
if (minimalPlaceholderFill == null) {
minimalPlaceholderFill = 0;
}
List<TemplateRespVO> subTemplateList = templateMapper.getByPid(template.getId());
for (TemplateRespVO subTemplate : subTemplateList) {
if (subTemplate.getIsPlaceholder() == 1) {
placeholderList.add(subTemplate.getSourceUrl());
}
}
if (minimalPlaceholderFill == 0) {
for (TemplateRespVO subTemplate : subTemplateList) {
if (subTemplate.getIsPlaceholder() == 1) {
minimalPlaceholderFill += 1;
}
}
}
log.info("当前模板{}启用默认,最小占位素材:{}", template.getName(), minimalPlaceholderFill);
// 查找人脸样本
List<FaceRespVO> list = faceMapper.listByScenicIdAndNotFinished(scenic.getId());
for (FaceRespVO face : list) {
log.info("当前模板{}启用默认,人脸样本:{}", template.getName(), face.getFaceUrl());
if (((new Date()).getTime() - face.getCreateAt().getTime()) > scenicConfig.getMaxJourneyHour() * 3600 * 1000) {
log.info("当前人脸样本{}已超过最大游玩{}小时,自动检测人脸", face.getFaceUrl(), scenicConfig.getMaxJourneyHour());
List<String> oldMatchedSampleListIds = new ArrayList<>();
if (face.getMatchSampleIds() != null) {
oldMatchedSampleListIds = Arrays.asList(face.getMatchSampleIds().split(","));
}
SearchFaceRespVo searchFace = faceService.searchFace(scenic.getId(), face.getId());
if (oldMatchedSampleListIds.size() == searchFace.getSampleListIds().size()) {
boolean isEqual = true;
for (Long sampleId : searchFace.getSampleListIds()) {
if (!oldMatchedSampleListIds.contains(sampleId.toString())) {
isEqual = false;
break;
}
}
if (isEqual) {
log.info("当前人脸样本{}已超过最大游玩{}小时,但人脸检测结果与上次相同,跳过", face.getFaceUrl(), scenicConfig.getMaxJourneyHour());
continue;
}
}
List<FaceSampleRespVO> faceSampleList = faceSampleMapper.listByIds(searchFace.getSampleListIds());
int matchedPlaceholder = 0;
for (FaceSampleRespVO faceSample : faceSampleList) {
if (placeholderList.contains(faceSample.getDeviceId().toString())) {
matchedPlaceholder += 1;
}
}
if (matchedPlaceholder >= minimalPlaceholderFill) {
log.info("当前人脸样本{}已超过最小占位素材{},自动创建任务", face.getFaceUrl(), minimalPlaceholderFill);
taskService.createRenderTask(scenic.getId(), template.getId(), face.getId());
faceMapper.finishedJourney(face.getId());
} else {
log.info("当前人脸样本{}未超过最小占位素材{},未达到自动生成条件", face.getFaceUrl(), minimalPlaceholderFill);
}
}
}
}
}
}
}

View File

@ -0,0 +1,35 @@
package com.ycwl.basic.task;
import com.ycwl.basic.mapper.pc.FaceSampleMapper;
import com.ycwl.basic.mapper.pc.ScenicMapper;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.service.task.TaskFaceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@EnableScheduling
@Slf4j
public class FaceCleaner {
@Autowired
private ScenicMapper scenicMapper;
@Autowired
private TaskFaceService faceService;
@Scheduled(cron = "0 0 4 * * ?")
public void clean(){
ScenicReqQuery scenicQuery = new ScenicReqQuery();
List<ScenicRespVO> scenicList = scenicMapper.list(scenicQuery);
scenicList.forEach(scenic -> {
log.info("当前景区{},开始删除人脸样本", scenic.getName());
faceService.batchDeleteFace(scenic.getId());
});
}
}

View File

@ -1,176 +0,0 @@
//package com.ycwl.basic.task;
//
//
//import cn.hutool.core.util.StrUtil;
//import com.alibaba.fastjson.JSONArray;
//import com.alibaba.fastjson.JSONObject;
//import com.tu.common.redis.RedisUtils;
//import com.tu.common.utils.ChinaMobileUtil;
//import com.tu.common.utils.DateUtils;
//import com.tu.dao.TuSpaceManageDao;
//import com.tu.dto.GetLiveSteamUrlDTO;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.core.annotation.Order;
//import org.springframework.scheduling.annotation.EnableScheduling;
//import org.springframework.scheduling.annotation.Scheduled;
//import org.springframework.stereotype.Component;
//
//import javax.annotation.PostConstruct;
//import java.util.*;
//
//import static com.tu.common.utils.ChinaMobileUtil.getTokenBySend;
//import static com.tu.common.utils.ChinaMobileUtil.sendPost;
//
//@Slf4j
//@Component
//@EnableScheduling
//public class GetSpaceChinaMobileLiveSteamJob {
//
// @Autowired
// TuSpaceManageDao tuSpaceManageDao;
// @Autowired
// RedisUtils redisUtils;
// @Autowired
// ChinaMobileUtil chinaMobileUtil;
// @Value("${spring.profiles.active}")
// private String springProfile;
//
// //public static String token = null;
// private static Queue failMsgSubQueue = new LinkedList(); // 保存订阅失败的数据
//
// /**
// * 订阅消息
// */
// @PostConstruct
// public void msgSubscription() {
// new Thread(() -> {
// if (springProfile.equals("prod")) {
// List<GetLiveSteamUrlDTO> liveSteamUrl = tuSpaceManageDao.getLiveSteamUrl(null, 2);
// List<String> deviceIdList = new ArrayList<>();
// for (GetLiveSteamUrlDTO item : liveSteamUrl) {
// String deviceSn = item.getDeviceSn();
// if (deviceSn != null && !deviceSn.equals("")) {
// deviceIdList.add(deviceSn);
//
// if (deviceIdList.size() == 29) {
// // 30条数据为一组分开订阅
// String msg = doMsgSubscription(deviceIdList);
// if (StrUtil.isBlank(msg)) {
// // 消息订阅失败,过一会儿再试试
// List<String> failDeviceIdList = new ArrayList<>();
// Collections.copy(failDeviceIdList, deviceIdList);
// failMsgSubQueue.offer(failDeviceIdList);
// }
// deviceIdList = new ArrayList<>();
// }
//
// }
// }
// doMsgSubscription(deviceIdList);
// }
// }).start();
//
// }
//
//
// /**
// * 每小时扫描订阅失败的消息
// */
// @PostConstruct
// @Scheduled(cron = "0 0 * * * *")
// public void scanQueue() {
// if (failMsgSubQueue.size() > 0) {
// Object poll = failMsgSubQueue.poll();
// if (poll != null) {
// List<String> pollList = (List<String>) poll;
// String msg = doMsgSubscription(pollList);
// if (StrUtil.isBlank(msg)) {
// // 消息订阅失败,过一会儿再试试
// List<String> failDeviceIdList = new ArrayList<>();
// Collections.copy(failDeviceIdList, pollList);
// failMsgSubQueue.offer(failDeviceIdList);
// }
// }
// }
// }
//
//
// @PostConstruct
// @Scheduled(fixedRate = 1000 * 60 * 60 * 2)
// @Order(1)
// public void getToken() {
// if (springProfile.equals("prod")) {
// String tokenBySend = getTokenBySend();
// redisUtils.set("CHINA_MOBILE_TOKEN_KEY_NEW", tokenBySend, RedisUtils.HOUR_TOW_EXPIRE);
// }
// }
//
// @Scheduled(cron = "0 */19 * * * *")
// @PostConstruct
// @Order(2)
// public void setLiveSteam() {
// new Thread(() -> {
// if (springProfile.equals("prod")) {
// List<GetLiveSteamUrlDTO> liveSteamUrl = tuSpaceManageDao.getLiveSteamUrl(null, 2);
// for (GetLiveSteamUrlDTO item : liveSteamUrl) {
// String url = getLiveSteam(item.getDeviceSn());
// if (url != null && !url.equals("")) {
// tuSpaceManageDao.updateLiveSteamUrl(item.getId(), url);
// }
// }
// }
// }).start();
// }
//
// public String getLiveSteam(String deviceId) {
// log.info("【移动】开始获取视频流" + deviceId);
// String url_hls = "https://open.andmu.cn/v3/open/api/device/hls";
// JSONObject bodyParam = new JSONObject();
// bodyParam.put("deviceId", deviceId);
// bodyParam.put("endTime", DateUtils.addDateDays(new Date(), 2).getTime());
// log.info("【移动】bodyParam->{}", bodyParam);
// String res = sendPost(url_hls, bodyParam, chinaMobileUtil.getTempToken());
// log.info("【移动】开始获取视频流结果:-》{}" + res);
// JSONObject jsonObject = JSONObject.parseObject(res);
// String m3u8Url = null;
// if (jsonObject.get("resultCode").toString().equals("000000")) {
// String data = jsonObject.get("data").toString();
// JSONObject jsonData = JSONObject.parseObject(data);
// m3u8Url = jsonData.get("m3u8Url").toString();
// }
// // token已过期
// if (jsonObject.get("resultCode").toString().equals("11504")) {
// // 删除redis的token会重新获取
// redisUtils.delete("CHINA_MOBILE_TOKEN_KEY_NEW");
// getLiveSteam(deviceId);
// }
// return m3u8Url;
// }
//
//
// public static void main(String[] args) {
//
// //List<String> deviceIdList =new ArrayList<>(30);
// //deviceIdList.add("2");
// //deviceIdList.add("1");
// //
// //List<String> failDeviceIdList = new ArrayList<>(deviceIdList);
// //failMsgSubQueue.offer(failDeviceIdList);
// //deviceIdList = new ArrayList<>();
// //List<String> pollList = ( List<String>) failMsgSubQueue.poll();
// //for (String s : pollList) {
// // System.out.println(s);
// //}
// //System.out.println(failMsgSubQueue.poll());
// //System.out.println(failMsgSubQueue.size());
//
//
// //String s = doMsgSubscription(Arrays.asList("040312e7fc9f", "743fc2dae410"));
// //System.out.println(s);
// }
//
//
//
//}

View File

@ -0,0 +1,49 @@
package com.ycwl.basic.task;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.mapper.pc.DeviceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
@EnableScheduling
@Slf4j
public class VideoPieceCleaner {
@Autowired
private DeviceMapper deviceMapper;
@Scheduled(cron = "0 0 0 * * ?")
public void clean() {
log.info("开始删除视频文件");
List<DeviceRespVO> deviceList = deviceMapper.list(new DeviceReqQuery());
for (DeviceRespVO device : deviceList) {
DeviceConfigEntity config = deviceMapper.getConfigByDeviceId(device.getId());
if (config == null) {
continue;
}
if (config.getStoreExpireDay() == null) {
continue;
}
if (config.getStoreExpireDay() <= 0) {
continue;
}
IDeviceStorageOperator storageOperator = DeviceFactory.getDeviceStorageOperator(null, config);
if (storageOperator == null) {
continue;
}
storageOperator.removeFilesBeforeDate(new Date(System.currentTimeMillis() - config.getStoreExpireDay() * 24 * 60 * 60 * 1000));
log.info("删除视频文件完成");
}
}
}

View File

@ -0,0 +1,370 @@
package com.ycwl.basic.task;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.mapper.pc.DeviceMapper;
import com.ycwl.basic.mapper.pc.FaceSampleMapper;
import com.ycwl.basic.mapper.pc.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.utils.OssUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Component
@EnableScheduling
@Slf4j
public class VideoPieceGetter {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private OssUtil ossUtil;
@Autowired
private SourceMapper sourceMapper;
@Data
public static class Task {
public Long deviceId;
public Long faceSampleId;
public Date createTime;
}
@Data
public static class FfmpegTask {
List<FileObject> fileList;
BigDecimal duration;
BigDecimal offsetStart;
String outputFile;
}
public static LinkedBlockingQueue<Task> queue = new LinkedBlockingQueue<>();
public static void addTask(Task task) {
queue.add(task);
}
@Scheduled(fixedDelay = 5000L)
public void doTask() {
Task task = queue.poll();
if (task == null) {
return;
}
log.info("poll task: {}", task);
FaceSampleRespVO faceSample = faceSampleMapper.getById(task.getFaceSampleId());
DeviceEntity device = deviceMapper.getByDeviceId(task.getDeviceId());
DeviceConfigEntity config = deviceMapper.getConfigByDeviceId(task.getDeviceId());
BigDecimal cutPre = BigDecimal.valueOf(5L);
BigDecimal cutPost = BigDecimal.valueOf(4L);
if (config == null) {
return;
}
// 有配置
if (config.getCutPre() != null) {
cutPre = config.getCutPre();
}
if (config.getCutPost() != null) {
cutPost = config.getCutPost();
}
IDeviceStorageOperator pieceGetter = DeviceFactory.getDeviceStorageOperator(device, config);
if (pieceGetter == null) {
return;
}
BigDecimal duration = cutPre.add(cutPost);
List<FileObject> listByDtRange = pieceGetter.getFileListByDtRange(
new Date(task.getCreateTime().getTime() - cutPre.multiply(BigDecimal.valueOf(1000)).longValue()),
new Date(task.getCreateTime().getTime() + cutPost.multiply(BigDecimal.valueOf(1000)).longValue())
);
if (listByDtRange.isEmpty()) {
queue.add(task);
return;
}
long offset = task.getCreateTime().getTime() - listByDtRange.get(0).getCreateTime().getTime();
FfmpegTask ffmpegTask = new FfmpegTask();
ffmpegTask.setFileList(listByDtRange);
ffmpegTask.setDuration(duration);
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
File outFile = new File(faceSample.getDeviceId().toString() + "_" + faceSample.getId() + ".mp4");
ffmpegTask.setOutputFile(outFile.getAbsolutePath());
boolean result = startFfmpegTask(ffmpegTask);
if (!result) {
log.warn("视频裁切失败");
return;
}
log.info("视频裁切成功");
try {
InputStream inputStream = new FileInputStream(outFile);
String url = ossUtil.uploadFile(inputStream, "user-video-source", outFile.getName());
SourceEntity sourceEntity = new SourceEntity();
sourceEntity.setVideoUrl(url);
sourceEntity.setFaceSampleId(faceSample.getId());
sourceEntity.setScenicId(faceSample.getScenicId());
sourceEntity.setDeviceId(faceSample.getDeviceId());
sourceEntity.setType(1);
sourceMapper.add(sourceEntity);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public boolean startFfmpegTask(FfmpegTask task) {
boolean result;
if (task.getFileList().size() == 1) {
// 单个文件切割,用简单方法
result = runFfmpegForSingleFile(task);
} else {
// 多个文件切割,用速度快的
result = runFfmpegForMultipleFile1(task);
}
// 先尝试方法1
if (result) {
return true;
}
log.warn("FFMPEG简易方法失败尝试复杂方法转码");
// 不行再尝试方法二
return runFfmpegForMultipleFile2(task);
}
private boolean runFfmpegForMultipleFile1(FfmpegTask task) {
// 多文件方法一先转换成ts然后合并切割
// 步骤一先转换成ts并行转换
boolean notOk = task.getFileList().stream().map(file -> {
try {
if (file.isNeedDownload() || (!file.getName().endsWith(".ts"))) {
String tmpFile = file.getName() + ".ts";
boolean result = convertMp4ToTs(file, tmpFile);
// 因为是并行转换,没法保证顺序,就直接存里面
if (result) {
file.setUrl(tmpFile);
} else {
// 失败了,务必删除临时文件
(new File(tmpFile)).delete();
}
return result;
} else {
return true;
}
} catch (IOException e) {
log.warn("转码出错");
return false;
}
}).anyMatch(b -> !b);
// 转码进程中出现问题
if (notOk) {
return false;
}
// 步骤二使用concat协议拼接裁切
boolean result;
try {
result = quickVideoCut(
"concat:" + task.getFileList().stream().map(FileObject::getUrl).collect(Collectors.joining("|")),
task.getOffsetStart(), task.getDuration(), task.getOutputFile()
);
} catch (IOException e) {
return false;
}
// 步骤三:删除临时文件
task.getFileList().stream().map(FileObject::getUrl).forEach(tmpFile -> {
File f = new File(tmpFile);
if (f.exists() && f.isFile()) {
f.delete();
}
});
return result;
}
private boolean runFfmpegForMultipleFile2(FfmpegTask task) {
// 多文件,方法二:使用计算资源编码
try {
return slowVideoCut(task.getFileList(), task.getOffsetStart(), task.getDuration(), task.getOutputFile());
} catch (IOException e) {
return false;
}
}
private boolean runFfmpegForSingleFile(FfmpegTask task) {
try {
return quickVideoCut(task.getFileList().get(0).getUrl(), task.getOffsetStart(), task.getDuration(), task.getOutputFile());
} catch (IOException e) {
return false;
}
}
/**
* 把MP4转换成可以拼接的TS文件
*
* @param file MP4文件或ffmpeg支持的输入
* @param outFileName 输出文件路径
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean convertMp4ToTs(FileObject file, String outFileName) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-i");
ffmpegCmd.add(file.getUrl());
ffmpegCmd.add("-c");
ffmpegCmd.add("copy");
ffmpegCmd.add("-bsf:v");
ffmpegCmd.add("h264_mp4toannexb");
ffmpegCmd.add("-f");
ffmpegCmd.add("mpegts");
ffmpegCmd.add(outFileName);
return handleFfmpegProcess(ffmpegCmd);
}
private boolean convertHevcToTs(FileObject file, String outFileName) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-i");
ffmpegCmd.add(file.getUrl());
ffmpegCmd.add("-c");
ffmpegCmd.add("copy");
ffmpegCmd.add("-bsf:v");
ffmpegCmd.add("hevc_mp4toannexb");
ffmpegCmd.add("-f");
ffmpegCmd.add("mpegts");
ffmpegCmd.add(outFileName);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 快速切割不产生转码速度快但可能会出现第一帧数据不是I帧导致前面的数据无法使用
*
* @param inputFile 输入文件ffmpeg支持的协议均可
* @param offset 离输入文件开始的偏移
* @param length 输出文件时长
* @param outputFile 输出文件名称
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean quickVideoCut(String inputFile, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-i");
ffmpegCmd.add(inputFile);
ffmpegCmd.add("-c:v");
ffmpegCmd.add("copy");
ffmpegCmd.add("-an");
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 转码切割,兜底逻辑,速度慢,但优势:成功后转码视频绝对可用
*
* @param inputFiles 输入文件Listffmpeg支持的协议均可
* @param offset 离输入文件开始的偏移
* @param length 输出文件时长
* @param outputFile 输出文件名称
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean slowVideoCut(List<FileObject> inputFiles, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
for (FileObject file : inputFiles) {
ffmpegCmd.add("-i");
ffmpegCmd.add(file.getUrl());
}
// 使用filter_complex做拼接
ffmpegCmd.add("-filter_complex");
ffmpegCmd.add(
IntStream.range(0, inputFiles.size()).mapToObj(i -> "[" + i + ":v]").collect(Collectors.joining("")) +
"concat=n=2:v=1[v]"
);
ffmpegCmd.add("-map");
ffmpegCmd.add("[v]");
ffmpegCmd.add("-preset:v");
ffmpegCmd.add("fast");
ffmpegCmd.add("-an");
// 没有使用copy因为使用了filter_complex
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 运行ffmpeg并确认ffmpeg是否正常退出
*
* @param ffmpegCmd ffmpeg命令
* @return 是否正常退出
*/
private static boolean handleFfmpegProcess(List<String> ffmpegCmd) throws IOException {
Date _startDt = new Date();
log.info("FFMPEG执行命令【{}】", String.join(" ", ffmpegCmd));
ProcessBuilder pb = new ProcessBuilder(ffmpegCmd);
Process ffmpegProcess = pb.start();
// 如果需要额外分析输出之类
if (log.isTraceEnabled()) {
InputStream stderr = ffmpegProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr));
String line;
while ((line = reader.readLine()) != null) {
log.trace(line);
}
}
try {
// 最长1分钟
boolean exited = ffmpegProcess.waitFor(1, TimeUnit.MINUTES);
if (exited) {
int code = ffmpegProcess.exitValue();
Date _endDt = new Date();
log.info("FFMPEG执行命令结束Code【{}】,耗费时间:【{}ms】命令【{}】", code, _endDt.getTime() - _startDt.getTime(), String.join(" ", ffmpegCmd));
return 0 == code;
} else {
log.error("FFMPEG执行命令没有在1分钟内退出命令【{}】", String.join(" ", ffmpegCmd));
ffmpegProcess.destroy();
return false;
}
} catch (InterruptedException e) {
// TODO: 被中断了
log.warn("FFMPEG执行命令【{}】,被中断了", String.join(" ", ffmpegCmd));
return false;
}
}
}