| New file |
| | |
| | | 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 = "实时转码为HLS流播放") |
| | | 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 = "转换海康视频为标准MP4") |
| | | 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 = "检查FFmpeg是否可用") |
| | | 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()); |
| | | } |
| | | } |
| | | } |