From ffc398d416aa4ba4f294c0cb86964e5cecff2ee7 Mon Sep 17 00:00:00 2001
From: liusuyi <1951119284@qq.com>
Date: Wed, 19 Nov 2025 11:37:33 +0800
Subject: [PATCH] 增加媒体库视频缩略图
---
ard-work/pom.xml | 6 +
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java | 176 ++++++++++++++++++++++++++++++++++++--------
ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java | 3
ard-work/src/main/resources/mapper/ArdMediaMapper.xml | 10 ++
4 files changed, 161 insertions(+), 34 deletions(-)
diff --git a/ard-work/pom.xml b/ard-work/pom.xml
index ef3f0a4..9c13535 100644
--- a/ard-work/pom.xml
+++ b/ard-work/pom.xml
@@ -113,6 +113,12 @@
<groupId>com.ard</groupId>
<artifactId>ard-oss</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.bytedeco</groupId>
+ <artifactId>javacv-platform</artifactId>
+ <version>1.5.8</version>
+ </dependency>
+
</dependencies>
<repositories>
<repository>
diff --git a/ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java b/ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java
index 98efa88..d7ae7e0 100644
--- a/ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java
+++ b/ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java
@@ -27,5 +27,6 @@
private Double latitude; // 纬度
private Double height; // 高度
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
- private Date captureTime; // 抓拍时间
+ private Date captureTime; // 抓拍时间
+ private String thumbnailUrl; // 视频缩略图URL
}
diff --git a/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java b/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java
index 38f4c77..5533884 100644
--- a/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java
+++ b/ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java
@@ -17,20 +17,32 @@
import com.ard.work.service.IArdFlightTaskService;
import com.ard.work.service.IArdServerInfoService;
import lombok.extern.slf4j.Slf4j;
+import org.bytedeco.javacpp.BytePointer;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+import org.bytedeco.opencv.global.opencv_imgproc;
+import org.bytedeco.opencv.opencv_core.Size;
+import org.opencv.core.Mat;
+import org.opencv.videoio.VideoCapture;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
+import javax.imageio.ImageIO;
import javax.net.ssl.*;
-import java.io.InputStream;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
-import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
@Slf4j
@Service
@@ -62,7 +74,6 @@
/**
* 同步所有服务的飞行历史
- *
*/
@Scheduled(fixedDelay = 10000)
public void syncAllFlightHistory() {
@@ -162,21 +173,42 @@
private void insertVideoToDatabase(ArdFlightTask flightTask, List<String> videoPathList) {
if (videoPathList != null && !videoPathList.isEmpty()) {
for (String videoPath : videoPathList) {
- ArdMedia ardMedia = ArdMedia.builder()
+ ArdMedia ardMedia = ArdMedia.builder()
.id(IdUtils.simpleUUID())
.taskId(flightTask.getTaskId())
.mediaType(1) // 1表示视频
.sourceUrl(videoPath)
.fileSize(getFileSizeByUrl(videoPath))
.build();
- // 异步上传文件到OSS,并在上传完成后插入数据库
- CompletableFuture.supplyAsync(() -> uploadFileToOss(flightTask.getBoxSn(), videoPath, "record1",
- MimeTypeUtils.VIDEO_MP4), threadPoolTaskExecutor)
- .thenAccept(url -> {
- ardMedia.setMediaUrl(url); // 获取上传后的文件 URL
- ardMedia.setCreateTime(new Date());
- ardMediaMapper.insertArdMedia(ardMedia); // 插入数据库
- });
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ // 上传视频
+ String videoUrl = uploadFileToOss(flightTask.getBoxSn(), videoPath, "record",
+ MimeTypeUtils.VIDEO_MP4);
+ ardMedia.setMediaUrl(videoUrl);
+ // 生成缩略图 InputStream
+ ByteArrayInputStream thumbnailStream = generateThumbnailInputStream(videoPath, 2, 200);
+ if (thumbnailStream != null) {
+ String thumbnailUrl = ossServiceContext.upload("record",
+ flightTask.getBoxSn() + "/" + IdUtils.fastSimpleUUID() + ".jpg",
+ thumbnailStream,
+ MimeTypeUtils.IMAGE_JPEG);
+ ardMedia.setThumbnailUrl(thumbnailUrl);
+ }
+
+ ardMedia.setCreateTime(new Date());
+ return ardMedia;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }, threadPoolTaskExecutor).thenAccept(am -> {
+ if (am != null) {
+ ardMediaMapper.insertArdMedia(am);
+ }
+ });
+
}
}
}
@@ -203,30 +235,50 @@
if (!newVideos.isEmpty()) {
// 插入新的视频记录
for (String videoPath : newVideos) {
- ArdMedia ardMedia = ArdMedia.builder()
+ ArdMedia ardMedia = ArdMedia.builder()
.id(IdUtils.simpleUUID())
.taskId(flightTask.getTaskId())
.mediaType(1) // 1表示视频
.fileSize(getFileSizeByUrl(videoPath))
.sourceUrl(videoPath)
.build();
- // 异步上传文件到OSS,并在上传完成后插入数据库
- CompletableFuture.supplyAsync(() -> uploadFileToOss(flightTask.getBoxSn(), videoPath, "record1",
- MimeTypeUtils.VIDEO_MP4), threadPoolTaskExecutor)
- .thenAccept(url -> {
- ardMedia.setMediaUrl(url); // 获取上传后的文件 URL
- ardMedia.setCreateTime(new Date());
- ardMediaMapper.insertArdMedia(ardMedia); // 插入数据库
- });
- }
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ // 上传视频
+ String videoUrl = uploadFileToOss(flightTask.getBoxSn(), videoPath, "record",
+ MimeTypeUtils.VIDEO_MP4);
+ ardMedia.setMediaUrl(videoUrl);
+
+ // 生成缩略图 InputStream
+ ByteArrayInputStream thumbnailStream = generateThumbnailInputStream(videoPath, 2, 200);
+ if (thumbnailStream != null) {
+ String thumbnailUrl = ossServiceContext.upload("record",
+ flightTask.getBoxSn() + "/" + IdUtils.fastSimpleUUID() + ".jpg",
+ thumbnailStream,
+ MimeTypeUtils.IMAGE_JPEG);
+ ardMedia.setThumbnailUrl(thumbnailUrl);
+ }
+
+ ardMedia.setCreateTime(new Date());
+ return ardMedia;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }, threadPoolTaskExecutor).thenAccept(am -> {
+ if (am != null) {
+ ardMediaMapper.insertArdMedia(am);
+ }
+ });
+ }
// 更新任务的视频数量
flightTask.setVideoCount(flightTask.getVideoCount() + newVideos.size());
ardFlightTaskMapper.updateArdFlightTask(flightTask);
}
}
}
-
// 插入图片记录
private void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
@@ -245,7 +297,7 @@
private void updatePhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) {
int count = flightTask.getPhotoCount(); //获取本地库图片数量
// 查询图片
- BoxPhotosDTO boxPhotosDTO = BoxPhotosDTO.builder()
+ BoxPhotosDTO boxPhotosDTO = BoxPhotosDTO.builder()
.taskId(flightTask.getTaskId())
.boxSn(boxSn)
.pageIndex(1)
@@ -296,7 +348,7 @@
.captureTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, photo.getTimeStamp()))
.build();
// 异步上传文件到OSS,并在上传完成后插入数据库
- CompletableFuture.supplyAsync(() -> uploadFileToOss(flightTask.getBoxSn(), photo.getImgUrl(), "pic1",
+ CompletableFuture.supplyAsync(() -> uploadFileToOss(flightTask.getBoxSn(), photo.getImgUrl(), "pic",
MimeTypeUtils.IMAGE_JPG), threadPoolTaskExecutor)
.thenAccept(url -> {
ardMedia.setMediaUrl(url); // 获取上传后的文件 URL
@@ -337,12 +389,7 @@
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// 创建所有主机名都信任的HostnameVerifier
- HostnameVerifier allHostsValid = new HostnameVerifier() {
- @Override
- public boolean verify(String hostname, SSLSession session) {
- return true;
- }
- };
+ HostnameVerifier allHostsValid = (hostname, session) -> true;
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
// 现在可以正常访问
@@ -374,7 +421,7 @@
return fileOssUrl; // 返回上传后的文件OSS URL
}
- //获取文件大小
+ // 获取文件大小
private long getFileSizeByUrl(String fileUrl) {
try {
URL url = new URL(fileUrl);
@@ -387,4 +434,69 @@
}
}
+ // 生成视频缩略图
+ public ByteArrayInputStream generateThumbnailInputStream(String videoFile,
+ int skipFrames,
+ int targetWidth) throws Exception {
+ long start = System.currentTimeMillis();
+ FFmpegFrameGrabber grabber = null;
+
+ try {
+ grabber = new FFmpegFrameGrabber(videoFile);
+ grabber.start();
+
+ int totalFrames = grabber.getLengthInFrames();
+ System.out.println("视频总帧数: " + totalFrames);
+ System.out.println("视频时长: " + grabber.getLengthInTime() / 1000000 + "秒");
+
+ Frame frame = null;
+ int currentFrame = 0;
+
+ // 读取指定帧数后的第一帧有效图像
+ while (currentFrame < totalFrames) {
+ frame = grabber.grabImage();
+ if (frame == null) {
+ break;
+ }
+
+ if (currentFrame > skipFrames && frame.image != null) {
+ break;
+ }
+ currentFrame++;
+ }
+
+ if (frame == null || frame.image == null) {
+ throw new Exception("无法获取有效视频帧");
+ }
+
+ // 转换 Frame 为 BufferedImage
+ Java2DFrameConverter converter = new Java2DFrameConverter();
+ BufferedImage image = converter.getBufferedImage(frame);
+
+ // 按目标宽度等比缩放
+ int targetHeight = (int) ((double) targetWidth / image.getWidth() * image.getHeight());
+ BufferedImage scaledImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_3BYTE_BGR);
+ scaledImage.getGraphics().drawImage(image, 0, 0, targetWidth, targetHeight, null);
+
+ // 转成 ByteArrayInputStream
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ImageIO.write(scaledImage, "jpg", baos);
+ long cost = System.currentTimeMillis() - start;
+ System.out.println("处理耗时: " + cost + "ms");
+
+ return new ByteArrayInputStream(baos.toByteArray());
+
+ } finally {
+ if (grabber != null) {
+ try {
+ grabber.stop();
+ grabber.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+
}
diff --git a/ard-work/src/main/resources/mapper/ArdMediaMapper.xml b/ard-work/src/main/resources/mapper/ArdMediaMapper.xml
index f80f327..21f6459 100644
--- a/ard-work/src/main/resources/mapper/ArdMediaMapper.xml
+++ b/ard-work/src/main/resources/mapper/ArdMediaMapper.xml
@@ -15,6 +15,7 @@
<result property="longitude" column="longitude"/>
<result property="latitude" column="latitude"/>
<result property="height" column="height"/>
+ <result property="thumbnailUrl" column="thumbnail_url"/>
<result property="captureTime" column="capture_time"/>
<result property="createTime" column="create_time"/>
</resultMap>
@@ -23,7 +24,7 @@
<!-- 查询媒体列表:根据条件动态生成SQL -->
<select id="selectArdMedia" resultMap="ArdMediaResult">
SELECT
- id, task_id, media_type, file_size, file_md5, media_url, source_url,
+ id, task_id, media_type, file_size, file_md5, media_url, source_url,thumbnail_url,
photo_id, longitude, latitude, height, capture_time, create_time
FROM ard_media
<where>
@@ -33,6 +34,7 @@
<if test="fileMd5 != null">AND file_md5 = #{fileMd5}</if>
<if test="mediaUrl != null">AND media_url = #{mediaUrl}</if>
<if test="sourceUrl != null">AND source_url = #{sourceUrl}</if>
+ <if test="thumbnailUrl != null">AND thumbnail_url = #{thumbnailUrl}</if>
<if test="photoId != null">AND photo_id = #{photoId}</if>
<if test="longitude != null">AND longitude = #{longitude}</if>
<if test="latitude != null">AND latitude = #{latitude}</if>
@@ -53,6 +55,7 @@
file_md5,
media_url,
source_url,
+ thumbnail_url,
photo_id,
longitude,
latitude,
@@ -71,6 +74,7 @@
file_md5,
media_url,
source_url,
+ thumbnail_url,
photo_id,
longitude,
latitude,
@@ -92,6 +96,7 @@
<if test="fileMd5 != null">file_md5,</if>
<if test="mediaUrl != null">media_url,</if>
<if test="sourceUrl != null">source_url,</if>
+ <if test="thumbnailUrl != null">thumbnail_url,</if>
<if test="photoId != null">photo_id,</if>
<if test="longitude != null">longitude,</if>
<if test="latitude != null">latitude,</if>
@@ -108,6 +113,7 @@
<if test="fileMd5 != null">#{fileMd5},</if>
<if test="mediaUrl != null">#{mediaUrl},</if>
<if test="sourceUrl != null">#{sourceUrl},</if>
+ <if test="thumbnailUrl != null">#{thumbnailUrl},</if>
<if test="photoId != null">#{photoId},</if>
<if test="longitude != null">#{longitude},</if>
<if test="latitude != null">#{latitude},</if>
@@ -126,6 +132,7 @@
file_md5,
media_url,
source_url,
+ thumbnail_url,
photo_id,
longitude,
latitude,
@@ -142,6 +149,7 @@
#{item.fileMd5},
#{item.mediaUrl},
#{item.sourceUrl},
+ #{item.thumbnailUrl},
#{item.photoId},
#{item.longitude},
#{item.latitude},
--
Gitblit v1.9.3