zhaowenyu
2026-02-27 b5c022295a2d678edff3f54d9ee10e2fa1515c2d
2026-02-27 优化媒体文件入库;新增视频时长、开始拍摄时间、结束时间;
1 files added
6 files modified
207 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/sample/media/FileController.java 20 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/media/model/MediaFileDTO.java 10 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/media/model/MediaFileEntity.java 10 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/media/service/IFileService.java 2 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/media/service/impl/FileServiceImpl.java 58 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/utils/TimestampUtils.java 15 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/utils/VideoDurationUtilFFmpeg.java 92 ●●●●● patch | view | raw | blame | history
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);
    }
}
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;
}
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;
}
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.
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());
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();
    }
    // -------------------------- 秒级时间戳操作 --------------------------
ard-sample/src/main/java/com/ard/sample/utils/VideoDurationUtilFFmpeg.java
New file
@@ -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("解析失败");
//        }
//    }
}