From a0b2af0203b00e1dfcb8b5977dd5019491533c26 Mon Sep 17 00:00:00 2001
From: zxl <763096477@qq.com>
Date: 星期五, 20 三月 2026 15:33:41 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/show-demo' into show_demo
---
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 | 758 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 766 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..16f91f9
--- /dev/null
+++ b/jyz-base-start/src/main/java/com/tievd/jyz/controller/VideoController.java
@@ -0,0 +1,758 @@
+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;
+
+ @Value("${image.local.path:E:\\yclCode\\DEV_ZHJYZ\\DEV_ZHJYZ\\images}")
+ private String imageBasePath;
+
+ 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("/random-images")
+ @Operation(summary = "鑾峰彇闅忔満鍥剧墖鍦板潃鍒楄〃")
+ public Result<?> getRandomImages(@RequestParam(defaultValue = "20") Integer count, HttpServletRequest request) {
+ try {
+ int safeCount = Math.max(1, Math.min(count, 200));
+ File imageDir = new File(imageBasePath);
+ if (!imageDir.exists() || !imageDir.isDirectory()) {
+ return Result.error("鍥剧墖鐩綍涓嶅瓨鍦�: " + imageBasePath);
+ }
+ File[] imageFiles = imageDir.listFiles(file -> file.isFile() && isImageFile(file.getName()));
+ if (imageFiles == null || imageFiles.length == 0) {
+ return Result.error("鐩綍涓病鏈夊浘鐗囨枃浠�");
+ }
+ String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), "");
+ Random random = new Random();
+ List<String> imageUrls = new ArrayList<>();
+ for (int i = 0; i < safeCount; i++) {
+ File selectedImage = imageFiles[random.nextInt(imageFiles.length)];
+ imageUrls.add(baseUrl + "/cube/jyz/video/image/" + selectedImage.getName());
+ }
+ return Result.ok(imageUrls);
+ } catch (Exception e) {
+ log.error("鑾峰彇闅忔満鍥剧墖澶辫触", e);
+ return Result.error("鑾峰彇闅忔満鍥剧墖澶辫触: " + e.getMessage());
+ }
+ }
+
+ @GetMapping("/image/{imageName:.+}")
+ @Operation(summary = "杩斿洖鏈湴鍥剧墖鏂囦欢")
+ public void streamImage(@PathVariable String imageName, HttpServletResponse response) {
+ try {
+ if (!imageName.matches("^[a-zA-Z0-9._-]+$")) {
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+ File imageDir = new File(imageBasePath);
+ File imageFile = new File(imageDir, imageName);
+ String basePath = imageDir.getCanonicalPath() + File.separator;
+ String imagePath = imageFile.getCanonicalPath();
+ if (!imagePath.startsWith(basePath) || !imageFile.exists() || !imageFile.isFile()) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ String contentType = Files.probeContentType(imageFile.toPath());
+ if (contentType == null || !contentType.startsWith("image/")) {
+ contentType = MediaType.IMAGE_JPEG_VALUE;
+ }
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("Cache-Control", "max-age=3600");
+ response.setContentType(contentType);
+ response.setHeader("Content-Length", String.valueOf(imageFile.length()));
+ Files.copy(imageFile.toPath(), response.getOutputStream());
+ response.getOutputStream().flush();
+ } catch (Exception e) {
+ log.error("鍥剧墖璇诲彇澶辫触: {}", imageName, e);
+ try {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ @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 isImageFile(String fileName) {
+ if (fileName == null) {
+ return false;
+ }
+ String lowerName = fileName.toLowerCase(Locale.ROOT);
+ return lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || lowerName.endsWith(".png") || lowerName.endsWith(".webp") || lowerName.endsWith(".gif") || lowerName.endsWith(".bmp");
+ }
+
+ 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