382 lines
14 KiB
Java
382 lines
14 KiB
Java
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 输入文件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;
|
||
}
|
||
}
|
||
|
||
}
|