From 3683a9d8e0e10b98546d5224b4d4e93f6683793e Mon Sep 17 00:00:00 2001 From: Chaos Date: Sun, 20 Jul 2025 07:17:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(chaos):=20=E5=AE=9E=E7=8E=B0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=88=86=E7=89=87=E4=B8=8A=E4=BC=A0=E5=92=8C=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD-=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=A7=86=E9=A2=91=E4=B8=8A=E4=BC=A0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E3=80=81=E6=9C=8D=E5=8A=A1=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=92=8C=E5=AE=9E=E7=8E=B0=E7=B1=BB=20-=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BA=86=E8=A7=86=E9=A2=91=E5=88=86=E7=89=87=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E3=80=81=E5=90=88=E5=B9=B6=E5=92=8C=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E5=A4=84=E7=90=86=E7=9A=84=E9=80=BB=E8=BE=91=20-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=20RabbitMQ=20=E6=B6=88=E6=81=AF=E9=98=9F?= =?UTF-8?q?=E5=88=97=E9=85=8D=E7=BD=AE=E5=92=8C=E6=B6=88=E6=81=AF=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E5=99=A8=20-=E4=BC=98=E5=8C=96=E4=BA=86=20JWT=20?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E8=BF=87=E6=BB=A4=E5=99=A8=E5=92=8C=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AE=B0=E5=BD=95=20-=20=E6=96=B0=E5=A2=9E=E4=BA=86?= =?UTF-8?q?=E8=B7=A8=E5=9F=9F=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../cn/nopj/chaos_api/mapper/UserMapper.java | 8 + .../nopj/chaos_api/dto/ProcessVideoPath.java | 8 + .../nopj/chaos_api/dto/VideoTaskPayload.java | 14 ++ .../service/VideoFileUploadService.java | 45 ++++ .../service/VideoProcessingService.java | 24 ++ chaos_api_service/pom.xml | 15 +- .../cn/nopj/chaos_api/config/AppConfig.java | 13 ++ .../config/Fastjson2MessageConverter.java | 66 ++++++ .../nopj/chaos_api/config/RabbitMQConfig.java | 37 +++ .../impl/VideoFileUploadServiceImpl.java | 92 ++++++++ .../impl/VideoProcessingServiceImpl.java | 181 +++++++++++++++ chaos_api_web/pom.xml | 7 +- .../cn/nopj/chaos_api/config/CorsConfig.java | 27 +++ .../config/{sec => }/SecurityConfig.java | 33 ++- .../jwt/JwtAuthenticationTokenFilter.java | 9 +- .../chaos_api/controller/AuthController.java | 13 +- .../chaos_api/controller/VideoController.java | 73 ++++++ .../service/impl/AuthServiceImpl.java | 1 + .../cn/nopj/chaos_api/util/JwtTokenUtil.java | 5 + .../src/main/resources/application.yaml | 21 +- pom.xml | 12 +- upload.html | 218 ++++++++++++++++++ 23 files changed, 916 insertions(+), 7 deletions(-) create mode 100644 chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/ProcessVideoPath.java create mode 100644 chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/VideoTaskPayload.java create mode 100644 chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoFileUploadService.java create mode 100644 chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoProcessingService.java create mode 100644 chaos_api_service/src/main/java/cn/nopj/chaos_api/config/AppConfig.java create mode 100644 chaos_api_service/src/main/java/cn/nopj/chaos_api/config/Fastjson2MessageConverter.java create mode 100644 chaos_api_service/src/main/java/cn/nopj/chaos_api/config/RabbitMQConfig.java create mode 100644 chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoFileUploadServiceImpl.java create mode 100644 chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoProcessingServiceImpl.java create mode 100644 chaos_api_web/src/main/java/cn/nopj/chaos_api/config/CorsConfig.java rename chaos_api_web/src/main/java/cn/nopj/chaos_api/config/{sec => }/SecurityConfig.java (65%) create mode 100644 chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/VideoController.java create mode 100644 upload.html diff --git a/.gitignore b/.gitignore index a91c35d..fa53b15 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ build/ ### Mac OS ### .DS_Store /.idea/ +/chaos/ diff --git a/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java b/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java index 805d1aa..d725456 100644 --- a/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java +++ b/chaos_api_data/src/main/java/cn/nopj/chaos_api/mapper/UserMapper.java @@ -29,4 +29,12 @@ public interface UserMapper extends BaseMapper { WHERE u.username = #{username} """) List findAuthoritiesByUsername(@Param("username") String username); + + /** + * 插入用户角色关系 + * @param id 用户id + * @param i 角色id + */ + @Select("INSERT INTO t_user_role (user_id, role_id) VALUES (#{id}, #{i})") + void insertUserRole(Long id, int i); } diff --git a/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/ProcessVideoPath.java b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/ProcessVideoPath.java new file mode 100644 index 0000000..63cf2d3 --- /dev/null +++ b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/ProcessVideoPath.java @@ -0,0 +1,8 @@ +package cn.nopj.chaos_api.dto; + +import lombok.Data; + +@Data +public class ProcessVideoPath { + public String path; +} diff --git a/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/VideoTaskPayload.java b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/VideoTaskPayload.java new file mode 100644 index 0000000..c499f1c --- /dev/null +++ b/chaos_api_domain/src/main/java/cn/nopj/chaos_api/dto/VideoTaskPayload.java @@ -0,0 +1,14 @@ +package cn.nopj.chaos_api.dto; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class VideoTaskPayload implements Serializable { + private String sourceFilePath; + private String uploadId; +} \ No newline at end of file diff --git a/chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoFileUploadService.java b/chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoFileUploadService.java new file mode 100644 index 0000000..f122e47 --- /dev/null +++ b/chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoFileUploadService.java @@ -0,0 +1,45 @@ +package cn.nopj.chaos_api.service; + +import java.io.IOException; +import java.util.Map; + +public interface VideoFileUploadService { + + /** + * 初始化上传 + * @param fileName 文件名 + * @param totalSize 文件大小 + * @return 上传信息 + */ + Map initUpload(String fileName, long totalSize); + + /** + * 上传分片 + * @param uploadId 上传ID + * @param chunkIndex 分片索引 + * @param bytes 分片内容 + * @throws IOException IO 异常 + */ + void uploadChunk(String uploadId, int chunkIndex, byte[] bytes) throws IOException; + + /** + * 合并并处理上传的文件 + * @param uploadId 上传ID + * @param fileName 文件名 + * @throws IOException IO 异常 + */ + void mergeAndProcess(String uploadId, String fileName) throws IOException; + + /** + * 将处理结果推送到MQ + * @param filePath 文件路径 + * @param uploadId 上传ID + */ + void pushResultToMQ(String filePath,String uploadId); + + /** + * 将处理结果推送到MQ + * @param filePath 文件路径 + */ + void pushResultToMQ(String filePath); +} diff --git a/chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoProcessingService.java b/chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoProcessingService.java new file mode 100644 index 0000000..6e91a59 --- /dev/null +++ b/chaos_api_interface/src/main/java/cn/nopj/chaos_api/service/VideoProcessingService.java @@ -0,0 +1,24 @@ +package cn.nopj.chaos_api.service; + +import cn.nopj.chaos_api.dto.VideoTaskPayload; + +import java.io.IOException; + +public interface VideoProcessingService { + /** + * 监听视频处理任务 + * + * @param payload 视频处理任务负载 + */ + void listenForVideoTasks(VideoTaskPayload payload); + + + /** + * 处理视频 + * + * @param sourceFilePath 源视频文件路径 + * @param uploadId 上传ID + */ + void processVideo(String sourceFilePath, String uploadId); + +} \ No newline at end of file diff --git a/chaos_api_service/pom.xml b/chaos_api_service/pom.xml index c10332a..3f228a8 100644 --- a/chaos_api_service/pom.xml +++ b/chaos_api_service/pom.xml @@ -36,6 +36,19 @@ chaos_api_common ${project.version} - + + org.springframework.boot + spring-boot-starter-amqp + + + org.projectlombok + lombok + provided + + + org.springframework + spring-web + compile + \ No newline at end of file diff --git a/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/AppConfig.java b/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/AppConfig.java new file mode 100644 index 0000000..fbece42 --- /dev/null +++ b/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/AppConfig.java @@ -0,0 +1,13 @@ +package cn.nopj.chaos_api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate (); + } +} diff --git a/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/Fastjson2MessageConverter.java b/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/Fastjson2MessageConverter.java new file mode 100644 index 0000000..c6d9317 --- /dev/null +++ b/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/Fastjson2MessageConverter.java @@ -0,0 +1,66 @@ +package cn.nopj.chaos_api.config; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.util.ClassUtils; + +import java.nio.charset.StandardCharsets; + +/** + * 自定义使用 Fastjson2 的消息转换器 + */ +public class Fastjson2MessageConverter implements MessageConverter { + + public static final String DEFAULT_CHARSET = StandardCharsets.UTF_8.name(); + + /** + * 将Java对象转换为AMQP消息 + * 在这个方法中,我们将对象序列化为JSON字节数组,并在消息头中添加类型信息。 + */ + @Override + public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + if (messageProperties == null) { + messageProperties = new MessageProperties(); + } + // 设置内容类型为JSON + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON); + messageProperties.setContentEncoding(DEFAULT_CHARSET); + + // 【核心】在消息头中添加完整的类名作为类型标识 + messageProperties.setHeader("__TypeId__", object.getClass().getName()); + + byte[] bytes = JSON.toJSONBytes(object); + messageProperties.setContentLength(bytes.length); + + return new Message(bytes, messageProperties); + } + + /** + * 将AMQP消息转换为Java对象 + * 在这个方法中,我们从消息头中获取类型信息,然后用Fastjson2将消息体反序列化为指定类型的对象。 + */ + @Override + public Object fromMessage(Message message) throws MessageConversionException { + MessageProperties properties = message.getMessageProperties(); + String typeId = properties.getHeader("__TypeId__"); + + if (typeId == null) { + throw new MessageConversionException("无法转换消息:未找到 __TypeId__ 消息头"); + } + + try { + // 根据类型标识找到对应的类 + Class targetClass = ClassUtils.forName(typeId, ClassUtils.getDefaultClassLoader()); + // 使用Fastjson2进行反序列化 + return JSON.parseObject(message.getBody(), targetClass, JSONReader.Feature.SupportSmartMatch); + } catch (ClassNotFoundException e) { + throw new MessageConversionException("无法解析类型 " + typeId, e); + } catch (Exception e) { + throw new MessageConversionException("使用Fastjson2转换消息失败", e); + } + } +} diff --git a/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/RabbitMQConfig.java b/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/RabbitMQConfig.java new file mode 100644 index 0000000..45ac9db --- /dev/null +++ b/chaos_api_service/src/main/java/cn/nopj/chaos_api/config/RabbitMQConfig.java @@ -0,0 +1,37 @@ +package cn.nopj.chaos_api.config; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + public static final String QUEUE_NAME = "video.processing.queue"; + public static final String EXCHANGE_NAME = "video.direct.exchange"; + public static final String ROUTING_KEY = "video.processing.key"; + + @Bean + public Queue videoQueue() { + // durable: true, 队列持久化 + return new Queue(QUEUE_NAME, true); + } + + @Bean + public DirectExchange videoExchange() { + return new DirectExchange(EXCHANGE_NAME); + } + + @Bean + public Binding binding(Queue queue, DirectExchange exchange) { + return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY); + } + + @Bean + public MessageConverter jsonMessageConverter() { + return new Fastjson2MessageConverter(); + } +} diff --git a/chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoFileUploadServiceImpl.java b/chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoFileUploadServiceImpl.java new file mode 100644 index 0000000..19ec878 --- /dev/null +++ b/chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoFileUploadServiceImpl.java @@ -0,0 +1,92 @@ +package cn.nopj.chaos_api.service.impl; + +import cn.nopj.chaos_api.config.RabbitMQConfig; +import cn.nopj.chaos_api.dto.VideoTaskPayload; +import cn.nopj.chaos_api.service.VideoFileUploadService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.FileInputStream; +import java.io.IOException; + +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Map; +import java.util.UUID; + +@Service +@Slf4j +public class VideoFileUploadServiceImpl implements VideoFileUploadService { + + @Value("${file.upload.temp-dir}") + private String tempDir; + + @Autowired + private RabbitTemplate rabbitTemplate; // 注入RabbitMQ模板 + + private static final long CHUNK_SIZE = 5 * 1024 * 1024; // 5MB + + public Map initUpload(String fileName, long totalSize) { + String uploadId = UUID.randomUUID().toString(); + Path uploadDir = Paths.get(tempDir, uploadId); + try { + Files.createDirectories(uploadDir); + } catch (IOException e) { + throw new RuntimeException("无法创建临时上传目录", e); + } + + return Map.of("uploadId", uploadId, "chunkSize", CHUNK_SIZE); + } + + public void uploadChunk(String uploadId, int chunkIndex, byte[] bytes) throws IOException { + Path chunkPath = Paths.get(tempDir, uploadId, String.valueOf(chunkIndex)); + Files.write(chunkPath, bytes); + } + + public void mergeAndProcess(String uploadId, String fileName) throws IOException { + Path uploadDir = Paths.get(tempDir, uploadId); + Path mergedFilePath = Paths.get(tempDir, fileName); + + // 合并文件 + try (var destChannel = Files.newByteChannel(mergedFilePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { + for (int i = 0; ; i++) { + Path chunkPath = uploadDir.resolve(String.valueOf(i)); + if (!Files.exists(chunkPath)) break; + try (FileInputStream fis = new FileInputStream(chunkPath.toFile()); + FileChannel sourceChannel = fis.getChannel()) { + sourceChannel.transferTo(0, sourceChannel.size(), destChannel); + } + Files.delete(chunkPath); + } + } + Files.delete(uploadDir); + + // 发送消息到RabbitMQ,而不是直接调用Service + pushResultToMQ(mergedFilePath.toString(), uploadId); + + } + public void pushResultToMQ(String filePath,String uploadId) { + VideoTaskPayload payload = new VideoTaskPayload(filePath, uploadId); + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.ROUTING_KEY, + payload + ); + log.info("已发送视频处理结果到消息队列: {}" , payload); + } + public void pushResultToMQ(String filePath) { + VideoTaskPayload payload = new VideoTaskPayload(filePath, UUID.randomUUID().toString()); + rabbitTemplate.convertAndSend( + RabbitMQConfig.EXCHANGE_NAME, + RabbitMQConfig.ROUTING_KEY, + payload + ); + log.info("已发送视频处理结果到消息队列: {}" , payload); + } +} diff --git a/chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoProcessingServiceImpl.java b/chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoProcessingServiceImpl.java new file mode 100644 index 0000000..cdde474 --- /dev/null +++ b/chaos_api_service/src/main/java/cn/nopj/chaos_api/service/impl/VideoProcessingServiceImpl.java @@ -0,0 +1,181 @@ +package cn.nopj.chaos_api.service.impl; + +import cn.nopj.chaos_api.config.RabbitMQConfig; +import cn.nopj.chaos_api.config.AppConfig; +import cn.nopj.chaos_api.dto.VideoTaskPayload; +import cn.nopj.chaos_api.service.VideoProcessingService; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Service +@Slf4j +public class VideoProcessingServiceImpl implements VideoProcessingService { + + @Value("${file.upload.ffmpeg-path}") + private String ffmpegPath; + @Value("${file.upload.temp-dir}") + private String tempDir; + + + @Autowired + private AppConfig restTemplate; + + @RabbitListener(queues = RabbitMQConfig.QUEUE_NAME) + public void listenForVideoTasks(VideoTaskPayload payload) { + System.out.println("收到视频处理任务: " + payload); + processVideo(payload.getSourceFilePath(), payload.getUploadId()); + } + + public void processVideo(String sourceFilePath, String uploadId) { + Path sourcePath = Paths.get(sourceFilePath); + String tempOutputDirName = "hls_temp_" + uploadId; + Path tempOutputDirPath = Paths.get(tempDir, tempOutputDirName); + + try { + // 1. 创建临时HLS输出目录 + Files.createDirectories(tempOutputDirPath); + String localM3u8Path = tempOutputDirPath.resolve("playlist.m3u8").toString(); + + + String segmentFilename = tempOutputDirPath.resolve("segment%05d.jpg").toString(); + + // 2. 执行FFmpeg命令进行切片 + ProcessBuilder processBuilder = new ProcessBuilder( + ffmpegPath, "-i", sourceFilePath, "-c:v", "libx264", "-c:a", "aac", + "-hls_time", "10", "-hls_list_size", "0", "-f", "hls", "-hls_segment_filename", segmentFilename, localM3u8Path + ); + // ... (FFmpeg执行逻辑不变) ... + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; while ((line = reader.readLine()) != null) { System.out.println("FFmpeg: " + line); } + } + if (process.waitFor() != 0) { throw new RuntimeException("FFmpeg切片失败"); } + + // 3. 【核心】上传所有.ts文件到外部接口,并收集返回的URL + Map segmentUrlMap = new HashMap<>(); + + File[] segmentFiles = tempOutputDirPath.toFile().listFiles((dir, name) -> name.endsWith(".jpg")); + if (segmentFiles == null) { throw new RuntimeException("找不到生成的.jpg切片文件"); } + + + + for (File segmentFile : segmentFiles) { + String returnedUrl = uploadFileToExternalServer(segmentFile); + if (returnedUrl == null) { + throw new RuntimeException("上传文件 " + segmentFile.getName() + " 失败"); + } + segmentUrlMap.put(segmentFile.getName(), returnedUrl); + log.info("上传 {} 成功, 地址: {}", segmentFile.getName(), returnedUrl); + } + + // 4. 【核心】根据返回的URL生成新的m3u8内容 + String finalM3u8Content = createFinalM3u8Content(localM3u8Path, segmentUrlMap); + + log.info("m3u8:{}",finalM3u8Content); + // 5. 【核心】将最终的m3u8内容也上传到外部接口 + Path finalM3u8File = tempOutputDirPath.resolve("final_playlist.m3u8"); + Files.writeString(finalM3u8File, finalM3u8Content); + String finalM3u8Url = uploadFileToExternalServer(finalM3u8File.toFile()); + + log.info("视频处理完成!最终M3U8访问地址:{}",finalM3u8Url); + // 在这里,你可以将 finalM3u8Url 保存到数据库 + + } catch (IOException | InterruptedException e) { + System.err.println("视频处理失败: " + e.getMessage()); + e.printStackTrace(); + } finally { + // 6. 清理所有本地临时文件 + try { + if (Files.exists(sourcePath)) { Files.delete(sourcePath); } + if (Files.exists(tempOutputDirPath)) { + Files.walk(tempOutputDirPath) + .sorted(java.util.Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + /** + * 调用外部接口上传单个文件 + * @param file 要上传的文件 + * @return 外部接口返回的文件URL + */ + private String uploadFileToExternalServer(File file) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.add("x-auth-token","47880955-1882-44ec-a250-a97a8f31a4eb"); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("file", new FileSystemResource(file)); // "file"是常见的表单字段名,请根据您的接口修改 + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + try { + String requestUrl = "https://pinyp.vspjc.com:59789/melody/api/v1/oss/upload"; + + ResponseEntity entity = restTemplate.restTemplate().postForEntity(requestUrl, requestEntity, String.class); + if (entity.getStatusCode() != HttpStatus.OK){ + log.error("请求失败: {}", entity); + throw new RuntimeException("请求失败"); + } + + JSONObject jsonObject = JSONObject.parse(entity.getBody()); + + if (Objects.requireNonNull(jsonObject).getInteger("code") != 12200){ + log.error("上传失败: {}", jsonObject); + throw new RuntimeException("上传失败,请检查接口是否失效"); + } + + String data = jsonObject.getString("data"); + //data = https://qny-imimg.uuvuem.cn/df53/wx/20250719/8cc4f34394fb49ec90b1316ca9e26b86.jpg@,@image/jpeg@,@qiniu + String[] split = data.split("@"); + String url = split[0]; + log.info("上传成功: {}", url); + return url; + + } catch (Exception e) { + log.error("上传文件失败: {}", e.getMessage()); + return null; + } + + } + /** + * 读取本地m3u8文件,并用远程URL替换ts文件名 + */ + private String createFinalM3u8Content(String localM3u8Path, Map tsUrlMap) throws IOException { + StringBuilder newContent = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(localM3u8Path))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.endsWith(".jpg")) { + String remoteUrl = tsUrlMap.get(line.trim()); + if (remoteUrl != null) { + newContent.append(remoteUrl).append("\n"); + } + } else { + newContent.append(line).append("\n"); + } + } + } + return newContent.toString(); + } +} diff --git a/chaos_api_web/pom.xml b/chaos_api_web/pom.xml index fca8464..5c7fc41 100644 --- a/chaos_api_web/pom.xml +++ b/chaos_api_web/pom.xml @@ -63,6 +63,11 @@ ch.qos.logback logback-classic + + org.projectlombok + lombok + provided + @@ -71,7 +76,7 @@ org.springframework.boot spring-boot-maven-plugin - 2.7.0 + diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/CorsConfig.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/CorsConfig.java new file mode 100644 index 0000000..816961a --- /dev/null +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/CorsConfig.java @@ -0,0 +1,27 @@ +package cn.nopj.chaos_api.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") // 匹配所有路径 + .allowedOrigins( + "http://localhost:63342", // 你的前端运行的地址 + "http://localhost:8080", // 其他可能的前端地址 + "http://127.0.0.1:5500", // 另一个常见的本地开发地址 + "null" + ) // 允许的来源 + // 请根据你的前端实际部署地址修改或增加这里的源 + // 在开发环境可以加 "null" 来处理从文件系统打开的页面 + // 生产环境请务必替换为明确的域名或IP,避免使用"*"或"null" + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法 + .allowedHeaders("*") // 允许所有请求头 + .allowCredentials(true) // 允许发送认证信息(如 Cookies, HTTP 认证及客户端 SSL 证书) + .maxAge(3600); // 预检请求的缓存时间(秒) + } +} \ No newline at end of file diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/sec/SecurityConfig.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/SecurityConfig.java similarity index 65% rename from chaos_api_web/src/main/java/cn/nopj/chaos_api/config/sec/SecurityConfig.java rename to chaos_api_web/src/main/java/cn/nopj/chaos_api/config/SecurityConfig.java index 7566fec..fc54807 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/sec/SecurityConfig.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/SecurityConfig.java @@ -1,10 +1,13 @@ -package cn.nopj.chaos_api.config.sec; +package cn.nopj.chaos_api.config; import cn.nopj.chaos_api.config.jwt.JwtAuthenticationTokenFilter; +import cn.nopj.chaos_api.config.sec.RestAuthenticationEntryPoint; +import cn.nopj.chaos_api.config.sec.RestfulAccessDeniedHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -13,9 +16,16 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; @Configuration @EnableWebSecurity +@EnableMethodSecurity public class SecurityConfig { private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; @@ -51,9 +61,30 @@ public class SecurityConfig { .exceptionHandling(e -> e .authenticationEntryPoint(restAuthenticationEntryPoint) .accessDeniedHandler(restfulAccessDeniedHandler)) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) ; http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } + + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList( + "http://localhost:63342", // 你的前端运行的地址 + "http://localhost:8080", // 其他可能的前端地址 + "http://127.0.0.1:5500", // 另一个常见的本地开发地址 + "null" // 如果是从文件系统直接打开HTML文件,Origin 会是 "null"。仅用于开发! + )); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); // 注意这里是 Long 类型 + source.registerCorsConfiguration("/**", configuration); // 对所有路径生效 + return source; + } } diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/jwt/JwtAuthenticationTokenFilter.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/jwt/JwtAuthenticationTokenFilter.java index 543773a..17f98fb 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/jwt/JwtAuthenticationTokenFilter.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/config/jwt/JwtAuthenticationTokenFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -21,6 +22,7 @@ import java.io.IOException; * JWT 登录授权过滤器 */ @Component +@Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired @@ -36,10 +38,15 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + log.info("JWT 登录授权过滤器"); + String authHeader = request.getHeader(this.tokenHeader); + log.info("authHeader: {}", authHeader); if (authHeader != null && authHeader.startsWith(this.tokenHead)) { - String authToken = authHeader.substring(this.tokenHead.length()); + String authToken = authHeader.substring(this.tokenHead.length()+1); + log.info("authToken={}", authToken); String username = jwtTokenUtil.getUsernameFromToken(authToken); + log.info("username={}", username); // 如果 Token 中有用户名但上下文中没有,说明是首次登录 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/AuthController.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/AuthController.java index edbcb1d..7b691ec 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/AuthController.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/AuthController.java @@ -23,6 +23,12 @@ public class AuthController { @Value("${jwt.tokenHead}") private String tokenHead; + /** + * 注册 + * + * @param registerRequest 注册信息 + * @return 注册结果 + */ @PostMapping("/register") public ApiResult register(@RequestBody RegisterRequest registerRequest) { if (authService.register(registerRequest) != null) { @@ -30,7 +36,12 @@ public class AuthController { } return ApiResult.failed("用户名已存在"); } - + /** + * 登录 + * + * @param loginRequest 登录信息 + * @return 登录结果 + */ @PostMapping("/login") public ApiResult login(@RequestBody LoginRequest loginRequest) { String token = authService.login(loginRequest.getUsername(), loginRequest.getPassword()); diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/VideoController.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/VideoController.java new file mode 100644 index 0000000..6967097 --- /dev/null +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/controller/VideoController.java @@ -0,0 +1,73 @@ +package cn.nopj.chaos_api.controller; + + +import cn.nopj.chaos_api.dto.ProcessVideoPath; +import cn.nopj.chaos_api.model.ApiResult; +import cn.nopj.chaos_api.service.VideoFileUploadService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Map; + +@RestController +@RequestMapping("/api/video") +@Slf4j +public class VideoController { + + @Autowired + private VideoFileUploadService videoFileUploadService; + + /** + * 初始化分片上传任务 + * @param fileInfo 包含文件名(fileName)和总大小(totalSize) + * @return 返回 uploadId 和每个分片的大小 + */ + @PostMapping("/init") + public ApiResult initUpload(@RequestBody Map fileInfo) { + log.info("初始化上传任务: {}", fileInfo); + String fileName = fileInfo.get("fileName").toString(); + long totalSize = Long.parseLong(fileInfo.get("totalSize").toString()); + return ApiResult.success(videoFileUploadService.initUpload(fileName, totalSize)); + } + + /** + * 上传分片 + * @param chunk 分片文件 + * @param uploadId 上传任务ID + * @param chunkIndex 分片序号 (从0开始) + */ + @PostMapping("/chunk") + + public ApiResult uploadChunk(@RequestParam("chunk") MultipartFile chunk, + @RequestParam("uploadId") String uploadId, + @RequestParam("chunkIndex") int chunkIndex) throws IOException { + videoFileUploadService.uploadChunk(uploadId, chunkIndex, chunk.getBytes()); + return ApiResult.success("分片 " + chunkIndex + " 上传成功"); + } + + /** + * 合并文件并触发异步处理 + * @param params 包含 uploadId 和 fileName + */ + @PostMapping("/merge") + public ApiResult mergeAndProcess(@RequestBody Map params) throws IOException { + String uploadId = params.get("uploadId"); + String fileName = params.get("fileName"); + videoFileUploadService.mergeAndProcess(uploadId, fileName); + return ApiResult.success("文件上传成功,已加入后台处理队列。"); + } + + /** + * 处理视频 + * @param processVideoPath 待处理的视频路径 + */ + @PostMapping("/process") + public ApiResult processVideo(@RequestBody ProcessVideoPath processVideoPath) throws IOException{ + videoFileUploadService.pushResultToMQ(processVideoPath.getPath()); + return ApiResult.success("视频处理任务已加入队列"); + } +} diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java index 903bedc..ef8bdfa 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/service/impl/AuthServiceImpl.java @@ -42,6 +42,7 @@ public class AuthServiceImpl implements AuthService { user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); userMapper.insert(user); // 你可以在这里为新用户分配默认角色 + userMapper.insertUserRole(user.getId(), 2); return user; } diff --git a/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java b/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java index 5dda5fc..416935c 100644 --- a/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java +++ b/chaos_api_web/src/main/java/cn/nopj/chaos_api/util/JwtTokenUtil.java @@ -5,6 +5,7 @@ import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.beans.factory.annotation.Value; @@ -13,7 +14,9 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Date; +@Slf4j @Component + public class JwtTokenUtil { @@ -38,7 +41,9 @@ public class JwtTokenUtil { */ public String getUsernameFromToken(String token) { try { + log.info("token:{};验证状态:{}",token,verifier.verify( token)); DecodedJWT jwt = verifier.verify(token); + log.info("从 Token 中获取用户名:{}", jwt.getSubject()); return jwt.getSubject(); } catch (Exception e) { return null; diff --git a/chaos_api_web/src/main/resources/application.yaml b/chaos_api_web/src/main/resources/application.yaml index 174f5b6..a10e2c0 100644 --- a/chaos_api_web/src/main/resources/application.yaml +++ b/chaos_api_web/src/main/resources/application.yaml @@ -15,6 +15,16 @@ spring: min-idle: 5 max-active: 20 max-wait: 60000 + rabbitmq: + host: 10.91.3.24 + port: 5672 + username: chaos + password: zx123456.. + servlet: + multipart: + max-file-size: 100GB + max-request-size: 100GB + mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml @@ -26,8 +36,17 @@ mybatis-plus: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + + + jwt: tokenHeader: Authorization tokenHead: Chaos secret: zHANgcHao@1995!20250506 - expiration: 604800 \ No newline at end of file + expiration: 604800 + + +file: + upload: + temp-dir: ./chaos/upload + ffmpeg-path: C:\Users\Chaos\AppData\Local\Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg-7.1.1-full_build\bin\ffmpeg.exe \ No newline at end of file diff --git a/pom.xml b/pom.xml index bec3636..4d15dff 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,17 @@ logback-classic 1.5.18 - + + org.springframework.boot + spring-boot-starter-amqp + 3.5.3 + + + + org.springframework + spring-web + 6.2.9 + \ No newline at end of file diff --git a/upload.html b/upload.html new file mode 100644 index 0000000..0f7313b --- /dev/null +++ b/upload.html @@ -0,0 +1,218 @@ + + + + + + 分片上传示例 + + + + +
+

视频分片上传

+ + +
未选择文件
+ +
+
0%
+
+

+
+ + + + + +