Parcourir la source

1、主要新增okhttp线程池和设置保活时间。

pc147123 il y a 4 mois
Parent
commit
00fb6982ba

+ 6 - 0
pom.xml

@@ -27,6 +27,12 @@
     </properties>
     <dependencies>
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.9.3</version>
+        </dependency>
+
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>

+ 14 - 0
src/main/java/com/example/unusualsounds/framework/config/CorsConfig.java

@@ -27,4 +27,18 @@ public class CorsConfig {
         bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 最高优先级
         return bean;
     }
+    //直接返回corsFilter
+//    @Bean
+//    public CorsFilter corsFilter() {
+//        // 1. 创建CORS配置对象
+//        CorsConfiguration config = new CorsConfiguration();
+//        config.addAllowedOriginPattern("*");
+//        config.addAllowedHeader("*");
+//        config.addAllowedMethod("*");
+//        config.setAllowCredentials(true);
+//        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+//        source.registerCorsConfiguration("/**",  config);
+//        return new CorsFilter(source);
+//    }
+
 }

+ 39 - 0
src/main/java/com/example/unusualsounds/framework/minio/MinioConfig.java

@@ -3,13 +3,22 @@ package com.example.unusualsounds.framework.minio;
 
 import io.minio.MinioClient;
 import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.ConnectionPool;
+import okhttp3.Dispatcher;
+import okhttp3.OkHttpClient;
+import org.apache.zookeeper.server.quorum.ObserverZooKeeperServer;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
+import java.util.concurrent.TimeUnit;
+
 @Configuration
 @Data
 @ConfigurationProperties(prefix = "minio")
