diff --git a/gb28181-wvp-proxy-api/src/main/java/cn/skcks/docking/gb28181/wvp/api/video/RecordController.java b/gb28181-wvp-proxy-api/src/main/java/cn/skcks/docking/gb28181/wvp/api/video/RecordController.java new file mode 100644 index 0000000..57fe2d7 --- /dev/null +++ b/gb28181-wvp-proxy-api/src/main/java/cn/skcks/docking/gb28181/wvp/api/video/RecordController.java @@ -0,0 +1,23 @@ +package cn.skcks.docking.gb28181.wvp.api.video; + +import cn.skcks.docking.gb28181.wvp.service.video.RecordService; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RequiredArgsConstructor +@RestController +@RequestMapping("/video/record") +public class RecordController { + private final RecordService recordService; + + @GetMapping + public void record(HttpServletResponse response, @RequestParam String url,@RequestParam long time){ + recordService.record(response,url,time); + } +} diff --git a/gb28181-wvp-proxy-service/pom.xml b/gb28181-wvp-proxy-service/pom.xml index 760c670..84e8fdb 100644 --- a/gb28181-wvp-proxy-service/pom.xml +++ b/gb28181-wvp-proxy-service/pom.xml @@ -39,6 +39,11 @@ spring-boot-starter-web + + jakarta.servlet + jakarta.servlet-api + + org.springframework.cloud spring-cloud-starter-openfeign diff --git a/gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/executor/DefaultVideoExecutor.java b/gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/executor/DefaultVideoExecutor.java new file mode 100644 index 0000000..e077b31 --- /dev/null +++ b/gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/executor/DefaultVideoExecutor.java @@ -0,0 +1,55 @@ +package cn.skcks.docking.gb28181.wvp.executor; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.*; + +@Configuration +@Order(1) +@EnableAsync(proxyTargetClass = true) +public class DefaultVideoExecutor { + /** + * cpu 核心数 + */ + public static final int CPU_NUM = Runtime.getRuntime().availableProcessors(); + /** + * 最大线程数 + */ + public static final int MAX_POOL_SIZE = CPU_NUM * 2; + /** + * 允许线程空闲时间(单位:默认为秒) + */ + private static final int KEEP_ALIVE_TIME = 20; + /** + * 队列长度 + */ + public static final int TASK_NUM = 5; + /** + * 线程名称(前缀) + */ + public static final String THREAD_NAME_PREFIX = "video-executor"; + + public static final String EXECUTOR_BEAN_NAME = "videoTaskExecutor"; + + + @Bean(EXECUTOR_BEAN_NAME) + public Executor videoTaskExecutor(){ + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(CPU_NUM); + executor.setMaxPoolSize(MAX_POOL_SIZE); + executor.setQueueCapacity(TASK_NUM); + executor.setKeepAliveSeconds(KEEP_ALIVE_TIME); + executor.setThreadNamePrefix(THREAD_NAME_PREFIX); + + // 线程池对拒绝任务的处理策略 + // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 初始化 + executor.initialize(); + return executor; + } +} diff --git a/gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/service/video/RecordService.java b/gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/service/video/RecordService.java new file mode 100644 index 0000000..b28fe74 --- /dev/null +++ b/gb28181-wvp-proxy-service/src/main/java/cn/skcks/docking/gb28181/wvp/service/video/RecordService.java @@ -0,0 +1,75 @@ +package cn.skcks.docking.gb28181.wvp.service.video; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.bytedeco.ffmpeg.global.avcodec; +import org.bytedeco.ffmpeg.global.avutil; +import org.bytedeco.javacv.*; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.file.Path; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +@Service +public class RecordService { + + @SneakyThrows + public void record(HttpServletResponse response, String url, long timeout){ + response.reset(); + // response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + response.setContentType("video/mp4"); + + Path tmp = Path.of(System.getProperty("java.io.tmpdir"), IdUtil.getSnowflakeNextIdStr()).toAbsolutePath(); + File file = new File(tmp + ".mp4"); + log.info("创建文件 {}, {}", file, file.createNewFile()); + + log.info("url {}", url); + try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url)) { + grabber.start(); + try(FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(file, grabber.getImageWidth(), grabber.getImageHeight(),grabber.getAudioChannels())){ + recorder.start(); + log.info("开始录像"); + log.info("{}", file); + recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); + recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); //视频源数据yuv + recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); //设置音频压缩方式 + recorder.setFormat("mp4"); + recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); //解码线程数 + + ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + AtomicBoolean record = new AtomicBoolean(true); + scheduledExecutorService.schedule(()->{ + log.info("到达超时时间, 结束录制"); + record.set(false); + }, timeout, TimeUnit.SECONDS); + + try { + Frame frame; + while (record.get() && (frame = grabber.grab()) != null) { + recorder.record(frame); + } + grabber.stop(); + grabber.release(); + recorder.stop(); + } catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) { + throw new RuntimeException(e); + } + } + } finally { + log.info("结束录制"); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + OutputStream outputStream = new BufferedOutputStream(response.getOutputStream()); + IoUtil.copy(inputStream, outputStream); + log.info("临时文件 {} 写入 响应 完成", file); + log.info("删除临时文件 {} {}", file, file.delete()); + } + } +}