parent
265d679da6
commit
fa99052078
|
|
@ -1,60 +1,60 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nursing-unit-services</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nursing-unit-services</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>nu-services-biz</artifactId>
|
||||
<artifactId>nu-services-biz</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nu-services-local-api</artifactId>
|
||||
<version>${nursingunit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>hibernate-re</artifactId>
|
||||
</dependency>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nu-services-local-api</artifactId>
|
||||
<version>${nursingunit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
<artifactId>hibernate-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>hibernate-re</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 企业微信/钉钉 api -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
<artifactId>weixin4j</artifactId>
|
||||
</dependency>
|
||||
<!-- 企业微信/钉钉 api -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
<artifactId>weixin4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 添加汉字转拼音依赖-->
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
<!-- 添加汉字转拼音依赖-->
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId>
|
||||
<version>2.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- COMMON 通用工具模块 -->
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nursing-unit-common</artifactId>
|
||||
<version>${nursingunit.version}</version>
|
||||
</dependency>
|
||||
<!-- COMMON 通用工具模块 -->
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nursing-unit-common</artifactId>
|
||||
<version>${nursingunit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nu-system-local-api</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nu-admin-local-api</artifactId>
|
||||
<version>${nursingunit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nu-admin-local-api</artifactId>
|
||||
<version>${nursingunit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.nursingunit.boot</groupId>
|
||||
<artifactId>nu-admin-biz</artifactId>
|
||||
|
|
@ -73,6 +73,35 @@
|
|||
<version>2.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 视频工具 -->
|
||||
<!-- 1. JavaCV 核心 (排除 javacpp) -->
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>javacv</artifactId>
|
||||
<version>1.5.9</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>javacpp</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 2. FFmpeg API (对应版本) -->
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>ffmpeg</artifactId>
|
||||
<version>6.0-1.5.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 3. Linux 平台的原生库 -->
|
||||
<dependency>
|
||||
<groupId>org.bytedeco</groupId>
|
||||
<artifactId>ffmpeg</artifactId>
|
||||
<version>6.0-1.5.9</version>
|
||||
<classifier>linux-x86_64</classifier>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package com.nu.modules.biz.order.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import lombok.Data;
|
||||
|
|
@ -11,6 +8,7 @@ import lombok.EqualsAndHashCode;
|
|||
import lombok.experimental.Accessors;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
|
@ -212,6 +210,8 @@ public class DirectiveOrder implements Serializable {
|
|||
/**
|
||||
* tplink监控视频上传任务ID
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.IGNORED)
|
||||
@Column(nullable = true, updatable = true)
|
||||
private String tplinkTaskId;
|
||||
/**
|
||||
* tplink切片crc64
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.nu.modules.biz.order.job;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.nu.entity.CosFileInfoEntity;
|
||||
import com.nu.entity.DirectiveOrderEntity;
|
||||
import com.nu.modules.biz.order.entity.DirectiveOrder;
|
||||
import com.nu.modules.biz.order.service.IDirectiveOrderJobService;
|
||||
|
|
@ -9,7 +8,6 @@ import com.nu.modules.sysconfig.ISysConfigApi;
|
|||
import com.nu.modules.tplink.camera.service.ICameraInfoJobService;
|
||||
import com.nu.utils.CosFileUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.Lists;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||
import org.jeecg.common.util.DateUtils;
|
||||
|
|
@ -18,7 +16,6 @@ import org.quartz.JobExecutionContext;
|
|||
import org.quartz.JobExecutionException;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
|
@ -65,10 +62,28 @@ public class DirectiveOrderEndTplinkJob implements Job {
|
|||
{
|
||||
List<DirectiveOrder> directiveOrderList = directiveOrderService.getUploadingTplink();
|
||||
directiveOrderList.forEach(order -> {
|
||||
Map<String, Object> result = tplinkService.getUploadToServerProcess(order.getTplinkTaskId());
|
||||
System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
|
||||
System.out.println(result.toString());
|
||||
System.out.println("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑");
|
||||
try {
|
||||
Map<String, Object> result = tplinkService.getUploadToServerProcess(order.getTplinkTaskId());
|
||||
System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
|
||||
System.out.println(result.toString());
|
||||
System.out.println("↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑");
|
||||
|
||||
if("success".equals(result.get("result")) ){
|
||||
if("1000".equals(result.get("process"))){
|
||||
order.setTplinkParams("成功");
|
||||
order.setOrderEndTime(new Date());
|
||||
order.setTplinkTaskId(null);
|
||||
directiveOrderService.updateById(order);
|
||||
}
|
||||
}else{
|
||||
order.setTplinkParams(result.toString());
|
||||
order.setOrderEndTime(new Date());
|
||||
order.setTplinkTaskId(null);
|
||||
directiveOrderService.updateById(order);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
// //任务一:COS上传进度查询
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
package com.nu.modules.biz.order.job;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.ffmpeg.ffmpeg;
|
||||
import org.bytedeco.javacpp.Loader;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 测试mp4视频切割与拼接
|
||||
* 策略:完全基于时间轴计算,不依赖视频元数据
|
||||
*/
|
||||
@Slf4j
|
||||
public class Mp4TestJob implements Job {
|
||||
|
||||
// ================= 常量配置 =================
|
||||
// 宿主机映射到容器内的路径
|
||||
private static final String BASE_PATH = "/opt/biz";
|
||||
private static final String INPUT_FILE_CONTAINER = BASE_PATH + "/upFiles101/2510101004-20260429130857-2039611288698490882.mp4";
|
||||
|
||||
// 视频元数据(硬编码)
|
||||
private static final LocalDateTime VIDEO_START_TIME = LocalDateTime.of(2026, 4, 29, 10, 59, 58);
|
||||
private static final long VIDEO_TOTAL_DURATION = 3601L; // 1小时01秒
|
||||
|
||||
@Override
|
||||
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
||||
try {
|
||||
// 确保输出目录存在
|
||||
File outputDir = new File(BASE_PATH);
|
||||
if (!outputDir.exists()) {
|
||||
outputDir.mkdirs();
|
||||
}
|
||||
|
||||
// ================= 任务规划 =================
|
||||
// 目标:
|
||||
// 1. 切 Part1: 11:15:15 -> 11:30:15
|
||||
// 2. 切 Part2: 11:30:16 -> 11:45:15
|
||||
// 3. 拼接: Part1的后半段(从11:20:15开始) + Part2的前半段(到11:40:15结束)
|
||||
|
||||
// 1. 处理 Part 1
|
||||
LocalDateTime p1StartTarget = LocalDateTime.of(2026, 4, 29, 11, 15, 15);
|
||||
LocalDateTime p1EndTarget = LocalDateTime.of(2026, 4, 29, 11, 30, 15);
|
||||
String part1File = BASE_PATH + "/part1_111515.mp4";
|
||||
|
||||
// 计算在源文件中的偏移量和时长
|
||||
long p1Offset = getSecondsFromStart(p1StartTarget);
|
||||
long p1Duration = Duration.between(p1StartTarget, p1EndTarget).getSeconds();
|
||||
cutVideo(INPUT_FILE_CONTAINER, part1File, p1Offset, p1Duration);
|
||||
|
||||
// 2. 处理 Part 2
|
||||
LocalDateTime p2StartTarget = LocalDateTime.of(2026, 4, 29, 11, 30, 16);
|
||||
LocalDateTime p2EndTarget = LocalDateTime.of(2026, 4, 29, 11, 45, 15);
|
||||
String part2File = BASE_PATH + "/part2_113016.mp4";
|
||||
|
||||
long p2Offset = getSecondsFromStart(p2StartTarget);
|
||||
long p2Duration = Duration.between(p2StartTarget, p2EndTarget).getSeconds();
|
||||
cutVideo(INPUT_FILE_CONTAINER, part2File, p2Offset, p2Duration);
|
||||
|
||||
// 3. 拼接逻辑
|
||||
// 需求:取 Part1 中 11:20:15 之后的内容 + Part2 中 11:40:15 之前的内容
|
||||
LocalDateTime splicePointP1 = LocalDateTime.of(2026, 4, 29, 11, 20, 15);
|
||||
LocalDateTime splicePointP2 = LocalDateTime.of(2026, 4, 29, 11, 40, 15);
|
||||
|
||||
String finalFile = BASE_PATH + "/final_112015_114015.mp4";
|
||||
spliceVideo(part1File, part2File, finalFile, splicePointP1, p1StartTarget, splicePointP2, p2StartTarget);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("视频处理失败", e);
|
||||
throw new JobExecutionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算目标时间相对于视频开始时间(2026-04-29 10:59:58)的秒数偏移
|
||||
*/
|
||||
private long getSecondsFromStart(LocalDateTime targetTime) {
|
||||
return Duration.between(VIDEO_START_TIME, targetTime).getSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切割视频
|
||||
* @param input 输入文件
|
||||
* @param output 输出文件
|
||||
* @param startTime 在源文件中的开始偏移量(秒)
|
||||
* @param duration 持续时间(秒)
|
||||
*/
|
||||
private void cutVideo(String input, String output, long startTime, long duration) throws IOException, InterruptedException {
|
||||
log.info("开始切割: [偏移{}秒, 时长{}秒] -> {}", startTime, duration, output);
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
String ffmpeg = Loader.load(ffmpeg.class);
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
ffmpeg,
|
||||
"-ss", String.valueOf(startTime),
|
||||
"-i", input,
|
||||
"-t", String.valueOf(duration),
|
||||
"-c", "copy",
|
||||
"-avoid_negative_ts", "make_zero",
|
||||
output
|
||||
);
|
||||
// 继承标准输出和错误流,方便调试
|
||||
pb.inheritIO().start().waitFor();
|
||||
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
log.info("切割完成: {}, 耗时: {}秒", output, cost / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接视频
|
||||
* 逻辑:
|
||||
* 1. 计算 Part1 需要截取的时长 (从 splicePoint 到 Part1 结束)
|
||||
* 2. 计算 Part2 需要截取的时长 (从 Part2 开始 到 splicePoint)
|
||||
* 3. 生成临时列表并执行拼接
|
||||
*/
|
||||
private void spliceVideo(String part1, String part2, String output,
|
||||
LocalDateTime p1SplicePoint, LocalDateTime p1Base,
|
||||
LocalDateTime p2SplicePoint, LocalDateTime p2Base) throws IOException, InterruptedException {
|
||||
|
||||
log.info("开始拼接: {} + {} -> {}", part1, part2, output);
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
// 1. 计算 Part1 截取时长:目标拼接点 - Part1起始时间
|
||||
long p1CutDuration = Duration.between(p1Base, p1SplicePoint).getSeconds();
|
||||
|
||||
// 2. 计算 Part2 截取时长:目标拼接点 - Part2起始时间
|
||||
long p2CutDuration = Duration.between(p2Base, p2SplicePoint).getSeconds();
|
||||
|
||||
// 3. 创建临时 concat 文件
|
||||
// 注意:这里使用 input seeking 方式 (-ss 在 -i 之前) 性能最好
|
||||
String concatFile = BASE_PATH + "/concat_list.txt";
|
||||
try (FileWriter writer = new FileWriter(concatFile)) {
|
||||
// 第一部分:从 part1 开头截取 p1CutDuration 秒
|
||||
writer.write("file '" + part1 + "'\n");
|
||||
// 第二部分:从 part2 开头截取 p2CutDuration 秒
|
||||
writer.write("file '" + part2 + "'\n");
|
||||
}
|
||||
|
||||
String ffmpeg = Loader.load(ffmpeg.class);
|
||||
// 使用 -ss 配合 -t 对 concat 流进行切片
|
||||
// 这里有个技巧:因为我们要的是 Part1 的后半段,但 concat 只能顺序读。
|
||||
// 所以更简单的做法是:分别切出这两段临时文件,再合并。
|
||||
// 但为了演示 concat 协议,我们采用“先合并再修剪”或者“分别修剪再合并”。
|
||||
// 鉴于 -c copy 的限制,最稳妥的“拼接特定片段”方案是:
|
||||
// 1. 先切出 Part1 的后半段临时文件
|
||||
// 2. 再切出 Part2 的前半段临时文件
|
||||
// 3. 合并这两个临时文件
|
||||
|
||||
// --- 修正方案:三步走 ---
|
||||
String temp1 = BASE_PATH + "/temp_p1_part.mp4";
|
||||
String temp2 = BASE_PATH + "/temp_p2_part.mp4";
|
||||
|
||||
// 步骤 A: 提取 Part1 的后半段 (从 p1CutDuration 开始,直到文件结束)
|
||||
// 注意:这里不需要 -t,直接截取到末尾
|
||||
ProcessBuilder pb1 = new ProcessBuilder(ffmpeg, "-ss", String.valueOf(p1CutDuration), "-i", part1, "-c", "copy", "-avoid_negative_ts", "make_zero", temp1);
|
||||
pb1.inheritIO().start().waitFor();
|
||||
|
||||
// 步骤 B: 提取 Part2 的前半段 (从 0 开始,时长 p2CutDuration)
|
||||
ProcessBuilder pb2 = new ProcessBuilder(ffmpeg, "-i", part2, "-t", String.valueOf(p2CutDuration), "-c", "copy", temp2);
|
||||
pb2.inheritIO().start().waitFor();
|
||||
|
||||
// 步骤 C: 拼接 temp1 和 temp2
|
||||
try (FileWriter writer = new FileWriter(concatFile)) {
|
||||
writer.write("file '" + temp1 + "'\n");
|
||||
writer.write("file '" + temp2 + "'\n");
|
||||
}
|
||||
|
||||
ProcessBuilder pbConcat = new ProcessBuilder(
|
||||
ffmpeg,
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", concatFile,
|
||||
"-c", "copy",
|
||||
output
|
||||
);
|
||||
pbConcat.inheritIO().start().waitFor();
|
||||
|
||||
// 清理临时文件
|
||||
new File(concatFile).delete();
|
||||
new File(temp1).delete();
|
||||
new File(temp2).delete();
|
||||
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
log.info("拼接完成: {}, 耗时: {}秒", output, cost / 1000);
|
||||
}
|
||||
}
|
||||
|
|
@ -751,7 +751,7 @@
|
|||
<select id="getUploadingTplink" resultType="com.nu.modules.biz.order.entity.DirectiveOrder">
|
||||
select ord.*
|
||||
from nu_biz_directive_order ord
|
||||
where ord.cos_status = '1'
|
||||
where ord.tplink_task_id is not null and order_end_time is null
|
||||
</select>
|
||||
<select id="getTplinkProcessing" resultType="com.nu.modules.biz.order.entity.DirectiveOrder">
|
||||
select ord.*
|
||||
|
|
|
|||
Loading…
Reference in New Issue