From b5c022295a2d678edff3f54d9ee10e2fa1515c2d Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Fri, 27 Feb 2026 09:39:40 +0800
Subject: [PATCH] 2026-02-27 优化媒体文件入库;新增视频时长、开始拍摄时间、结束时间;
---
ard-sample/src/main/java/com/ard/sample/media/model/MediaFileDTO.java | 10 ++
ard-sample/src/main/java/com/ard/sample/media/service/IFileService.java | 2
ard-sample/src/main/java/com/ard/sample/utils/VideoDurationUtilFFmpeg.java | 92 +++++++++++++++++++++++
ard-sample/src/main/java/com/ard/sample/utils/TimestampUtils.java | 15 +++
ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java | 58 ++++++++++++--
ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java | 20 ++++
ard-sample/src/main/java/com/ard/sample/media/model/MediaFileEntity.java | 10 ++
7 files changed, 194 insertions(+), 13 deletions(-)
diff --git a/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java b/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java
index 62106be..860cd75 100644
--- a/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java
+++ b/ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java
@@ -6,6 +6,7 @@
import com.ard.sample.media.service.IFileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -58,13 +59,26 @@
/**
* 根据任务id获取全部媒体库文件列表
+ *
+ * @param page 页码,默认值为1,从1开始计数
+ * @param pageSize 每页条数,默认值为10,参数名映射为page_size(前端传参用)
+ * @param jobId 任务ID,必填参数,用于筛选指定任务下的媒体文件
+ * @param isType 文件类型筛选标识,非必填(若需必填可加required = true):
+ * 1 - 筛选文件名包含V.jpeg的文件;
+ * 0 - 筛选文件名包含V.mp4的文件
+ * @return 分页的媒体文件列表数据
* */
@ApiOperation("获取任务中媒体文件列表")
@GetMapping("/jobfiles")
- public ResponseResult<PaginationData<MediaFileDTO>> getJobFiles(@RequestParam(defaultValue = "1") Long page,
+ public ResponseResult<PaginationData<MediaFileDTO>> getJobFiles(@ApiParam(value = "页码,默认1", defaultValue = "1")
+ @RequestParam(defaultValue = "1") Long page,
+ @ApiParam(value = "每页条数,默认10", defaultValue = "10")
@RequestParam(name = "page_size", defaultValue = "10") Long pageSize,
- @RequestParam(name = "job_id") String jobId) {
- PaginationData<MediaFileDTO> filesList = fileService.getJobFiles(page, pageSize, jobId);
+ @ApiParam(value = "任务ID(必填),筛选指定任务下的文件", required = true)
+ @RequestParam(name = "job_id") String jobId,
+ @ApiParam(value = "文件类型筛选:1=V.jpeg文件,0=V.mp4文件")
+ @RequestParam(name = "isType") String isType) {
+ PaginationData<MediaFileDTO> filesList = fileService.getJobFiles(page, pageSize, jobId, isType);
return ResponseResult.success(filesList);
}
}
diff --git a/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileDTO.java b/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileDTO.java
index 9dc6383..57d61a3 100644
--- a/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileDTO.java
+++ b/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileDTO.java
@@ -38,7 +38,15 @@
private String fingerprint;
- private LocalDateTime createTime;
+ private String createTime;
+
+ private String updateTime;
+
+ private String startTime;
+
+ private String endTime;
+
+ private double duration;
private String jobId;
}
diff --git a/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileEntity.java b/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileEntity.java
index 46cd8f6..5e3d6d4 100644
--- a/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileEntity.java
+++ b/ard-sample/src/main/java/com/ard/sample/media/model/MediaFileEntity.java
@@ -7,6 +7,7 @@
import lombok.NoArgsConstructor;
import java.io.Serializable;
+import java.math.BigDecimal;
/**
* @author sean
@@ -64,5 +65,14 @@
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Long updateTime;
+
+ @TableField("start_time")
+ private long startTime;
+
+ @TableField("end_time")
+ private long endTime;
+
+ @TableField("duration")
+ private double duration;
}
diff --git a/ard-sample/src/main/java/com/ard/sample/media/service/IFileService.java b/ard-sample/src/main/java/com/ard/sample/media/service/IFileService.java
index 4e9cb0d..de08a9b 100644
--- a/ard-sample/src/main/java/com/ard/sample/media/service/IFileService.java
+++ b/ard-sample/src/main/java/com/ard/sample/media/service/IFileService.java
@@ -39,7 +39,7 @@
* @param jobId 任务id
* @return List<MediaFileDTO> 媒体库文件列表
* */
- PaginationData<MediaFileDTO> getJobFiles(long page, long pageSize,String jobId);
+ PaginationData<MediaFileDTO> getJobFiles(long page, long pageSize,String jobId, String isType);
/**
* Paginate through all media files in this workspace.
diff --git a/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java b/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java
index b63a5a5..f177c56 100644
--- a/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java
+++ b/ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java
@@ -2,6 +2,8 @@
import com.ard.common.config.OssConfiguration;
import com.ard.oss.service.impl.OssServiceContext;
+import com.ard.sample.utils.TimestampUtils;
+import com.ard.sample.utils.VideoDurationUtilFFmpeg;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ard.sample.common.model.Pagination;
@@ -14,18 +16,18 @@
import com.ard.sample.media.model.MediaFileDTO;
import com.ard.sample.media.model.MediaFileEntity;
import com.ard.sample.media.service.IFileService;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
+import java.math.BigDecimal;
import java.net.URL;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
import java.util.stream.Collectors;
/**
@@ -33,9 +35,13 @@
* @version 0.2
* @date 2021/12/9
*/
+@Slf4j
@Service
@Transactional
public class FileServiceImpl implements IFileService {
+
+ // 定义全局的日期时间格式化器(复用避免重复创建)
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Resource
private IFileMapper mapper;
@@ -45,6 +51,12 @@
@Autowired
private OssServiceContext ossService;
+
+ @Autowired
+ private VideoDurationUtilFFmpeg _videoDurationUtil;
+
+ @Autowired
+ private TimestampUtils _timestampUtils;
private Optional<MediaFileEntity> getMediaByFingerprint(String fingerprint) {
MediaFileEntity fileEntity = mapper.selectOne(new LambdaQueryWrapper<MediaFileEntity>()
@@ -67,6 +79,17 @@
public Integer saveFile(FileUploadDTO file) {
MediaFileEntity fileEntity = this.fileUploadConvertToEntity(file);
fileEntity.setFileId(UUID.randomUUID().toString());
+ if(fileEntity.getFileName().contains(".mp4")) {
+ log.info("mp4文件入库时读取视频时长,文件名:{},文件地址:{}",fileEntity.getFileName(),fileEntity.getFilePath());
+ double duration = _videoDurationUtil.getRemoteMp4Duration(file.getPath());
+ log.info("mp4视频文件时长为:{}",duration);
+ fileEntity.setDuration(duration);
+ log.info("视频创建时间:{}",fileEntity.getStartTime());
+ long endTime=_timestampUtils.plusSecondsByMillis(fileEntity.getStartTime(), duration);
+ log.info("视频创建时间+时长=结束时间:{}",endTime);
+ fileEntity.setEndTime(endTime);
+ }
+
return mapper.insert(fileEntity);
}
@@ -85,12 +108,19 @@
* @return List<MediaFileDTO> 媒体库文件列表
*/
@Override
- public PaginationData<MediaFileDTO> getJobFiles(long page, long pageSize,String jobId) {
+ public PaginationData<MediaFileDTO> getJobFiles(long page, long pageSize,String jobId, String isType) {
Page<MediaFileEntity> pageData = mapper.selectPage(
new Page<MediaFileEntity>(page, pageSize),
new LambdaQueryWrapper<MediaFileEntity>()
- .eq(jobId!=null, MediaFileEntity::getJobId, jobId)
- .orderByDesc(MediaFileEntity::getId));
+ .eq(Objects.nonNull(jobId), MediaFileEntity::getJobId, jobId)
+// .orderByDesc(MediaFileEntity::getId));
+ // isType=1 时匹配 V.jpeg
+ .like("1".equals(isType), MediaFileEntity::getFileName, "V.jpeg")
+ // isType=0 时匹配 V.mp4
+ .like("0".equals(isType), MediaFileEntity::getFileName, "V.mp4")
+ // 排序修改:按start_time正序(升序)排列
+ .orderByAsc(MediaFileEntity::getStartTime)); // 关键改动1:替换排序方式
+
List<MediaFileDTO> records = pageData.getRecords()
.stream()
.map(this::entityConvertToDto)
@@ -140,6 +170,10 @@
MediaFileEntity.MediaFileEntityBuilder builder = MediaFileEntity.builder();
if (file != null) {
+ long creaeTime = 0;
+ if(file.getMetadata().getCreatedTime() != null) {
+ creaeTime = file.getMetadata().getCreatedTime().getTime();
+ }
builder.fileName(file.getName())
.filePath(file.getPath())
.fingerprint(file.getFingerprint())
@@ -148,6 +182,7 @@
.isOriginal(file.getExt().getIsOriginal())
.jobId(file.getExt().getFlightId())
.drone(file.getExt().getSn())
+ .startTime(creaeTime)
.tinnyFingerprint(file.getExt().getTinnyFingerprint());
// domain-type-subType
@@ -180,7 +215,14 @@
.tinnyFingerprint(entity.getTinnyFingerprint())
.payload(entity.getPayload())
.createTime(LocalDateTime.ofInstant(
- Instant.ofEpochMilli(entity.getCreateTime()), ZoneId.systemDefault()))
+ Instant.ofEpochMilli(entity.getCreateTime()), ZoneId.systemDefault()).format(DATE_TIME_FORMATTER))
+ .updateTime(LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(entity.getUpdateTime()), ZoneId.systemDefault()).format(DATE_TIME_FORMATTER))
+ .startTime(LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(entity.getStartTime()), ZoneId.systemDefault()).format(DATE_TIME_FORMATTER))
+ .endTime(LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(entity.getEndTime()), ZoneId.systemDefault()).format(DATE_TIME_FORMATTER))
+ .duration(entity.getDuration())
.drone(entity.getDrone())
.jobId(entity.getJobId());
diff --git a/ard-sample/src/main/java/com/ard/sample/utils/TimestampUtils.java b/ard-sample/src/main/java/com/ard/sample/utils/TimestampUtils.java
index e748a19..180486b 100644
--- a/ard-sample/src/main/java/com/ard/sample/utils/TimestampUtils.java
+++ b/ard-sample/src/main/java/com/ard/sample/utils/TimestampUtils.java
@@ -75,6 +75,21 @@
.toEpochMilli();
}
+ /**
+ * 毫秒级时间戳 + 带小数的秒数
+ * @param millisTimestamp 原毫秒级时间戳(13位)
+ * @param seconds 秒数(正数加,负数减,支持小数,如 1.5、-2.3)
+ * @return 计算后的毫秒级时间戳
+ */
+ public long plusSecondsByMillis(long millisTimestamp, double seconds) {
+ // 将带小数的秒数转换为精确的毫秒数(四舍五入保证整数)
+ long millisToAdd = Math.round(seconds * ONE_SECOND);
+ // 基于原时间戳加减对应的毫秒数
+ return Instant.ofEpochMilli(millisTimestamp)
+ .plusMillis(millisToAdd)
+ .toEpochMilli();
+ }
+
// -------------------------- 秒级时间戳操作 --------------------------
diff --git a/ard-sample/src/main/java/com/ard/sample/utils/VideoDurationUtilFFmpeg.java b/ard-sample/src/main/java/com/ard/sample/utils/VideoDurationUtilFFmpeg.java
new file mode 100644
index 0000000..bc874e8
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/utils/VideoDurationUtilFFmpeg.java
@@ -0,0 +1,92 @@
+package com.ard.sample.utils;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 视频时长解析工具类(FFmpeg 命令行版)
+ * 无需下载文件,直接解析远程 HTTPS 地址的 MP4 视频时长
+ */
+@Component
+public class VideoDurationUtilFFmpeg {
+
+ private static final Pattern DURATION_PATTERN = Pattern.compile("Duration: (\\d{2}):(\\d{2}):(\\d{2}\\.\\d{2})");
+
+ /**
+ * 获取远程 HTTPS 地址的 MP4 视频时长
+ * @param videoHttpsUrl 视频 HTTPS 地址
+ * @return 视频时长(秒,保留2位小数),解析失败返回 null
+ */
+ public double getRemoteMp4Duration(String videoHttpsUrl) {
+ // 入参校验
+ if (videoHttpsUrl == null || !videoHttpsUrl.trim().toLowerCase().startsWith("https")
+ || !videoHttpsUrl.trim().toLowerCase().endsWith(".mp4")) {
+ throw new IllegalArgumentException("无效的MP4视频HTTPS地址:" + videoHttpsUrl);
+ }
+
+ try {
+ // 构建 FFmpeg 命令,直接解析远程 URL
+ String[] command = {
+ "ffmpeg",
+ "-i", videoHttpsUrl,
+ "-hide_banner"
+ };
+
+ Process process = new ProcessBuilder(command)
+ .redirectErrorStream(true)
+ .start();
+
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream())
+ );
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Matcher matcher = DURATION_PATTERN.matcher(line);
+ if (matcher.find()) {
+ int hours = Integer.parseInt(matcher.group(1));
+ int minutes = Integer.parseInt(matcher.group(2));
+ double seconds = Double.parseDouble(matcher.group(3));
+
+ double totalSeconds = hours * 3600 + minutes * 60 + seconds;
+
+ return totalSeconds;
+// return new BigDecimal(totalSeconds)
+// .setScale(2, RoundingMode.HALF_UP);
+ }
+ }
+
+ // 等待进程结束
+ process.waitFor();
+ if (process.exitValue() != 0) {
+ throw new RuntimeException("FFmpeg 执行失败,退出码:" + process.exitValue());
+ }
+
+ } catch (Exception e) {
+ System.err.println("解析视频时长失败,URL:" + videoHttpsUrl + ",异常:" + e.getMessage());
+ }
+ return -1;
+ }
+
+// // 测试方法(可删除)
+// public static void main(String[] args) {
+// VideoDurationUtilFFmpeg util = new VideoDurationUtilFFmpeg();
+//
+// TimestampUtils _timestampUtils = new TimestampUtils();
+// // 替换为你的测试视频 URL
+// double duration = util.getRemoteMp4Duration("https://192.168.1.3:9001/dockfile/wayline/d16eb96c-a0cc-458f-852c-09dddd59827f/DJI_202602121540_002_d16eb96c-a0cc-458f-852c-09dddd59827f/DJI_20260212154239_0004_V.mp4");
+// long endTime=_timestampUtils.plusSecondsByMillis(System.currentTimeMillis(),duration);
+// if (duration != -1) {
+// System.out.println("视频时长:" + duration + " 秒");
+// System.out.println("视频结束时间:" + endTime);
+// } else {
+// System.out.println("解析失败");
+// }
+// }
+}
\ No newline at end of file
--
Gitblit v1.9.3