parent
2d7b3d532b
commit
bf058c7304
|
|
@ -0,0 +1,23 @@
|
|||
package com.nu.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: mp4视频处理信息
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-05-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class Mp4HandleEntity implements Serializable {
|
||||
private Date startTime;
|
||||
private Date endTime;
|
||||
private String orgCode;
|
||||
private String orderId;
|
||||
private List<TplinkInfoEntity> taskList;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.nu.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: 视频切片信息
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-05-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class TplinkInfoEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 设备标识
|
||||
*/
|
||||
private String sn;
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private Date startTime;
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
private Date endTime;
|
||||
/**
|
||||
* tplink网络地址
|
||||
*/
|
||||
private String tplinkPath;
|
||||
/**
|
||||
* 机构编码
|
||||
*/
|
||||
private String orgCode;
|
||||
}
|
||||
|
|
@ -1,163 +1,162 @@
|
|||
package com.nu.modules.tplink.uploadlog.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.query.QueryRuleEnum;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import com.nu.modules.tplink.uploadlog.entity.TplinkUploadLog;
|
||||
import com.nu.modules.tplink.uploadlog.service.ITplinkUploadLogService;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.nu.entity.Mp4HandleEntity;
|
||||
import com.nu.entity.TplinkInfoEntity;
|
||||
import com.nu.modules.sysconfig.ISysConfigApi;
|
||||
import com.nu.modules.tplink.uploadlog.entity.TplinkUploadLog;
|
||||
import com.nu.modules.tplink.uploadlog.service.ITplinkUploadLogService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: tplink切片上传日志
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2026-05-09
|
||||
* @Date: 2026-05-09
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Api(tags="tplink切片上传日志")
|
||||
@Api(tags = "tplink切片上传日志")
|
||||
@RestController
|
||||
@RequestMapping("/tplink/uploadlog/tplinkUploadLog")
|
||||
@Slf4j
|
||||
public class TplinkUploadLogController extends JeecgController<TplinkUploadLog, ITplinkUploadLogService> {
|
||||
@Autowired
|
||||
private ITplinkUploadLogService tplinkUploadLogService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param tplinkUploadLog
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "tplink切片上传日志-分页列表查询")
|
||||
@ApiOperation(value="tplink切片上传日志-分页列表查询", notes="tplink切片上传日志-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<TplinkUploadLog>> queryPageList(TplinkUploadLog tplinkUploadLog,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<TplinkUploadLog> queryWrapper = QueryGenerator.initQueryWrapper(tplinkUploadLog, req.getParameterMap());
|
||||
Page<TplinkUploadLog> page = new Page<TplinkUploadLog>(pageNo, pageSize);
|
||||
IPage<TplinkUploadLog> pageList = tplinkUploadLogService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param tplinkUploadLog
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-添加")
|
||||
@ApiOperation(value="tplink切片上传日志-添加", notes="tplink切片上传日志-添加")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody TplinkUploadLog tplinkUploadLog) {
|
||||
tplinkUploadLogService.save(tplinkUploadLog);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param tplinkUploadLog
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-编辑")
|
||||
@ApiOperation(value="tplink切片上传日志-编辑", notes="tplink切片上传日志-编辑")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody TplinkUploadLog tplinkUploadLog) {
|
||||
tplinkUploadLogService.updateById(tplinkUploadLog);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-通过id删除")
|
||||
@ApiOperation(value="tplink切片上传日志-通过id删除", notes="tplink切片上传日志-通过id删除")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
tplinkUploadLogService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-批量删除")
|
||||
@ApiOperation(value="tplink切片上传日志-批量删除", notes="tplink切片上传日志-批量删除")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.tplinkUploadLogService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "tplink切片上传日志-通过id查询")
|
||||
@ApiOperation(value="tplink切片上传日志-通过id查询", notes="tplink切片上传日志-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<TplinkUploadLog> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
TplinkUploadLog tplinkUploadLog = tplinkUploadLogService.getById(id);
|
||||
if(tplinkUploadLog==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(tplinkUploadLog);
|
||||
}
|
||||
@Autowired
|
||||
private ITplinkUploadLogService tplinkUploadLogService;
|
||||
@Autowired
|
||||
private ISysConfigApi sysConfigApi;
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param tplinkUploadLog
|
||||
*/
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param tplinkUploadLog
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "tplink切片上传日志-分页列表查询")
|
||||
@ApiOperation(value = "tplink切片上传日志-分页列表查询", notes = "tplink切片上传日志-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<TplinkUploadLog>> queryPageList(TplinkUploadLog tplinkUploadLog,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<TplinkUploadLog> queryWrapper = QueryGenerator.initQueryWrapper(tplinkUploadLog, req.getParameterMap());
|
||||
Page<TplinkUploadLog> page = new Page<TplinkUploadLog>(pageNo, pageSize);
|
||||
IPage<TplinkUploadLog> pageList = tplinkUploadLogService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param tplinkUploadLog
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-添加")
|
||||
@ApiOperation(value = "tplink切片上传日志-添加", notes = "tplink切片上传日志-添加")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody TplinkUploadLog tplinkUploadLog) {
|
||||
tplinkUploadLogService.save(tplinkUploadLog);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param tplinkUploadLog
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-编辑")
|
||||
@ApiOperation(value = "tplink切片上传日志-编辑", notes = "tplink切片上传日志-编辑")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody TplinkUploadLog tplinkUploadLog) {
|
||||
tplinkUploadLogService.updateById(tplinkUploadLog);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-通过id删除")
|
||||
@ApiOperation(value = "tplink切片上传日志-通过id删除", notes = "tplink切片上传日志-通过id删除")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
tplinkUploadLogService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "tplink切片上传日志-批量删除")
|
||||
@ApiOperation(value = "tplink切片上传日志-批量删除", notes = "tplink切片上传日志-批量删除")
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.tplinkUploadLogService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "tplink切片上传日志-通过id查询")
|
||||
@ApiOperation(value = "tplink切片上传日志-通过id查询", notes = "tplink切片上传日志-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<TplinkUploadLog> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
TplinkUploadLog tplinkUploadLog = tplinkUploadLogService.getById(id);
|
||||
if (tplinkUploadLog == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(tplinkUploadLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param tplinkUploadLog
|
||||
*/
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, TplinkUploadLog tplinkUploadLog) {
|
||||
|
|
@ -165,16 +164,80 @@ public class TplinkUploadLogController extends JeecgController<TplinkUploadLog,
|
|||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequiresPermissions("tplink.uploadlog:nu_iot_tplink_upload_log:importExcel")
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, TplinkUploadLog.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取tplink回放
|
||||
*
|
||||
* @param dto
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "获取tplink回放")
|
||||
@ApiOperation(value = "获取tplink回放", notes = "获取tplink回放")
|
||||
@PostMapping(value = "/tplinkPlayBack")
|
||||
public Result<String> tplinkPlayBack(@RequestBody Mp4HandleEntity dto) {
|
||||
JSONObject mediaservOpenUrl = sysConfigApi.getByKeyByDS("master", "mediaserv_open_url");
|
||||
String mediaservApiAddress = mediaservOpenUrl.getString("configValue");
|
||||
|
||||
//获取对应日期所有源视频(这个多查出来一些不会影响处理效率)
|
||||
Date startTime = dto.getStartTime();
|
||||
Date endTime = dto.getEndTime();
|
||||
LocalDateTime startLdt = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
LocalDateTime endLdt = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
LocalDateTime normalizedStart = startLdt.toLocalDate().atStartOfDay();
|
||||
LocalDateTime normalizedEnd = endLdt.toLocalDate().atTime(23, 59, 59);
|
||||
QueryWrapper<TplinkUploadLog> qw = new QueryWrapper<>();
|
||||
qw.ge("start_time", normalizedStart)
|
||||
.le("start_time", normalizedEnd);
|
||||
List<TplinkUploadLog> sourceMp4List = tplinkUploadLogService.list(qw);
|
||||
|
||||
if (CollectionUtils.isEmpty(sourceMp4List)) {
|
||||
return Result.error("无回放文件");
|
||||
}
|
||||
|
||||
dto.setOrgCode(sourceMp4List.get(0).getOrgCode());
|
||||
dto.setTaskList(BeanUtil.copyToList(sourceMp4List, TplinkInfoEntity.class));
|
||||
|
||||
|
||||
try {
|
||||
//完整请求地址
|
||||
String targetUrl = mediaservApiAddress + "/api/tplink/tplinkPlayBack";
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<Result<String>> response = restTemplate.exchange(
|
||||
targetUrl,
|
||||
HttpMethod.POST,
|
||||
new HttpEntity<>(dto),
|
||||
new ParameterizedTypeReference<Result<String>>() {
|
||||
}
|
||||
);
|
||||
|
||||
//处理返回结果
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
Result<String> remoteResult = response.getBody();
|
||||
if (remoteResult.getCode() == 200) {
|
||||
String videoPath = remoteResult.getResult();
|
||||
return Result.OK(videoPath);
|
||||
} else {
|
||||
return Result.error("无回放文件-api500");
|
||||
}
|
||||
} else {
|
||||
return Result.error("无回放文件-api404");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Result.error("无回放文件-请求异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,34 +74,6 @@
|
|||
<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,192 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue