FrameTour-BE/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java

382 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.ycwl.basic.task;
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.DeviceMapper;
import com.ycwl.basic.mapper.FaceSampleMapper;
import com.ycwl.basic.mapper.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.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
import com.ycwl.basic.storage.enums.StorageType;
import com.ycwl.basic.utils.SnowFlakeUtil;
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 SourceMapper sourceMapper;
@Data
public static class Task {
public String type = "normal";
public Long deviceId;
public Long faceSampleId;
public Date createTime;
public Callback callback;
public Long memberId;
public static interface Callback {
void onInvoke();
}
}
@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 = 1000L)
public void doTask() {
Task task = queue.poll();
if (task == null) {
return;
}
log.info("poll task: {}", task);
if (!task.getType().equalsIgnoreCase("normal")) {
task.getCallback().onInvoke();
return;
}
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())
);
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("视频裁切成功");
IStorageAdapter adapter = StorageFactory.use("assets");
String url = adapter.uploadFile(outFile, "video-source", outFile.getName());
SourceEntity imgSource = sourceMapper.findBySampleId(faceSample.getId());
SourceEntity sourceEntity = new SourceEntity();
sourceEntity.setId(SnowFlakeUtil.getLongId());
if (imgSource != null) {
sourceEntity.setUrl(imgSource.getUrl());
sourceEntity.setPosJson(imgSource.getPosJson());
sourceEntity.setMemberId(imgSource.getMemberId());
}
sourceEntity.setVideoUrl(url);
sourceEntity.setFaceSampleId(faceSample.getId());
sourceEntity.setMemberId(task.getMemberId());
sourceEntity.setScenicId(faceSample.getScenicId());
sourceEntity.setDeviceId(faceSample.getDeviceId());
sourceEntity.setType(1);
sourceMapper.add(sourceEntity);
}
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;
}
}
}