You've already forked FrameTour-BE
获取人脸对应视频流程,自动删除源视频流程,自动创建任务渲染流程,自动删除人脸数据逻辑
This commit is contained in:
142
src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java
Normal file
142
src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
src/main/java/com/ycwl/basic/task/FaceCleaner.java
Normal file
35
src/main/java/com/ycwl/basic/task/FaceCleaner.java
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
//}
|
49
src/main/java/com/ycwl/basic/task/VideoPieceCleaner.java
Normal file
49
src/main/java/com/ycwl/basic/task/VideoPieceCleaner.java
Normal 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("删除视频文件完成");
|
||||
}
|
||||
}
|
||||
}
|
370
src/main/java/com/ycwl/basic/task/VideoPieceGetter.java
Normal file
370
src/main/java/com/ycwl/basic/task/VideoPieceGetter.java
Normal 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 输入文件List,ffmpeg支持的协议均可
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user