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("解析失败"); // } // } }