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