+@Slf4j
 public class MinioConfig {
 
 
@@ -17,6 +26,32 @@ public class MinioConfig {
     private String ak;
     private String sk;
     private String bucket;
+    @Value("${OkHttp.num}")
+    private Integer maxConnectionNum;
+    @Value("${OkHttp.KeepLive}")
+    private Integer keepLive;
+
+
+    @Bean
+    public OkHttpClient okHttpClient(){
+        //新增并发数
+        Dispatcher dispatcher = new Dispatcher();
+        dispatcher.setMaxRequests(128);
+        dispatcher.setMaxRequestsPerHost(20);
+
+
+        //新增连接池配置
+        OkHttpClient build = new OkHttpClient.Builder()
+                .connectionPool(new ConnectionPool(maxConnectionNum, keepLive, TimeUnit.MINUTES))
+                .connectTimeout(120, TimeUnit.SECONDS)
+                .readTimeout(120, TimeUnit.SECONDS)
+                .writeTimeout(120, TimeUnit.SECONDS)
+                .dispatcher(dispatcher)
+                .build();
+        log.info("OkHttpClient 实例哈希值: {}", build.hashCode());
+        return build;
+    }
+
 
     @Bean
     public MinioClient minioClient() {
@@ -24,7 +59,11 @@ public class MinioConfig {
         return MinioClient.builder()
                 .endpoint(endpoint)
                 .credentials(ak, sk)
+                .httpClient(okHttpClient())
                 .build();
     }
 
+
+
+
 }

+ 104 - 78
src/main/java/com/example/unusualsounds/framework/minio/MinioUtil.java

@@ -3,7 +3,6 @@ package com.example.unusualsounds.framework.minio;
 import com.example.unusualsounds.common.utils.DateUtils;
 import com.example.unusualsounds.common.utils.UUID;
 import io.minio.*;
-import io.minio.errors.*;
 import io.minio.http.Method;
 import io.minio.messages.Bucket;
 import io.minio.messages.Item;
@@ -11,19 +10,15 @@ import jakarta.annotation.Resource;
 import jakarta.servlet.ServletOutputStream;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.extern.slf4j.Slf4j;
+import okhttp3.ConnectionPool;
+import okhttp3.OkHttpClient;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.util.FastByteArrayOutputStream;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
 import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -37,184 +32,215 @@ public class MinioUtil {
     @Resource
     private MinioClient minioClient;
 
+    private final OkHttpClient okHttpClient;
+
+    @Autowired
+    public MinioUtil(OkHttpClient okHttpClient) { // 构造器注入
+        this.okHttpClient = okHttpClient;
+        log.info("MinioUtil 中的OkHttpClient实例哈希值: {}", okHttpClient.hashCode());
+    }
+
 
     /**
-     * 查看存储bucket是否存在
+     * 查看存储 bucket 是否存在
+     *
+     * @param bucketName 桶名称
      * @return boolean
      */
     public Boolean bucketExists(String bucketName) {
-        Boolean found;
         try {
-            found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
+            return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("检查 bucket 是否存在失败: {}", e.getMessage(), e);
             return false;
         }
-        return found;
     }
 
     /**
-     * 创建存储bucket
+     * 创建存储 bucket
+     *
+     * @param bucketName 桶名称
      * @return Boolean
      */
     public Boolean makeBucket(String bucketName) {
         try {
-            minioClient.makeBucket(MakeBucketArgs.builder()
-                    .bucket(bucketName)
-                    .build());
+            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
+            return true;
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("创建 bucket 失败: {}", e.getMessage(), e);
             return false;
         }
-        return true;
     }
+
     /**
-     * 删除存储bucket
+     * 删除存储 bucket
+     *
+     * @param bucketName 桶名称
      * @return Boolean
      */
     public Boolean removeBucket(String bucketName) {
         try {
-            minioClient.removeBucket(RemoveBucketArgs.builder()
-                    .bucket(bucketName)
-                    .build());
+            minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
+            return true;
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("删除 bucket 失败: {}", e.getMessage(), e);
             return false;
         }
-        return true;
     }
+
     /**
-     * 获取全部bucket
+     * 获取全部 bucket
+     *
+     * @return List<Bucket>
      */
     public List<Bucket> getAllBuckets() {
         try {
-            List<Bucket> buckets = minioClient.listBuckets();
-            return buckets;
+            return minioClient.listBuckets();
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("获取所有 bucket 失败: {}", e.getMessage(), e);
+            return null;
         }
-        return null;
     }
 
-
-
     /**
      * 文件上传
      *
      * @param file 文件
-     * @return Boolean
+     * @param path 路径
+     * @return 文件访问路径
      */
     public String upload(MultipartFile file, String path) {
+
+        ConnectionPool pool = okHttpClient.connectionPool();
+        log.info("上传前连接池状态:空闲连接数={}, 活跃连接数={}",
+                pool.idleConnectionCount(),
+                pool.connectionCount());
+
         String originalFilename = file.getOriginalFilename();
-        if (StringUtils.isBlank(originalFilename)){
-            throw new RuntimeException();
+        if (StringUtils.isBlank(originalFilename)) {
+            throw new RuntimeException("文件名为空");
         }
-        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
 
-        String objectName = path+ DateUtils.getDate() + "/" + fileName;
+        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
+        String objectName = path + DateUtils.getDate() + "/" + fileName;
 
+        try (InputStream inputStream = file.getInputStream()) {
+            PutObjectArgs objectArgs = PutObjectArgs.builder()
+                    .bucket(prop.getBucket())
+                    .object(objectName)
+                    .stream(inputStream, file.getSize(), -1)
+                    .contentType(file.getContentType())
+                    .build();
 
-        try {
-            PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(prop.getBucket()).object(objectName)
-                    .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
-            //文件名称相同会覆盖
             minioClient.putObject(objectArgs);
+            log.info("上传成功后接池状态:空闲连接数={}, 活跃连接数={}",
+                    pool.idleConnectionCount(),
+                    pool.connectionCount());
+
+            return prop.getEndpoint() + "/" + prop.getBucket() + "/" + objectName;
+
+
         } catch (Exception e) {
-            e.printStackTrace();
+            log.info("上传失败连接池状态:空闲连接数={}, 活跃连接数={}",
+                    pool.idleConnectionCount(),
+                    pool.connectionCount());
+            log.error("文件上传失败: {}", e.getMessage(), e);
             return null;
         }
-        return prop.getEndpoint()+"/"+prop.getBucket()+"/"+objectName;
     }
 
     /**
      * 预览图片
-     * @param fileName
-     * @return
+     *
+     * @param fileName 文件名
+     * @return 预览 URL
      */
-    public String preview(String fileName)  {
-
-        // 查看文件地址
-        GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder()
+    public String preview(String fileName) {
+        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                 .bucket(prop.getBucket())
                 .object(fileName)
                 .method(Method.GET)
                 .build();
+
         try {
-            String url = minioClient.getPresignedObjectUrl(build);
-            return url;
+            return minioClient.getPresignedObjectUrl(args);
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("获取预览 URL 失败: {}", e.getMessage(), e);
+            return null;
         }
-        return null;
     }
 
     /**
      * 文件下载
-     * @param fileName 文件名称
-     * @param res response
-     * @return Boolean
+     *
+     * @param fileName 文件名
+     * @param res      HttpServletResponse
      */
     public void download(String fileName, HttpServletResponse res) {
-        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(prop.getBucket())
-                .object(fileName).build();
-        try (GetObjectResponse response = minioClient.getObject(objectArgs)){
+        GetObjectArgs objectArgs = GetObjectArgs.builder()
+                .bucket(prop.getBucket())
+                .object(fileName)
+                .build();
+
+        try (GetObjectResponse response = minioClient.getObject(objectArgs)) {
             byte[] buf = new byte[1024];
             int len;
-            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()){
-                while ((len=response.read(buf))!=-1){
-                    os.write(buf,0,len);
+            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
+                while ((len = response.read(buf)) != -1) {
+                    os.write(buf, 0, len);
                 }
                 os.flush();
                 byte[] bytes = os.toByteArray();
+
                 res.setCharacterEncoding("utf-8");
-                // 设置强制下载不打开
                 res.setContentType("application/force-download");
                 res.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
-                try (ServletOutputStream stream = res.getOutputStream()){
+
+                try (ServletOutputStream stream = res.getOutputStream()) {
                     stream.write(bytes);
                     stream.flush();
                 }
             }
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("文件下载失败: {}", e.getMessage(), e);
         }
     }
 
-
-
-
     /**
      * 查看文件对象
-     * @return 存储bucket内文件对象信息
+     *
+     * @return 存储桶内文件对象信息
      */
     public List<Item> listObjects() {
         Iterable<Result<Item>> results = minioClient.listObjects(
                 ListObjectsArgs.builder().bucket(prop.getBucket()).build());
+
         List<Item> items = new ArrayList<>();
         try {
             for (Result<Item> result : results) {
                 items.add(result.get());
             }
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("列出文件对象失败: {}", e.getMessage(), e);
             return null;
         }
         return items;
     }
 
     /**
-     * 删除
-     * @param fileName
-     * @return
-     * @throws Exception
+     * 删除文件
+     *
+     * @param fileName 文件名
+     * @return 是否删除成功
      */
-    public boolean remove(String fileName){
+    public boolean remove(String fileName) {
         try {
-            minioClient.removeObject( RemoveObjectArgs.builder().bucket(prop.getBucket()).object(fileName).build());
-        }catch (Exception e){
+            minioClient.removeObject(
+                    RemoveObjectArgs.builder().bucket(prop.getBucket()).object(fileName).build());
+            return true;
+        } catch (Exception e) {
+            log.error("删除文件失败: {}", e.getMessage(), e);
             return false;
         }
-        return true;
     }
-
 }

+ 262 - 68
src/main/java/com/example/unusualsounds/project/vox/config/VideoTaskManager.java

@@ -7,6 +7,7 @@ import com.example.unusualsounds.framework.minio.MinioUtil;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
+import okhttp3.ConnectionPool;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.*;
@@ -14,10 +15,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestTemplate;
 
 import java.io.*;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -38,16 +36,17 @@ public class VideoTaskManager {
     private String postAnalysisUrl;
 
     @Autowired
-    private  MinioUtil minioUtil;
+    private MinioUtil minioUtil;
 
 
     private final ConcurrentMap<String, VideoTask> taskMap = new ConcurrentHashMap<>();
 
 
-    public boolean startTask(String streamId,String deviceName,String skillID, String rtspUrl,String skillName,String openUuid) {
+    public boolean startTask(String streamId, String deviceName, String skillID, String rtspUrl, String skillName, String openUuid) {
         String compositeKey = streamId + "_" + skillName;
+
         return taskMap.computeIfAbsent(compositeKey, key -> {
-            VideoTask task = new VideoTask(key,rtspUrl,skillName, skillID,deviceName,openUuid);
+            VideoTask task = new VideoTask(key, rtspUrl, skillName, skillID, deviceName, openUuid);
             task.start();
             return task;
         }) != null;
@@ -76,7 +75,7 @@ public class VideoTaskManager {
     public List<TaskStatus> listTasks() {
 
         return taskMap.entrySet().stream()
-                .map(e -> new TaskStatus(e.getKey(), e.getValue().getRtspUrl(),e.getValue().openUuid, e.getValue().isRunning()))
+                .map(e -> new TaskStatus(e.getKey(), e.getValue().getRtspUrl(), e.getValue().openUuid, e.getValue().isRunning()))
                 .collect(Collectors.toList());
     }
 
@@ -100,32 +99,52 @@ public class VideoTaskManager {
         private Process process;
         private Future<?> future;
 
-        VideoTask(String streamId, String rtspUrl,String skillName,String skillId, String deviceName,String openUuid) {
+        VideoTask(String streamId, String rtspUrl, String skillName, String skillId, String deviceName, String openUuid) {
             this.streamId = streamId;
             this.rtspUrl = rtspUrl;
-            this.skillName=skillName;
-            this.skillId=skillId;
-            this.deviceName=deviceName;
-            this.openUuid= openUuid;
+            this.skillName = skillName;
+            this.skillId = skillId;
+            this.deviceName = deviceName;
+            this.openUuid = openUuid;
         }
 
         void start() {
+//            List<String> command = Arrays.asList(
+////                    "D:\\file\\ffmpeg\\ffmpeg.exe",
+//                    "ffmpeg",
+//                    "-rtsp_transport", "tcp",
+//                    "-i", rtspUrl,
+//                    "-vn",
+//                    "-c", "copy",
+//                    "-f", "segment",
+//                    "-segment_time", "300", //100秒
+//                    "-segment_format", "mp4",
+//                    "-reset_timestamps", "1",
+//                    "-force_key_frames", "expr:gte(t,n_floor(t/100)*100)",
+//                    "-write_empty_segments", "1",
+//                    "-segment_atclocktime", "1",
+//                    "-strftime", "1",
+//                    videoDir+streamId+"-%Y-%m-%d_%H-%M-%S"+".mp4"
+//            );
+
+            //现场环境内ffmpeg commandList
             List<String> command = Arrays.asList(
-//                    "D:\\file\\ffmpeg\\ffmpeg.exe",
                     "ffmpeg",
                     "-rtsp_transport", "tcp",
                     "-i", rtspUrl,
                     "-vn",
-                    "-c", "copy",
+                    "-c:v", "copy",
+                    "-c:a", "aac",   // 现场音频为pcm_alaw转mp4不兼容,需要先转aac
                     "-f", "segment",
-                    "-segment_time", "300", //100秒
+                    "-segment_time", "300", //5分钟
                     "-segment_format", "mp4",
                     "-reset_timestamps", "1",
                     "-force_key_frames", "expr:gte(t,n_floor(t/100)*100)",
                     "-write_empty_segments", "1",
                     "-segment_atclocktime", "1",
                     "-strftime", "1",
-                    videoDir+streamId+"-%Y-%m-%d_%H-%M-%S"+".mp4"
+                    videoDir + streamId + "-%Y-%m-%d_%H-%M-%S" + ".mp4"
+
             );
 
             ProcessBuilder pb = new ProcessBuilder(command)
@@ -166,6 +185,7 @@ public class VideoTaskManager {
 
 
         private Void monitorProcess() {
+            List<CompletableFuture<?>> futures = new ArrayList<>();
             try (BufferedReader reader = new BufferedReader(
                     new InputStreamReader(process.getInputStream()))) {
                 String line;
@@ -174,25 +194,41 @@ public class VideoTaskManager {
                     if (Thread.interrupted()) {
                         log.info("[{}] 进程监控被中断", streamId);
                         return null;
-                    };
+                    }
+                    ;
                     //[segment @ 000002bcb71e5680] Opening 'D:\tmp\data\vox\output-2025-02-14_17-53-20499aec0d-0579-4ce8-8a19-0cdea2c8a648.mp4' for writing
-//                    log.info("[{}]FFmpeg: {}", streamId, line);
+                    //不打印rtsp的过程日志
+                    //log.info("[{}]FFmpeg: {}", streamId, line);
                     Matcher matcher = compile.matcher(line);
                     if (matcher.find()) {
                         String filePath = matcher.group(1).trim();
                         log.info("[{}] 新的视频文件已生成: {}", streamId, filePath);
-                        //上传文件、插入数据表告知算法解析;
-                        uploadFile(filePath,skillName,streamId,skillId,deviceName);
+                        //使用CompletableFuture进行检查
+                        CompletableFuture<CompletableFuture<?>> exceptionally = checkUploadFileAsync(filePath)
+                                .thenApply(isCompleted -> {
+                                    if (isCompleted) {
+                                        return uploadFileAsync(filePath, skillName, streamId, skillId, deviceName);
+                                    } else {
+                                        log.warn("文件 {} 未完成,跳过上传", filePath);
+                                        return CompletableFuture.completedFuture(null);
+                                    }
+                                })
+                                .exceptionally(ex -> {
+                                    log.error("处理文件 {} 时发生错误: {}", filePath, ex.getMessage(), ex);
+                                    return null;
+                                });
+                        futures.add(exceptionally);
                     }
 
                 }
-                    int exitCode = process.waitFor();
-                    log.info("[{}] 进程退出,显示代码 {}", streamId, exitCode);
+                int exitCode = process.waitFor();
+                log.info("[{}] 进程退出,显示代码 {}", streamId, exitCode);
+                CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt(); // 重新设置中断状态
                 log.warn("[{}] 进程监控被中断", streamId, e);
-            }
-            catch (IOException e) {
+            } catch (IOException e) {
                 log.error("[{}] 进程监控失败", streamId, e);
             } finally {
                 cleanup();
@@ -212,66 +248,224 @@ public class VideoTaskManager {
     }
 
     /**
-     * 上传切分的文件
+     * 检查切片文件是否完成
      * @param filePath
      * @return
      */
-    private CompletableFuture<Void> uploadFile(String filePath,String skillName,String streamId,String skillId, String deviceName){
-
-        return CompletableFuture.runAsync(()->{
+    private CompletableFuture<Boolean> checkUploadFileAsync(String filePath) {
+        return CompletableFuture.supplyAsync(() -> {
+            Process process = null;
             try {
-                while (true){
-                //检查文件是否完成
+                int attempts = 0;
+                int waitTime = 2*60*1000; //2分钟
+                while (attempts < 5) {
                     List<String> checkVoxFinish = Arrays.asList(
-//                            "D:\\file\\ffmpeg\\ffmpeg.exe",
-                            "ffmpeg",
-                            "-v","error",
+                            "ffprobe",
+                            "-v", "error",
                             "-i", filePath,
-                            "-f", "null",
-                            "-"
+                            "-show_entries", "format=duration",
+                            "-of", "default=noprint_wrappers=1:nokey=1"
                     );
-                    Process process = new ProcessBuilder(checkVoxFinish).start();
-                    BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+
+                    process = new ProcessBuilder(checkVoxFinish).start();
                     StringBuilder errorOutput = new StringBuilder();
 
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-                        errorOutput.append(line).append("\n");
+                    // 读取错误输出
+                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+                        String line;
+                        while ((line = reader.readLine()) != null) {
+                            errorOutput.append(line).append("\n");
+                        }
                     }
+
                     int exitCode = process.waitFor();
                     if (exitCode != 0 || !errorOutput.toString().trim().isEmpty()) {
-                        log.error("checkVoxFinish:[{}]文件未完成",filePath);
-                        //重置状态errorOutput状态
-                        errorOutput.delete(0, errorOutput.length());
-                        //等待2分钟后再次检查
-                        Thread.sleep(120000);
-                    }else {
-                        //上传文件
-                        File file = new File(filePath);
-                        FileToMultipartFile fileToMultipartFile = new FileToMultipartFile(file);
-                        //上传文件
-                        String upload = minioUtil.upload(fileToMultipartFile,sourceDir);
-                        log.info("上传文件:{}",upload);
-
-                        //发送消息
-                        Map<String, Object> map = new HashMap<>();
-                        map.put("filePath",upload);
-                        map.put("skillName",skillName);
-                        map.put("streamId",streamId);
-                        map.put("deviceName",deviceName);
-                        map.put("skillId",skillId);
-                        log.info(VoiceAnalysisUtils.postVoxAnalysis(map, postAnalysisUrl));
-                        break;
+                        log.error("checkUploadFile:[{}]文件未完成,错误信息:{}", filePath, errorOutput.toString());
+                        Thread.sleep(waitTime);
+                        attempts++;
+                    } else {
+                        log.info("checkUploadFile:[{}]文件已完成", filePath);
+                        return true;
                     }
                 }
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            } catch (IOException e) {
+                return false; // 超过最大尝试次数,返回 false
+            } catch (InterruptedException | IOException e) {
+                log.error("checkUploadFile:检查文件[{}]失败", filePath, e);
                 throw new RuntimeException(e);
+            } finally {
+                if (process != null) {
+                    process.destroy();
+                }
             }
         });
-
     }
 
+    /**
+     *  上传切片音频文件
+     * @param filePath
+     * @param skillName
+     * @param streamId
+     * @param skillId
+     * @param deviceName
+     * @return
+     */
+    private CompletableFuture<Void> uploadFileAsync(String filePath, String skillName, String streamId, String skillId, String deviceName) {
+        return CompletableFuture.runAsync(() -> {
+            int maxRetries = 4; // 最大重试次数
+            int retryCount = 0; // 当前重试次数
+            boolean uploadSuccess = false; // 标记上传是否成功
+            while (retryCount < maxRetries && !uploadSuccess) {
+                try {
+                    // 上传文件
+                    File file = new File(filePath);
+                    FileToMultipartFile fileToMultipartFile = new FileToMultipartFile(file);
+                    String upload = minioUtil.upload(fileToMultipartFile, sourceDir);
+                    log.info("上传文件:{}", upload);
+                    if (upload == null || upload.isEmpty()) {
+                        // 手动抛出异常
+                        throw new Exception();
+                    } else {
+                        // 发送消息
+                        Map<String, Object> map = new HashMap<>();
+                        map.put("filePath", upload);
+                        map.put("skillName", skillName);
+                        map.put("streamId", streamId);
+                        map.put("deviceName", deviceName);
+                        map.put("skillId", skillId);
 
-}
+                        log.info(VoiceAnalysisUtils.postVoxAnalysis(map, postAnalysisUrl));
+                        // 标记上传成功
+                        uploadSuccess = true;
+                    }
+                } catch (Exception e) {
+                    // 记录错误日志
+                    log.error("上传文件{}或发送消息失败,重试次数: {}/{},错误信息: {}", filePath, retryCount + 1, maxRetries, e.getMessage(), e);
+                    // 增加重试计数
+                    retryCount++;
+                    // 如果达到最大重试次数,抛出异常
+                    if (retryCount >= maxRetries) {
+                        log.error("已达到最大重试次数,上传{}失败", filePath);
+                        break;
+                    }
+                    try {
+                        Thread.sleep(20000); // 等待 20 秒
+                    } catch (InterruptedException interruptedException) {
+                        log.error("线程等待中断: {}", interruptedException.getMessage());
+                        Thread.currentThread().interrupt(); // 恢复中断状态
+                    }
+                }
+            }
+        });
+    }
+}
+
+
+//    /**
+//     * 只检查文件是否完成
+//     *
+//     * @param filePath
+//     * @return
+//     */
+//    private CompletableFuture<Void> checkUploadFile(String filePath,String skillName,String streamId,String skillId, String deviceName){
+//
+//        return CompletableFuture.runAsync(()->{
+//            Process process  = null;
+//            BufferedReader reader = null;
+//            try {
+//                //不能无限检查3*5 15分钟
+//                int isNum=0;
+//                boolean isFlag =false;
+//                while (isNum<5){
+//                //检查文件是否完成
+//                    List<String> checkVoxFinish = Arrays.asList(
+////                            "D:\\file\\ffmpeg\\ffmpeg.exe",
+//                            "ffmpeg",
+//                            "-v","error",
+//                            "-i", filePath,
+//                            "-f", "null",
+//                            "-"
+//                    );
+//                    process = new ProcessBuilder(checkVoxFinish).start();
+//                    reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+//                    StringBuilder errorOutput = new StringBuilder();
+//
+//                    String line;
+//                    while ((line = reader.readLine()) != null) {
+//                        errorOutput.append(line).append("\n");
+//                    }
+//                    int exitCode = process.waitFor();
+//                    if (exitCode != 0 || !errorOutput.toString().trim().isEmpty()) {
+//                        log.error("checkVoxFinish:[{}]文件未完成",filePath);
+//                        //重置状态errorOutput状态
+//                        errorOutput.delete(0, errorOutput.length());
+//                        //等待3分钟后再次检查
+//                        Thread.sleep(180000);
+//                        isNum++;
+//                    }else {
+//                        //跳出循环,上传文件
+//                        isFlag =true;
+//                        break;
+//                    }
+//                }
+//                if (isFlag) {
+//                    int maxRetries = 4; // 最大重试次数
+//                    int retryCount = 0; // 当前重试次数
+//                    boolean uploadSuccess = false; // 标记上传是否成功
+//                    while (retryCount < maxRetries && !uploadSuccess) {
+//                        try {
+//                            // 上传文件
+//                            File file = new File(filePath);
+//                            FileToMultipartFile fileToMultipartFile = new FileToMultipartFile(file);
+//                            String upload = minioUtil.upload(fileToMultipartFile, sourceDir);
+//                            log.info("上传文件:{}", upload);
+//                            // 发送消息
+//                            Map<String, Object> map = new HashMap<>();
+//                            map.put("filePath", upload);
+//                            map.put("skillName", skillName);
+//                            map.put("streamId", streamId);
+//                            map.put("deviceName", deviceName);
+//                            map.put("skillId", skillId);
+//                            log.info(VoiceAnalysisUtils.postVoxAnalysis(map, postAnalysisUrl));
+//                            // 标记上传成功
+//                            uploadSuccess = true;
+//                        } catch (Exception e) {
+//                            // 记录错误日志
+//                            log.error("上传文件或发送消息失败,重试次数: {}/{},错误信息: {}", retryCount + 1, maxRetries, e.getMessage(), e);
+//                            // 增加重试计数
+//                            retryCount++;
+//                            // 如果达到最大重试次数,抛出异常
+//                            if (retryCount >= maxRetries) {
+//                                log.error("已达到最大重试次数,上传失败");
+//                                throw new RuntimeException("上传文件失败,已达到最大重试次数", e);
+//                            }
+//                            // 可选:等待一段时间再重试(例如 2 秒)
+//                            try {
+//                                Thread.sleep(2000); // 等待 2 秒
+//                            } catch (InterruptedException interruptedException) {
+//                                log.error("线程等待中断: {}", interruptedException.getMessage());
+//                                Thread.currentThread().interrupt(); // 恢复中断状态
+//                            }
+//                        }
+//                    }
+//                }
+//
+//            } catch (InterruptedException e) {
+//                throw new RuntimeException(e);
+//            } catch (IOException e) {
+//                throw new RuntimeException(e);
+//            } finally {
+//                if(reader !=null){
+//                    try {
+//                        reader.close();
+//                    } catch (IOException e) {
+//                        log.error("reader关闭失败{}",e.getMessage());
+//                    }
+//                }
+//                if(process !=null){
+//                    process.destroy();
+//                }
+//
+//            }
+//        });
+//
+//    }

+ 8 - 8
src/main/java/com/example/unusualsounds/project/vox/controller/CheckVoxController.java

@@ -65,14 +65,14 @@ public class CheckVoxController {
         boolean  skillOpen = device.getSkills().get(0).isOpen();
 
             if(skillOpen){
-                //添加校验视频流地址是否有效,无效则不启动算法
-                boolean checkStream = videoServiceImpl.checkStream(camera);
-                log.info("视频流地址校验结果:{}",checkStream);
-                if(!checkStream){
-                    //保存启停接口的设备信息到数据库,便于后续定时任务调用
-                    voiceTicketLogsServiceImpl.saveStartStop(device,openUuid,"视频流地址无效");
-                    return AjaxResult.error("视频流地址无效");
-                }
+                //添加校验视频流地址是否有效,无效则不启动算法(考虑现场网络环境差,不进行15s校验rtsp)
+//                boolean checkStream = videoServiceImpl.checkStream(camera);
+//                log.info("视频流地址校验结果:{}",checkStream);
+//                if(!checkStream){
+//                    //保存启停接口的设备信息到数据库,便于后续定时任务调用
+//                    voiceTicketLogsServiceImpl.saveStartStop(device,openUuid,"视频流地址无效");
+//                    return AjaxResult.error("视频流地址无效");
+//                }
                 //开启处理视频流和接入算法
                 videoServiceImpl.handlesStream(device.getDeviceId(),camera.getSrcURL(),skill,camera.getName(),openUuid);
                 //保存启停接口的设备信息到数据库,便于后续定时任务调用

+ 2 - 2
src/main/java/com/example/unusualsounds/project/vox/task/TicketsTask.java

@@ -84,10 +84,10 @@ public class TicketsTask {
 
 
     /**
-     * 每两小时检查一次
+     * 每两小时检查一次-->每天凌晨两点进行检测
      * @throws IOException
      */
-    @Scheduled(cron = "0 2 * * * ?")
+    @Scheduled(cron = "0 0 2 * * ?")
     public void delVideoFile() throws IOException {
         Path path = Paths.get(videoDir);
         if(!Files.exists(path)) {

+ 28 - 2
src/main/resources/application-dev.yml

@@ -1,4 +1,4 @@
-# 应用服务 WEB 访问端口
+# 应用服务 WEB 访问端口 公司本地
 server:
   port: 19801
 
@@ -70,4 +70,30 @@ vox:
   #vox服务地址
   post-analysis-url: http://192.168.1.188:8899/audio
   #视频流切片保存地址
-  video-dir: D:\tmp\data\vox\
+  video-dir: D:\tmp\data\vox\
+
+#一见平台相关配置
+yijian-interface:
+  #中心节点配置信息
+  center:
+      ak: fd01ec4de8724d71bb764190f202929c
+      sk: f44a391cadcc42f98167e7e42086514f
+      endpoint: http://10.68.204.52:8412
+
+
+  #接口地址前缀配置
+  uri-path-prefix: /api/spi
+  #设备信息接口地址 GET /v1/devices/{deviceID}
+  device-info: /v1/devices/{deviceID}
+  #组织树接口地址 GET /v1/organizations
+  organizations: /v1/organizations
+
+
+#摄像头数量配置
+Camera:
+  num: 60
+
+
+OkHttp:
+  num: 30
+  KeepLive: 13

+ 96 - 0
src/main/resources/application-edge.yml

@@ -0,0 +1,96 @@
+# 应用服务 WEB 访问端口 一见边端侧
+server:
+  port: 19801
+
+knife4j:
+  #开启生产环境屏蔽
+  production: true
+
+
+minio:
+  endpoint: http://minio-idaas:9000
+  ak: AKIAIOSFODNN7EXAMPLE
+  sk: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
+  bucket: windmill
+  dir: store/abnormal-sound/
+  source-dir: store/abnormal-sound/source/
+
+spring:
+  datasource:
+    dynamic:
+      primary: master
+      datasource:
+        #中心测
+        master:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://mysql.jxfdc004.svc.cluster.local:8436/iip_api_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+          username: root
+          password: fbLfJZ1sFMAw
+        #边缘测
+        slave:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: jdbc:mysql://mysql.jxfdc004.svc.cluster.local:8436/iip_api_service?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
+          username: root
+          password: fbLfJZ1sFMAw
+    druid:
+      initial-size: 5
+      min-idle: 5
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 6000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 60000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+  servlet:
+    multipart:
+      max-file-size: 100MB
+      max-request-size: 100MB
+  data:
+    redis:
+      host: redis-single-master.jxfdc004.svc.cluster.local
+      port: 6379
+      password: 12fbLfJZ1a3F
+      database: 0
+      timeout: 6000ms
+      lettuce:
+        pool:
+          min-idle: 5
+          max-idle: 10
+          max-wait: -1ms
+          max-active: 1000
+
+#异音算法接口
+vox:
+  #vox服务地址
+#  post-analysis-url: http://vox-model-service.vox-services.svc.cluster.local:8899/audio
+  post-analysis-url: http://vox-model-service:8899/audio
+  #视频流切片保存地址
+  video-dir: /tmp/
+
+#一见平台相关配置
+yijian-interface:
+  center:
+    ak: a440a816330a4e558ec33d50aaad379d
+    sk: d9068f8ff3604a45a83b81c987de673a
+    endpoint: http://jxygz-lysffx.chnenergy.com.cn:8412
+
+  #接口地址前缀配置
+  uri-path-prefix: /api/spi
+  #设备信息接口地址 GET /v1/devices/{deviceID}
+  device-info: /v1/devices/{deviceID}
+  #组织树接口地址 GET /v1/organizations
+  organizations: /v1/organizations
+
+#摄像头数量配置
+Camera:
+  num: 60
+
+OkHttp:
+  num: 30
+  #当前分片时间是5分钟,保活取两倍+1
+  KeepLive: 13

+ 25 - 2
src/main/resources/application-prod.yml

@@ -1,4 +1,4 @@
-# 应用服务 WEB 访问端口
+# 应用服务 WEB 访问端口 一见中心侧
 server:
   port: 19801
 
@@ -71,4 +71,27 @@ vox:
 #  post-analysis-url: http://vox-model-service.vox-services.svc.cluster.local:8899/audio
   post-analysis-url: http://vox-model-service:8899/audio
   #视频流切片保存地址
-  video-dir: /tmp
+  video-dir: /tmp/
+
+#一见平台相关配置
+yijian-interface:
+  #中心节点配置信息
+  center:
+    ak: a440a816330a4e558ec33d50aaad379d
+    sk: d9068f8ff3604a45a83b81c987de673a
+    endpoint: http://10.170.69.123:8412
+  #接口地址前缀配置
+  uri-path-prefix: /api/spi
+  #设备信息接口地址 GET /v1/devices/{deviceID}
+  device-info: /v1/devices/{deviceID}
+  #组织树接口地址 GET /v1/organizations
+  organizations: /v1/organizations
+
+
+#摄像头数量配置
+Camera:
+  num: 60
+
+OkHttp:
+  num: 30
+  KeepLive: 13

+ 25 - 1
src/main/resources/application-test.yml

@@ -71,4 +71,28 @@ vox:
 #  post-analysis-url: http://vox-model-service.vox-services.svc.cluster.local:8899/audio
   post-analysis-url: http://vox-model-service:8899/audio
   #视频流切片保存地址
-  video-dir: /tmp
+  video-dir: /tmp/
+
+#一见平台相关配置
+yijian-interface:
+  #中心节点配置信息
+  center:
+      ak: fd01ec4de8724d71bb764190f202929c
+      sk: f44a391cadcc42f98167e7e42086514f
+      endpoint: http://10.68.204.52:8412
+
+
+  #接口地址前缀配置
+  uri-path-prefix: /api/spi
+  #设备信息接口地址 GET /v1/devices/{deviceID}
+  device-info: /v1/devices/{deviceID}
+  #组织树接口地址 GET /v1/organizations
+  organizations: /v1/organizations
+
+#摄像头数量配置
+Camera:
+  num: 60
+
+OkHttp:
+  num: 30
+  KeepLive: 13

+ 2 - 23
src/main/resources/application.yml

@@ -1,8 +1,6 @@
 spring:
   profiles:
-    active: prod
-
-
+    active: dev
 
 springdoc:
   swagger-ui:
@@ -46,31 +44,12 @@ knife4j:
 
 logging:
   level:
-    root: warn
+    root: info
     com.example.unusualsounds: debug
 
-#摄像头数量配置
-Camera:
-  num: 40
 
-#一见平台相关配置
-yijian-interface:
-  #中心节点配置信息
-  center:
-    ak: a440a816330a4e558ec33d50aaad379d
-    sk: d9068f8ff3604a45a83b81c987de673a
-    endpoint: http://10.170.69.123:8412
-#    ak: fd01ec4de8724d71bb764190f202929c
-#    sk: f44a391cadcc42f98167e7e42086514f
-#    endpoint: http://10.68.204.52:8412
 
 
-  #接口地址前缀配置
-  uri-path-prefix: /api/spi
-  #设备信息接口地址 GET /v1/devices/{deviceID}
-  device-info: /v1/devices/{deviceID}
-  #组织树接口地址 GET /v1/organizations
-  organizations: /v1/organizations
 
 
 

+ 17 - 3
src/test/java/com/example/unusualsounds/UnusualSoundsApplicationTests.java

@@ -5,9 +5,7 @@ import static com.baidubce.auth.SignOptions.DEFAULT;
 import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson2.JSON;
 import com.baidubce.http.HttpMethodName;
-import com.example.unusualsounds.common.utils.BaiDuUtils;
-import com.example.unusualsounds.common.utils.DateUtils;
-import com.example.unusualsounds.common.utils.RedisUtils;
+import com.example.unusualsounds.common.utils.*;
 import com.example.unusualsounds.common.utils.UUID;
 import com.example.unusualsounds.framework.minio.MinioUtil;
 import com.example.unusualsounds.project.device.entity.vo.SoundSensorsQueryVo;
@@ -264,6 +262,22 @@ class UnusualSoundsApplicationTests {
 
     @Autowired
     private MinioUtil minioUtil;
+
+
+    @Test
+    void testokHttpPool(){
+
+
+        String dirfile = "D:\\file\\ffmpeg\\voiceTest.mp4";
+
+        File file = new File(dirfile);
+        FileToMultipartFile fileToMultipartFile = new FileToMultipartFile(file);
+
+        String upload = minioUtil.upload(fileToMultipartFile, "store/abnormal-sound/source/");
+        System.out.println("upload:"+upload);
+    }
+
+
     @Test
     void testMinioDownload(){