zhaowenyu
2025-11-21 19144edf60036ad3b9a30b1b92e985de69231137
Merge remote-tracking branch 'origin/dev' into dev
3 files added
3 files modified
365 ■■■■■ changed files
ard-admin/src/main/resources/application-daqing-druid.yml 61 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/resources/application-daqing.yml 81 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/resources/application.yml 3 ●●●●● patch | view | raw | blame | history
ard-work/pom.xml 6 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java 175 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/util/osSelect.java 39 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/resources/application-daqing-druid.yml
New file
@@ -0,0 +1,61 @@
# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://192.168.10.103:3306/ard-uav?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: Ard12345*
            # 从库数据源
            slave:
                # 从数据源开关/默认关闭
                enabled: false
                url:
                username:
                password:
            # 初始连接数
            initialSize: 5
            # 最小连接池数量
            minIdle: 10
            # 最大连接池数量
            maxActive: 20
            # 配置获取连接等待超时的时间
            maxWait: 60000
            # 配置连接超时时间
            connectTimeout: 30000
            # 配置网络超时时间
            socketTimeout: 60000
            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # 配置一个连接在池中最小生存的时间,单位是毫秒
            minEvictableIdleTimeMillis: 300000
            # 配置一个连接在池中最大生存的时间,单位是毫秒
            maxEvictableIdleTimeMillis: 900000
            # 配置检测连接是否有效
            validationQuery: SELECT 1 FROM DUAL
            testWhileIdle: true
            testOnBorrow: false
            testOnReturn: false
            webStatFilter:
                enabled: true
            statViewServlet:
                enabled: true
                # 设置白名单,不填则允许所有访问
                allow:
                url-pattern: /druid/*
                # 控制台管理用户名和密码
                login-username: ruoyi
                login-password: 123456
            filter:
                stat:
                    enabled: true
                    # 慢SQL记录
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: true
                wall:
                    config:
                        multi-statement-allow: true
ard-admin/src/main/resources/application-daqing.yml
New file
@@ -0,0 +1,81 @@
# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8080
# token配置
token:
  # 是否允许账户多终端同时登录(true允许 false不允许)
  soloLogin: false
# 日志配置
logging:
  level:
    com.ard: info
    org.springframework: warn
## knife4j配置
knife4j:
  # knife4j增强功能,开启后下面参数才生效
  enable: true
  #是否为生产模式 项目部属时为true 防止接口暴露
  production: false
  # 配置用户密码
  basic:
    enable: false
    username: admin
    password: ardkj12345
# 地图资源配置
geoserver:
  deviceCoordinateType: WGS84
  tifPath: H:\map\hlj.tif
wayline:
  #可理解为100米大小占图像的比例
  targetRatio: 0.5
  #最大航段距离
  maxLength: 2000
  #最小航段距离
  minLength: 1
  #随飞随动分段长度
  PeerAtLength: 10
  #航线KMZ临时存放路径
  waylineSavePath: /home/ard/myworkspace/uav_server/mainserver/waylines
#上云API
cloud-api:
  app:
    id: 143640
    key: da5babf38a5d05b0f46a7d82910da1b
    license: khjd7FzUTzJgaArNv97XdA2nKQtYfESOvBHLqjC6MpvPqxCfuQ82nOlTgzoiLp7UoZujARopN2A1ctPFaJ8oHiHQrn3IRV4wqN3hdSM40xXj02YVvTWQBT54WS8Ks+E8Zatp3gk2T0+7cM5a5a3mWzxLMv7CNqw2z9pFBoOBdpY=
    host: 192.168.10.103
    port: 123
#流媒体
mediamtx:
  apiHost: 192.168.10.103
  playHost: 192.168.10.103
  https: true
# mqtt配置
mqtt:
  protocol: tcp
  host: 192.168.10.103
  port: 1883
  username: admin
  password: public
  client-id: ard-uav
  inbound-topic: sys/product/+/status,thing/product/+/requests
# oss配置
oss:
  enable: true
  provider: minio
  endpoint: https://192.168.10.103:9001
  access-key: admin
  secret-key: ard12345
  bucket: cloud-bucket
  expire: 3600
  region: us-east-1
  object-dir-prefix: wayline
ard-admin/src/main/resources/application.yml
@@ -163,5 +163,6 @@
  # 激活配置文件
  profiles:
    active: daqing-druid,daqing
    #active: prod-druid,prod
    active: dev-druid,dev
    #active: dev-druid,dev
ard-work/pom.xml
@@ -113,12 +113,6 @@
            <groupId>com.ard</groupId>
            <artifactId>ard-oss</artifactId>
        </dependency>
        <!-- JavaCV -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.8</version>
        </dependency>
    </dependencies>
    <repositories>
        <repository>
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java
@@ -16,28 +16,26 @@
import com.ard.work.mapper.ArdMediaMapper;
import com.ard.work.service.IArdFlightTaskService;
import com.ard.work.service.IArdServerInfoService;
import com.ard.work.util.osSelect;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
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.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -175,7 +173,7 @@
        }
        String boxSn = flightTask.getBoxSn();
        for (String videoPath : videoPathList) {
            //videoPath = "https://192.168.1.3:9001/record/DBM250974065008/8212fd8e89814d66b6c32f48d05f5101.mp4";
            ArdMedia media = ArdMedia.builder()
                    .id(IdUtils.simpleUUID())
                    .taskId(flightTask.getTaskId())
@@ -185,6 +183,7 @@
                    .build();
            // ---- 1. 视频上传(失败不抛异常,返回 null) ----
            CompletableFuture<String> videoFuture = CompletableFuture.supplyAsync(() -> {
                try (InputStream inputStream = openUrlInputStream(videoPath, 5000, 10000)) {
                    if (inputStream != null) {
@@ -201,9 +200,10 @@
                return null;
            }, threadPoolTaskExecutor);
            // ---- 2. 缩略图上传(失败不抛异常,返回 null) ----
            CompletableFuture<String> thumbFuture = CompletableFuture.supplyAsync(() -> {
                try {
                    try (ByteArrayInputStream thumbStream = generateThumbnail(videoPath, 2, 200)) {
                    try (ByteArrayInputStream thumbStream = generateThumbnail(videoPath, 1, 200)) {
                        if (thumbStream != null) {
                            return ossServiceContext.upload(
                                    "record",
@@ -278,7 +278,7 @@
                    // ---- 2. 缩略图上传(失败不抛异常,返回 null) ----
                    CompletableFuture<String> thumbFuture = CompletableFuture.supplyAsync(() -> {
                        try {
                            try (ByteArrayInputStream thumbStream = generateThumbnail(videoPath, 2, 200)) {
                            try (ByteArrayInputStream thumbStream = generateThumbnail(videoPath, 1, 200)) {
                                if (thumbStream != null) {
                                    return ossServiceContext.upload(
                                            "record",
@@ -415,66 +415,129 @@
    }
    // 生成视频缩略图
    public ByteArrayInputStream generateThumbnail(String videoFile, int skipFrames, int targetWidth) {
        long start = System.currentTimeMillis();
        FFmpegFrameGrabber grabber = null;
    public ByteArrayInputStream generateThumbnail1(String videoFile, int skipFrames, int targetWidth) {
//        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());
//
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        } finally {
//            if (grabber != null) {
//                try {
//                    grabber.stop();
//                    grabber.close();
//                } catch (Exception e) {
//                    e.printStackTrace();
//                }
//            }
//        }
        return null;
    }
    public ByteArrayInputStream generateThumbnail(
            String videoFile,
            int skipSeconds,
            int targetWidth) {
        String ffmpegPath = "";
        if (osSelect.isWindows()) {
            ffmpegPath = "ffmpeg";
        } else if (osSelect.isLinux()) {
            ffmpegPath = "/usr/local/ffmpeg/bin/ffmpeg";
        }
        try {
            grabber = new FFmpegFrameGrabber(videoFile);
            grabber.start();
            List<String> command = new ArrayList<>();
            command.add(ffmpegPath);
            command.add("-ss");
            command.add(String.format("00:00:%02d", skipSeconds));
            command.add("-i");
            command.add(videoFile);
            command.add("-vframes");
            command.add("1");
            int totalFrames = grabber.getLengthInFrames();
            System.out.println("视频总帧数: " + totalFrames);
            System.out.println("视频时长: " + grabber.getLengthInTime() / 1000000 + "秒");
            // 等比缩放
            command.add("-vf");
            command.add("scale=" + targetWidth + ":-1");
            Frame frame = null;
            int currentFrame = 0;
            // **确保输出 JPEG**
            command.add("-f");
            command.add("mjpeg");      // 关键点:输出格式固定为 JPEG
            command.add("-vcodec");
            command.add("mjpeg");
            // 读取指定帧数后的第一帧有效图像
            while (currentFrame < totalFrames) {
                frame = grabber.grabImage();
                if (frame == null) {
                    break;
                }
            // 输出到管道
            command.add("pipe:1");
                if (currentFrame > skipFrames && frame.image != null) {
                    break;
                }
                currentFrame++;
            }
            // ⭐ 打印最终命令(可复制)
            log.info("ffmpeg command: {}", String.join(" ", command));
            if (frame == null || frame.image == null) {
                throw new Exception("无法获取有效视频帧");
            }
            ProcessBuilder pb = new ProcessBuilder(command);
            pb.redirectErrorStream(false);
            Process process = pb.start();
            // 转换 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
            // 读取 FFmpeg 输出(JPEG 数据)到内存
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(scaledImage, "jpg", baos);
            long cost = System.currentTimeMillis() - start;
            System.out.println("处理耗时: " + cost + "ms");
            try (InputStream is = process.getInputStream()) {
                byte[] buffer = new byte[8192];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    baos.write(buffer, 0, len);
                }
            }
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("FFmpeg 执行失败,退出码: " + exitCode);
            }
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (grabber != null) {
                try {
                    grabber.stop();
                    grabber.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException("FFmpeg 生成缩略图失败", e);
        }
    }
ard-work/src/main/java/com/ard/work/util/osSelect.java
New file
@@ -0,0 +1,39 @@
package com.ard.work.util;
/**
 * 操作系统和架构判断工具类
 * @author jiangxin
 */
public class osSelect {
    private static final String OS_NAME = System.getProperty("os.name").toLowerCase();
    private static final String OS_ARCH = System.getProperty("os.arch").toLowerCase(); // 例如:x86_64 / amd64 / aarch64
    public static boolean isLinux() {
        return OS_NAME.contains("linux");
    }
    public static boolean isWindows() {
        return OS_NAME.contains("windows");
    }
    public static boolean isMac() {
        return OS_NAME.contains("mac");
    }
    public static boolean isArm64() {
        return OS_ARCH.contains("aarch64") || OS_ARCH.contains("arm64");
    }
    public static boolean isX64() {
        return OS_ARCH.contains("x86_64") || OS_ARCH.contains("amd64");
    }
    public static String getArch() {
        return OS_ARCH;
    }
    public static String getOsName() {
        return OS_NAME;
    }
}