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 = "实时转码为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());
|
}
|
}
|
}
|