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

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,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;
}
}
}