From cdb6bfa134554d9901fb3b624577b1d2b207b2da Mon Sep 17 00:00:00 2001
From: peng <peng.com>
Date: 星期四, 19 三月 2026 16:21:58 +0800
Subject: [PATCH] 视频查看

---
 jyz-base-start/pom.xml                                                     |    7 
 system-quick-start/src/main/resources/application.yml                      |    1 
 jyz-base-start/src/main/java/com/tievd/jyz/controller/VideoController.java |  685 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 693 insertions(+), 0 deletions(-)

diff --git a/jyz-base-start/pom.xml b/jyz-base-start/pom.xml
index b02d328..5ba8f38 100644
--- a/jyz-base-start/pom.xml
+++ b/jyz-base-start/pom.xml
@@ -162,6 +162,13 @@
             <artifactId>spring-boot-starter-test</artifactId>
         </dependency>
 
+        <!-- JavaCV 瑙嗛澶勭悊 -->
+        <dependency>
+            <groupId>org.bytedeco</groupId>
+            <artifactId>javacv-platform</artifactId>
+            <version>1.5.9</version>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git a/jyz-base-start/src/main/java/com/tievd/jyz/controller/VideoController.java b/jyz-base-start/src/main/java/com/tievd/jyz/controller/VideoController.java
new file mode 100644
index 0000000..d3badba
--- /dev/null
+++ b/jyz-base-start/src/main/java/com/tievd/jyz/controller/VideoController.java
@@ -0,0 +1,685 @@
+package com.tievd.jyz.controller;
+
+import com.tievd.cube.commons.base.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.bytedeco.ffmpeg.global.avcodec;
+import org.bytedeco.ffmpeg.global.avutil;
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.bytedeco.javacv.FFmpegFrameRecorder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.Map;
+
+/**
+ * 鏈湴瑙嗛鎾斁鎺у埗鍣�
+ * 鐢ㄤ簬棣栭〉瑙嗛鎾斁娴嬭瘯
+ */
+@Slf4j
+@RestController
+@RequestMapping("/jyz/video")
+@Tag(name = "鏈湴瑙嗛鎾斁鎺ュ彛")
+public class VideoController {
+
+    @Value("${video.local.path:E:\\yclCode\\DEV_ZHJYZ\\DEV_ZHJYZ\\video-test}")
+    private String videoBasePath;
+    
+    private final Map<String, AtomicInteger> convertingTasks = new ConcurrentHashMap<>();
+    private final int MAX_CONCURRENT_CONVERSIONS = 2;
+    
+    @GetMapping("/list")
+    @Operation(summary = "鑾峰彇鏈湴瑙嗛鍒楄〃")
+    public Result<?> getVideoList() {
+        try {
+            File videoDir = new File(videoBasePath);
+            if (!videoDir.exists() || !videoDir.isDirectory()) {
+                return Result.error("瑙嗛鐩綍涓嶅瓨鍦�: " + videoBasePath);
+            }
+            
+            List<String> videoFiles = Arrays.stream(videoDir.listFiles())
+                    .filter(file -> file.isFile() && file.getName().toLowerCase().endsWith(".mp4"))
+                    .map(File::getName)
+                    .collect(Collectors.toList());
+            
+            return Result.ok(videoFiles);
+        } catch (Exception e) {
+            log.error("鑾峰彇瑙嗛鍒楄〃澶辫触", e);
+            return Result.error("鑾峰彇瑙嗛鍒楄〃澶辫触: " + e.getMessage());
+        }
+    }
+
+    @GetMapping("/random")
+    @Operation(summary = "鑾峰彇闅忔満瑙嗛瀹屾暣鍦板潃")
+    public Result<?> getRandomVideo(HttpServletRequest request) {
+        try {
+            File videoDir = new File(videoBasePath);
+            if (!videoDir.exists() || !videoDir.isDirectory()) {
+                return Result.error("瑙嗛鐩綍涓嶅瓨鍦�: " + videoBasePath);
+            }
+            
+            List<File> videoFiles = Arrays.stream(videoDir.listFiles())
+                    .filter(file -> file.isFile() && file.getName().toLowerCase().endsWith(".mp4"))
+                    .collect(Collectors.toList());
+            
+            if (videoFiles.isEmpty()) {
+                return Result.error("鐩綍涓病鏈夎棰戞枃浠�");
+            }
+            
+            int randomIndex = (int) (Math.random() * videoFiles.size());
+            File selectedVideo = videoFiles.get(randomIndex);
+            
+            String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
+            String mp4Url = baseUrl + "/cube/jyz/video/stream/" + selectedVideo.getName();
+            log.info("=== 杩斿洖 mp4 URL ===\n{}", mp4Url);
+            
+            return Result.ok(mp4Url);
+        } catch (Exception e) {
+            log.error("鑾峰彇闅忔満瑙嗛澶辫触", e);
+            return Result.error("鑾峰彇闅忔満瑙嗛澶辫触: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/stream/{videoName}")
+    @Operation(summary = "杩斿洖瑙嗛鏂囦欢")
+    public void streamVideo(@PathVariable String videoName, HttpServletRequest request, HttpServletResponse response) {
+        try {
+            if (!videoName.toLowerCase().endsWith(".mp4")) {
+                videoName = videoName + ".mp4";
+            }
+            File videoFile = new File(videoBasePath, videoName);
+            if (isConvertedVideoName(videoName)) {
+                if (isBrowserPlayableVideoFile(videoFile)) {
+                    serveStandardVideo(videoFile, request, response);
+                    return;
+                }
+                String sourceName = videoName.replaceAll("_converted\\.mp4$", ".mp4");
+                File sourceFile = new File(videoBasePath, sourceName);
+                if (!sourceFile.exists()) {
+                    response.sendError(HttpServletResponse.SC_NOT_FOUND);
+                    return;
+                }
+                boolean converted = ensureConvertedVideo(sourceFile, videoFile, sourceName);
+                if (converted && isBrowserPlayableVideoFile(videoFile)) {
+                    serveStandardVideo(videoFile, request, response);
+                    return;
+                }
+                serveStandardVideo(sourceFile, request, response);
+                return;
+            }
+            if (!videoFile.exists()) {
+                response.sendError(HttpServletResponse.SC_NOT_FOUND);
+                return;
+            }
+            
+            byte[] header = new byte[16];
+            try (FileInputStream fis = new FileInputStream(videoFile)) {
+                fis.read(header);
+            }
+            
+            boolean isHikvisionFormat = header[0] == 0x49 && header[1] == 0x4D && header[2] == 0x4B && header[3] == 0x48;
+            String convertedName = videoName.replaceAll("\\.mp4$", "_converted.mp4");
+            File convertedFile = new File(videoBasePath, convertedName);
+            if (isBrowserPlayableVideoFile(convertedFile)) {
+                serveStandardVideo(convertedFile, request, response);
+                return;
+            }
+            
+            if (isHikvisionFormat || needsBrowserCompatibleConversion(videoFile)) {
+                boolean converted = ensureConvertedVideo(videoFile, convertedFile, videoName);
+                if (converted && isBrowserPlayableVideoFile(convertedFile)) {
+                    serveStandardVideo(convertedFile, request, response);
+                    return;
+                }
+                serveStandardVideo(videoFile, request, response);
+                return;
+            }
+            
+            serveStandardVideo(videoFile, request, response);
+        } catch (Exception e) {
+            log.error("瑙嗛鎾斁澶辫触: {}", videoName, e);
+            try {
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            } catch (IOException ignored) {}
+        }
+    }
+
+    private boolean ensureConvertedVideo(File inputFile, File outputFile, String taskId) {
+        if (isBrowserPlayableVideoFile(outputFile)) {
+            return true;
+        }
+        AtomicInteger marker = new AtomicInteger(1);
+        AtomicInteger currentTask = convertingTasks.putIfAbsent(taskId, marker);
+        if (currentTask != null) {
+            return waitForConvertedVideo(outputFile, 20000);
+        }
+        try {
+            int currentCount = convertingTasks.values().stream().mapToInt(AtomicInteger::get).sum();
+            if (currentCount > MAX_CONCURRENT_CONVERSIONS) {
+                return waitForConvertedVideo(outputFile, 20000);
+            }
+            return convertVideoWithJavaCV(inputFile, outputFile);
+        } finally {
+            convertingTasks.remove(taskId);
+        }
+    }
+
+    private boolean isValidVideoFile(File file) {
+        return file != null && file.exists() && file.isFile() && file.length() > 0;
+    }
+
+    private boolean isConvertedVideoName(String videoName) {
+        return videoName != null && videoName.toLowerCase(Locale.ROOT).endsWith("_converted.mp4");
+    }
+
+    private boolean waitForConvertedVideo(File outputFile, long waitMs) {
+        long deadline = System.currentTimeMillis() + waitMs;
+        while (System.currentTimeMillis() < deadline) {
+            if (isBrowserPlayableVideoFile(outputFile)) {
+                return true;
+            }
+            try {
+                Thread.sleep(200);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return isBrowserPlayableVideoFile(outputFile);
+            }
+        }
+        return isBrowserPlayableVideoFile(outputFile);
+    }
+
+    private boolean needsBrowserCompatibleConversion(File videoFile) {
+        FFmpegFrameGrabber grabber = null;
+        try {
+            grabber = new FFmpegFrameGrabber(videoFile);
+            grabber.start();
+            String videoCodec = normalizeCodecName(grabber.getVideoCodecName());
+            String audioCodec = normalizeCodecName(grabber.getAudioCodecName());
+            boolean videoCompatible = "h264".equals(videoCodec) || "avc1".equals(videoCodec);
+            boolean audioCompatible = audioCodec.isEmpty() || "aac".equals(audioCodec) || "mp3".equals(audioCodec) || "mp2".equals(audioCodec);
+            log.info("瑙嗛缂栫爜妫�娴� {} -> videoCodec={}, audioCodec={}", videoFile.getName(), videoCodec, audioCodec);
+            if (videoCodec.isEmpty()) {
+                return true;
+            }
+            return !(videoCompatible && audioCompatible);
+        } catch (Exception e) {
+            log.warn("瑙嗛缂栫爜妫�娴嬪け璐ワ紝杩涘叆杞崲娴佺▼: {}", videoFile.getName(), e);
+            return true;
+        } finally {
+            try {
+                if (grabber != null) {
+                    grabber.stop();
+                    grabber.release();
+                }
+            } catch (Exception e) {
+                log.warn("閲婃斁缂栫爜妫�娴嬭祫婧愬け璐�: {}", videoFile.getName(), e);
+            }
+        }
+    }
+
+    private boolean isPlayableVideoFile(File file) {
+        if (!isValidVideoFile(file)) {
+            return false;
+        }
+        FFmpegFrameGrabber grabber = null;
+        try {
+            grabber = new FFmpegFrameGrabber(file);
+            grabber.start();
+            return grabber.getImageWidth() > 0 || grabber.getLengthInFrames() > 0 || grabber.getLengthInTime() > 0;
+        } catch (Exception e) {
+            return false;
+        } finally {
+            try {
+                if (grabber != null) {
+                    grabber.stop();
+                    grabber.release();
+                }
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    private boolean isBrowserPlayableVideoFile(File file) {
+        if (!isPlayableVideoFile(file)) {
+            return false;
+        }
+        FFmpegFrameGrabber grabber = null;
+        try {
+            grabber = new FFmpegFrameGrabber(file);
+            grabber.start();
+            String format = normalizeCodecName(grabber.getFormat());
+            String videoCodec = normalizeCodecName(grabber.getVideoCodecName());
+            String audioCodec = normalizeCodecName(grabber.getAudioCodecName());
+            boolean containerCompatible = format.contains("mp4") || format.contains("mov");
+            boolean videoCompatible = "h264".equals(videoCodec) || "avc1".equals(videoCodec);
+            boolean audioCompatible = audioCodec.isEmpty() || "aac".equals(audioCodec) || "mp3".equals(audioCodec) || "mp2".equals(audioCodec);
+            boolean durationValid = grabber.getLengthInTime() > 0 || grabber.getLengthInFrames() > 0;
+            return containerCompatible && videoCompatible && audioCompatible && durationValid;
+        } catch (Exception e) {
+            return false;
+        } finally {
+            try {
+                if (grabber != null) {
+                    grabber.stop();
+                    grabber.release();
+                }
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    private String normalizeCodecName(String codecName) {
+        return codecName == null ? "" : codecName.trim().toLowerCase(Locale.ROOT);
+    }
+
+    private boolean convertVideoWithJavaCV(File inputFile, File outputFile) {
+        FFmpegFrameGrabber grabber = null;
+        FFmpegFrameRecorder recorder = null;
+        try {
+            log.info("寮�濮嬩娇鐢� JavaCV 杞崲瑙嗛: {} -> {}", inputFile.getName(), outputFile.getName());
+            if (outputFile.exists() && !outputFile.delete()) {
+                log.warn("鍒犻櫎鏃х殑杞崲鏂囦欢澶辫触: {}", outputFile.getAbsolutePath());
+            }
+            grabber = new FFmpegFrameGrabber(inputFile);
+            grabber.start();
+            int width = Math.max(grabber.getImageWidth(), 2);
+            int height = Math.max(grabber.getImageHeight(), 2);
+            if (width % 2 != 0) {
+                width -= 1;
+            }
+            if (height % 2 != 0) {
+                height -= 1;
+            }
+            width = Math.max(width, 2);
+            height = Math.max(height, 2);
+            int audioChannels = Math.max(grabber.getAudioChannels(), 0);
+            recorder = new FFmpegFrameRecorder(outputFile, width, height, audioChannels);
+            recorder.setFormat("mp4");
+            recorder.setOption("movflags", "faststart");
+            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
+            recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
+            recorder.setVideoOption("preset", "medium");
+            recorder.setVideoOption("profile", "main");
+            recorder.setVideoBitrate(grabber.getVideoBitrate() > 0 ? grabber.getVideoBitrate() : 2_000_000);
+            if (audioChannels > 0) {
+                recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
+                recorder.setAudioBitrate(128000);
+                recorder.setSampleRate(grabber.getSampleRate() > 0 ? grabber.getSampleRate() : 44100);
+                recorder.setAudioChannels(audioChannels);
+            } else {
+                recorder.setAudioChannels(0);
+            }
+            double frameRate = grabber.getFrameRate();
+            if (frameRate > 0 && frameRate < 120) {
+                recorder.setFrameRate(frameRate);
+            } else {
+                recorder.setFrameRate(25);
+            }
+            recorder.start();
+            org.bytedeco.javacv.Frame frame;
+            while ((frame = grabber.grabFrame()) != null) {
+                if (frame.timestamp > 0) {
+                    recorder.setTimestamp(frame.timestamp);
+                }
+                recorder.record(frame);
+            }
+            recorder.stop();
+            grabber.stop();
+            if (isBrowserPlayableVideoFile(outputFile)) {
+                log.info("瑙嗛杞崲鎴愬姛: {} -> {}", inputFile.getName(), outputFile.getName());
+                return true;
+            }
+            log.error("瑙嗛杞崲澶辫触: 杈撳嚭鏂囦欢涓嶅彲鎾斁 {}", outputFile.getAbsolutePath());
+            if (outputFile.exists() && !outputFile.delete()) {
+                log.warn("鍒犻櫎澶辫触鐨勮浆鎹㈡枃浠跺け璐�: {}", outputFile.getAbsolutePath());
+                }
+            return false;
+        } catch (Exception e) {
+            log.error("瑙嗛杞崲寮傚父: {}", inputFile.getName(), e);
+            if (outputFile.exists()) {
+                outputFile.delete();
+            }
+            return false;
+        } finally {
+            try {
+                if (recorder != null) {
+                    recorder.release();
+                }
+                if (grabber != null) {
+                    grabber.release();
+                }
+            } catch (Exception e) {
+                log.warn("閲婃斁杞爜璧勬簮寮傚父: {}", inputFile.getName(), e);
+            }
+        }
+    }
+    
+    private void serveStandardVideo(File videoFile, HttpServletRequest request, HttpServletResponse response) throws IOException {
+        long fileSize = videoFile.length();
+        
+        response.setHeader("Access-Control-Allow-Origin", "*");
+        response.setContentType("video/mp4");
+        response.setHeader("Content-Length", String.valueOf(fileSize));
+        response.setHeader("Accept-Ranges", "bytes");
+        response.setHeader("Cache-Control", "max-age=86400");
+        
+        if (request != null) {
+            String range = request.getHeader("Range");
+            if (range != null && range.startsWith("bytes=")) {
+                String[] ranges = range.substring(6).split("-");
+                long start = Long.parseLong(ranges[0]);
+                long end = ranges.length > 1 && !ranges[1].isEmpty() ? Long.parseLong(ranges[1]) : fileSize - 1;
+                long contentLength = end - start + 1;
+                
+                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+                response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
+                response.setHeader("Content-Length", String.valueOf(contentLength));
+                
+                try (FileInputStream fis = new FileInputStream(videoFile)) {
+                    fis.skip(start);
+                    byte[] buffer = new byte[8192];
+                    long remaining = contentLength;
+                    int bytesRead;
+                    while (remaining > 0 && (bytesRead = fis.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) {
+                        response.getOutputStream().write(buffer, 0, bytesRead);
+                        remaining -= bytesRead;
+                    }
+                }
+                response.getOutputStream().flush();
+                return;
+            }
+        }
+        
+        Files.copy(videoFile.toPath(), response.getOutputStream());
+        response.getOutputStream().flush();
+    }
+
+    @GetMapping("/hls/{videoName}")
+    @Operation(summary = "瀹炴椂杞爜涓篐LS娴佹挱鏀�")
+    public Result<?> streamHls(@PathVariable String videoName, HttpServletRequest request) {
+        try {
+            if (!videoName.matches("^[a-zA-Z0-9_.]+$")) {
+                return Result.error("鏃犳晥鐨勬枃浠跺悕");
+            }
+            
+            String baseName = videoName.replaceAll("\\.mp4$", "");
+            File inputFile = new File(videoBasePath, videoName);
+            File hlsDir = new File(videoBasePath, baseName + "_hls");
+            
+            if (!inputFile.exists()) {
+                return Result.error("瑙嗛鏂囦欢涓嶅瓨鍦�: " + videoName);
+            }
+            
+            hlsDir.mkdirs();
+            File m3u8File = new File(hlsDir, "playlist.m3u8");
+            
+            String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
+            String m3u8Url = baseUrl + "/cube/jyz/video/hls/" + baseName + "/playlist.m3u8";
+            String tsUrl = baseUrl + "/cube/jyz/video/hls/" + baseName + "/";
+            
+            String m3u8Content = "#EXTM3U\n" +
+                    "#EXT-X-VERSION:3\n" +
+                    "#EXT-X-TARGETDURATION:10\n" +
+                    "#EXT-X-MEDIA-SEQUENCE:0\n" +
+                    "#EXTINF:10.0,\n" +
+                    "segment0.ts\n" +
+                    "#EXT-X-ENDLIST\n";
+            
+            Files.write(m3u8File.toPath(), m3u8Content.getBytes());
+            
+            for (int i = 0; i < 10; i++) {
+                File tsFile = new File(hlsDir, "segment" + i + ".ts");
+                ProcessBuilder pb = new ProcessBuilder(
+                    "ffmpeg", "-y", "-i", inputFile.getAbsolutePath(),
+                    "-ss", String.valueOf(i * 10),
+                    "-t", "10",
+                    "-c:v", "libx264", "-crf", "23", "-preset", "fast",
+                    "-c:a", "aac", "-b:a", "128k",
+                    "-f", "mpegts", tsFile.getAbsolutePath()
+                );
+                pb.redirectErrorStream(true);
+                Process process = pb.start();
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                    while (reader.readLine() != null) {}
+                }
+                process.waitFor();
+            }
+            
+            log.info("HLS 杞爜瀹屾垚: {}", m3u8File.getAbsolutePath());
+            
+            Map<String, String> result = new HashMap<>();
+            result.put("m3u8Url", m3u8Url);
+            result.put("type", "hls");
+            return Result.ok(result);
+            
+        } catch (Exception e) {
+            log.error("HLS 杞爜澶辫触: {}", videoName, e);
+            return Result.error("HLS 杞爜澶辫触: " + e.getMessage());
+        }
+    }
+
+    @GetMapping("/hls/{baseName}/**")
+    @Operation(summary = "HLS鏂囦欢鏈嶅姟")
+    public void serveHlsFile(@PathVariable String baseName, HttpServletRequest request, HttpServletResponse response) {
+        String pathInfo = request.getRequestURI();
+        String fileName = pathInfo.substring(pathInfo.lastIndexOf('/') + 1);
+        
+        File hlsDir = new File(videoBasePath, baseName + "_hls");
+        File file = new File(hlsDir, fileName);
+        
+        if (!file.exists()) {
+            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        try {
+            String contentType = fileName.endsWith(".m3u8") ? "application/vnd.apple.mpegurl" : "video/MP2T";
+            response.setContentType(contentType);
+            response.setHeader("Access-Control-Allow-Origin", "*");
+            response.setHeader("Cache-Control", "no-cache");
+            
+            Files.copy(file.toPath(), response.getOutputStream());
+            response.getOutputStream().flush();
+        } catch (Exception e) {
+            log.error("HLS 鏂囦欢鏈嶅姟澶辫触: {}", fileName, e);
+        }
+    }
+
+    @GetMapping("/download/{videoName}")
+    @Operation(summary = "涓嬭浇瑙嗛鏂囦欢")
+    public ResponseEntity<Resource> downloadVideo(@PathVariable String videoName) {
+        try {
+            if (!videoName.matches("^[a-zA-Z0-9_.]+$")) {
+                return ResponseEntity.badRequest().build();
+            }
+            
+            if (!videoName.toLowerCase().endsWith(".mp4")) {
+                videoName = videoName + ".mp4";
+            }
+            
+            Path videoPath = Paths.get(videoBasePath, videoName);
+            File videoFile = videoPath.toFile();
+            
+            if (!videoFile.exists()) {
+                return ResponseEntity.notFound().build();
+            }
+            
+            Resource resource = new FileSystemResource(videoFile);
+            
+            return ResponseEntity.ok()
+                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + videoName + "\"")
+                    .body(resource);
+                    
+        } catch (Exception e) {
+            log.error("瑙嗛涓嬭浇澶辫触: {}", videoName, e);
+            return ResponseEntity.internalServerError().build();
+        }
+    }
+
+    @GetMapping("/convert/{videoName}")
+    @Operation(summary = "杞崲娴峰悍瑙嗛涓烘爣鍑哅P4")
+    public Result<?> convertVideo(@PathVariable String videoName) {
+        try {
+            if (!videoName.matches("^[a-zA-Z0-9_.]+$")) {
+                return Result.error("鏃犳晥鐨勬枃浠跺悕");
+            }
+            
+            String outputName = videoName.replaceAll("\\.mp4$", "") + "_converted.mp4";
+            File inputFile = new File(videoBasePath, videoName);
+            File outputFile = new File(videoBasePath, outputName);
+            
+            if (!inputFile.exists()) {
+                return Result.error("婧愯棰戞枃浠朵笉瀛樺湪: " + videoName);
+            }
+            
+            if (outputFile.exists()) {
+                return Result.ok("/cube/jyz/video/stream/" + outputName);
+            }
+            
+            ProcessBuilder pb = new ProcessBuilder(
+                "ffmpeg", "-y", "-i", inputFile.getAbsolutePath(),
+                "-c:v", "libx264", "-crf", "23", "-preset", "medium",
+                "-c:a", "aac", "-b:a", "128k",
+                outputFile.getAbsolutePath()
+            );
+            pb.redirectErrorStream(true);
+            
+            Process process = pb.start();
+            StringBuilder output = new StringBuilder();
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    output.append(line).append("\n");
+                }
+            }
+            
+            int exitCode = process.waitFor();
+            log.info("FFmpeg 杞崲杈撳嚭: {}", output);
+            
+            if (exitCode == 0 && outputFile.exists()) {
+                log.info("瑙嗛杞崲鎴愬姛: {} -> {}", videoName, outputName);
+                return Result.ok("/cube/jyz/video/stream/" + outputName);
+            } else {
+                log.error("瑙嗛杞崲澶辫触, exitCode: {}, output: {}", exitCode, output);
+                return Result.error("瑙嗛杞崲澶辫触: " + output.substring(Math.max(0, output.length() - 500)));
+            }
+            
+        } catch (Exception e) {
+            log.error("瑙嗛杞崲寮傚父: {}", videoName, e);
+            return Result.error("瑙嗛杞崲寮傚父: " + e.getMessage());
+        }
+    }
+
+    @GetMapping("/convert/all")
+    @Operation(summary = "鎵归噺杞崲鎵�鏈夋捣搴疯棰�")
+    public Result<?> convertAllVideos() {
+        try {
+            File videoDir = new File(videoBasePath);
+            File[] videoFiles = videoDir.listFiles((dir, name) -> 
+                name.toLowerCase().endsWith(".mp4") && !name.contains("_converted"));
+            
+            if (videoFiles == null || videoFiles.length == 0) {
+                return Result.ok("娌℃湁闇�瑕佽浆鎹㈢殑瑙嗛鏂囦欢");
+            }
+            
+            int successCount = 0;
+            int failCount = 0;
+            List<String> successList = new ArrayList<>();
+            List<String> failList = new ArrayList<>();
+            
+            for (File inputFile : videoFiles) {
+                String outputName = inputFile.getName().replaceAll("\\.mp4$", "") + "_converted.mp4";
+                File outputFile = new File(videoBasePath, outputName);
+                
+                if (outputFile.exists()) {
+                    successCount++;
+                    successList.add(outputName);
+                    continue;
+                }
+                
+                try {
+                    ProcessBuilder pb = new ProcessBuilder(
+                        "ffmpeg", "-y", "-i", inputFile.getAbsolutePath(),
+                        "-c:v", "libx264", "-crf", "23", "-preset", "medium",
+                        "-c:a", "aac", "-b:a", "128k",
+                        outputFile.getAbsolutePath()
+                    );
+                    pb.redirectErrorStream(true);
+                    
+                    Process process = pb.start();
+                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                        while (reader.readLine() != null) {}
+                    }
+                    
+                    int exitCode = process.waitFor();
+                    if (exitCode == 0 && outputFile.exists()) {
+                        successCount++;
+                        successList.add(outputName);
+                        log.info("杞崲鎴愬姛: {}", inputFile.getName());
+                    } else {
+                        failCount++;
+                        failList.add(inputFile.getName());
+                        log.error("杞崲澶辫触: {}", inputFile.getName());
+                    }
+                } catch (Exception e) {
+                    failCount++;
+                    failList.add(inputFile.getName() + "(" + e.getMessage() + ")");
+                    log.error("杞崲寮傚父: {}", inputFile.getName(), e);
+                }
+            }
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("successCount", successCount);
+            result.put("failCount", failCount);
+            result.put("successList", successList);
+            result.put("failList", failList);
+            return Result.ok(result);
+            
+        } catch (Exception e) {
+            log.error("鎵归噺杞崲寮傚父", e);
+            return Result.error("鎵归噺杞崲寮傚父: " + e.getMessage());
+        }
+    }
+
+    @GetMapping("/check-ffmpeg")
+    @Operation(summary = "妫�鏌Fmpeg鏄惁鍙敤")
+    public Result<?> checkFFmpeg() {
+        try {
+            ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-version");
+            pb.redirectErrorStream(true);
+            Process process = pb.start();
+            StringBuilder output = new StringBuilder();
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                String line = reader.readLine();
+                if (line != null) {
+                    output.append(line);
+                }
+            }
+            int exitCode = process.waitFor();
+            if (exitCode == 0) {
+                return Result.ok("FFmpeg 宸插畨瑁�: " + output.toString().split("\n")[0]);
+            } else {
+                return Result.error("FFmpeg 鏈畨瑁呮垨涓嶅彲鐢�");
+            }
+        } catch (Exception e) {
+            return Result.error("FFmpeg 妫�鏌ュけ璐�: " + e.getMessage());
+        }
+    }
+}
diff --git a/system-quick-start/src/main/resources/application.yml b/system-quick-start/src/main/resources/application.yml
index 6cff0f6..e373f77 100644
--- a/system-quick-start/src/main/resources/application.yml
+++ b/system-quick-start/src/main/resources/application.yml
@@ -86,6 +86,7 @@
           - /demo/**
           - /sysLogo/**
           - /jyz/sysInfo/localInfo
+          - /jyz/video/**
   mybatis-plus:
     plugin:
       enable-tenant: false

--
Gitblit v1.8.0