1、mp4切片、拼接 测试用例

2、指令工单上传视频定时任务-测试相关代码调整(非最终正式版本)
This commit is contained in:
1378012178@qq.com 2026-05-06 08:43:44 +08:00
parent 265d679da6
commit fa99052078
5 changed files with 293 additions and 57 deletions

View File

@ -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>

View File

@ -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

View File

@ -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上传进度查询

View File

@ -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);
}
}

View File

@ -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.*