|
| 1 | +package cn.programcx.foxnaserver.api.media; |
| 2 | + |
| 3 | +import cn.programcx.foxnaserver.common.Result; |
| 4 | +import cn.programcx.foxnaserver.dto.media.JobStatus; |
| 5 | +import cn.programcx.foxnaserver.entity.TranscodeJob; |
| 6 | +import cn.programcx.foxnaserver.service.media.TranscodeJobService; |
| 7 | +import cn.programcx.foxnaserver.service.media.VideoFingerprintService; |
| 8 | +import cn.programcx.foxnaserver.util.JwtUtil; |
| 9 | +import com.baomidou.mybatisplus.core.metadata.IPage; |
| 10 | +import io.swagger.v3.oas.annotations.Operation; |
| 11 | +import io.swagger.v3.oas.annotations.Parameter; |
| 12 | +import io.swagger.v3.oas.annotations.tags.Tag; |
| 13 | +import lombok.Data; |
| 14 | +import lombok.RequiredArgsConstructor; |
| 15 | +import lombok.extern.slf4j.Slf4j; |
| 16 | +import org.springframework.data.redis.core.RedisTemplate; |
| 17 | +import org.springframework.web.bind.annotation.*; |
| 18 | + |
| 19 | +import java.util.HashMap; |
| 20 | +import java.util.List; |
| 21 | +import java.util.Map; |
| 22 | + |
| 23 | +/** |
| 24 | + * 转码任务管理控制器 |
| 25 | + */ |
| 26 | +@Slf4j |
| 27 | +@RestController |
| 28 | +@RequestMapping("/api/transcode/jobs") |
| 29 | +@RequiredArgsConstructor |
| 30 | +@Tag(name = "TranscodeJobController", description = "转码任务管理接口") |
| 31 | +public class TranscodeJobController { |
| 32 | + |
| 33 | + private final TranscodeJobService transcodeJobService; |
| 34 | + private final VideoFingerprintService fingerprintService; |
| 35 | + private final RedisTemplate<String, Object> redisTemplate; |
| 36 | + |
| 37 | + /** |
| 38 | + * 获取当前用户ID (UUID string) |
| 39 | + */ |
| 40 | + private String getCurrentUserId() { |
| 41 | + String uuid = JwtUtil.getCurrentUuid(); |
| 42 | + if (uuid == null || uuid.isEmpty()) { |
| 43 | + throw new RuntimeException("未登录或登录已过期"); |
| 44 | + } |
| 45 | + return uuid; |
| 46 | + } |
| 47 | + |
| 48 | + /** |
| 49 | + * 创建视频转码任务 |
| 50 | + */ |
| 51 | + @Operation(summary = "创建视频转码任务", description = "创建一个新的视频转码任务") |
| 52 | + @PostMapping("/create") |
| 53 | + public Result<TranscodeJob> createJob(@RequestBody CreateJobRequest request) { |
| 54 | + try { |
| 55 | + String userId = getCurrentUserId(); |
| 56 | + |
| 57 | + // 如果没有提供指纹,生成一个 |
| 58 | + String fingerprint = request.getFingerprint(); |
| 59 | + if (fingerprint == null || fingerprint.isEmpty()) { |
| 60 | + fingerprint = fingerprintService.generateFingerprint(request.getVideoPath()); |
| 61 | + } |
| 62 | + |
| 63 | + // 检查是否已有相同的完成任务 |
| 64 | + TranscodeJob existingJob = transcodeJobService.findCompletedByFingerprint(fingerprint); |
| 65 | + if (existingJob != null && existingJob.getCreatorId().equals(userId)) { |
| 66 | + log.info("用户 [{}] 请求转码已存在的视频 [{}],复用任务 [{}]", |
| 67 | + userId, request.getVideoPath(), existingJob.getJobId()); |
| 68 | + return Result.success(existingJob, "该视频已转码完成,直接复用"); |
| 69 | + } |
| 70 | + |
| 71 | + TranscodeJob job = transcodeJobService.createVideoJob( |
| 72 | + userId, |
| 73 | + request.getVideoPath(), |
| 74 | + request.getAudioTrackIndex(), |
| 75 | + request.getSubtitleTrackIndex(), |
| 76 | + request.isImmediate(), |
| 77 | + fingerprint |
| 78 | + ); |
| 79 | + |
| 80 | + return Result.success(job); |
| 81 | + } catch (Exception e) { |
| 82 | + log.error("创建转码任务失败: {}", e.getMessage()); |
| 83 | + return Result.error(500, "创建转码任务失败: " + e.getMessage()); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * 检查指纹状态 |
| 89 | + */ |
| 90 | + @Operation(summary = "检查视频指纹", description = "检查视频是否已转码过,避免重复转码") |
| 91 | + @GetMapping("/check-fingerprint") |
| 92 | + public Result<FingerprintCheckResult> checkFingerprint( |
| 93 | + @Parameter(description = "视频文件路径") @RequestParam String videoPath, |
| 94 | + @Parameter(description = "指纹(可选,不传则后端计算)") @RequestParam(required = false) String fingerprint) { |
| 95 | + try { |
| 96 | + String userId = getCurrentUserId(); |
| 97 | + |
| 98 | + if (fingerprint == null || fingerprint.isEmpty()) { |
| 99 | + fingerprint = fingerprintService.generateFingerprint(videoPath); |
| 100 | + } |
| 101 | + |
| 102 | + // 检查用户是否已有该视频的任务 |
| 103 | + TranscodeJob userJob = transcodeJobService.findCompletedByFingerprint(fingerprint); |
| 104 | + if (userJob != null && userJob.getCreatorId().equals(userId)) { |
| 105 | + return Result.success(new FingerprintCheckResult(true, userJob.getJobId(), |
| 106 | + fingerprint, userJob.getHlsPath(), userJob.getStatus()), "已存在可用的转码结果"); |
| 107 | + } |
| 108 | + |
| 109 | + // 检查是否有进行中的任务 |
| 110 | + final String fp = fingerprint; // lambda需要final变量 |
| 111 | + TranscodeJob processingJob = transcodeJobService.listAllJobsByCreator(userId).stream() |
| 112 | + .filter(j -> fp.equals(j.getFingerprint())) |
| 113 | + .filter(j -> TranscodeJob.Status.PENDING.name().equals(j.getStatus()) || |
| 114 | + TranscodeJob.Status.PROCESSING.name().equals(j.getStatus())) |
| 115 | + .findFirst() |
| 116 | + .orElse(null); |
| 117 | + |
| 118 | + if (processingJob != null) { |
| 119 | + return Result.success(new FingerprintCheckResult(false, processingJob.getJobId(), |
| 120 | + fingerprint, null, processingJob.getStatus()), "转码任务进行中"); |
| 121 | + } |
| 122 | + |
| 123 | + return Result.success(new FingerprintCheckResult(false, null, fingerprint, null, null), |
| 124 | + "未找到转码记录,需要新建任务"); |
| 125 | + |
| 126 | + } catch (Exception e) { |
| 127 | + log.error("检查指纹失败: {}", e.getMessage()); |
| 128 | + return Result.error(500, "检查指纹失败: " + e.getMessage()); |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * 查询任务详情 |
| 134 | + */ |
| 135 | + @Operation(summary = "查询任务详情", description = "根据任务ID查询转码任务详情") |
| 136 | + @GetMapping("/{jobId}") |
| 137 | + public Result<TranscodeJob> getJobDetail( |
| 138 | + @Parameter(description = "任务ID") @PathVariable String jobId) { |
| 139 | + try { |
| 140 | + String userId = getCurrentUserId(); |
| 141 | + TranscodeJob job = transcodeJobService.getByJobIdAndCreator(jobId, userId); |
| 142 | + |
| 143 | + if (job == null) { |
| 144 | + return Result.error(404, "任务不存在或无权限访问"); |
| 145 | + } |
| 146 | + JobStatus jobStatus = (JobStatus) redisTemplate.opsForValue().get("job:" + jobId); |
| 147 | + if (jobStatus != null) { |
| 148 | + job.setProgress(jobStatus.getProgress()); |
| 149 | + job.setTotalStages(jobStatus.getStages()); |
| 150 | + job.setCurrentStage(jobStatus.getCurrentStage()); |
| 151 | + } |
| 152 | +// |
| 153 | + return Result.success(job); |
| 154 | + } catch (Exception e) { |
| 155 | + log.error("查询任务详情失败: {}", e.getMessage()); |
| 156 | + return Result.error(500, "查询任务详情失败: " + e.getMessage()); |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + /** |
| 161 | + * 分页查询任务列表 |
| 162 | + */ |
| 163 | + @Operation(summary = "查询任务列表", description = "分页查询当前用户的转码任务列表") |
| 164 | + @GetMapping("/list") |
| 165 | + public Result<IPage<TranscodeJob>> listJobs( |
| 166 | + @Parameter(description = "页码,默认1") @RequestParam(defaultValue = "1") int page, |
| 167 | + @Parameter(description = "每页大小,默认10") @RequestParam(defaultValue = "10") int size) { |
| 168 | + try { |
| 169 | + String userId = getCurrentUserId(); |
| 170 | + IPage<TranscodeJob> jobs = transcodeJobService.listJobsByCreator(userId, page, size); |
| 171 | + // 从Redis获取任务状态 |
| 172 | + for (TranscodeJob job : jobs.getRecords()) { |
| 173 | + JobStatus jobStatus = (JobStatus) redisTemplate.opsForValue().get("job:" + job.getJobId()); |
| 174 | + if (jobStatus != null) { |
| 175 | + job.setProgress(jobStatus.getProgress()); |
| 176 | + job.setTotalStages(jobStatus.getStages()); |
| 177 | + job.setCurrentStage(jobStatus.getCurrentStage()); |
| 178 | + } |
| 179 | + } |
| 180 | + return Result.success(jobs); |
| 181 | + } catch (Exception e) { |
| 182 | + log.error("查询任务列表失败: {}", e.getMessage()); |
| 183 | + return Result.error(500, "查询任务列表失败: " + e.getMessage()); |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * 查询所有任务(不分页) |
| 189 | + */ |
| 190 | + @Operation(summary = "查询所有任务", description = "查询当前用户的所有转码任务") |
| 191 | + @GetMapping("/list-all") |
| 192 | + public Result<List<TranscodeJob>> listAllJobs() { |
| 193 | + try { |
| 194 | + String userId = getCurrentUserId(); |
| 195 | + List<TranscodeJob> jobs = transcodeJobService.listAllJobsByCreator(userId); |
| 196 | + for (TranscodeJob job : jobs) { |
| 197 | + JobStatus jobStatus = (JobStatus) redisTemplate.opsForValue().get("job:" + job.getJobId()); |
| 198 | + if (jobStatus != null) { |
| 199 | + job.setProgress(jobStatus.getProgress()); |
| 200 | + job.setTotalStages(jobStatus.getStages()); |
| 201 | + job.setCurrentStage(jobStatus.getCurrentStage()); |
| 202 | + } |
| 203 | + } |
| 204 | + return Result.success(jobs); |
| 205 | + } catch (Exception e) { |
| 206 | + log.error("查询任务列表失败: {}", e.getMessage()); |
| 207 | + return Result.error(500, "查询任务列表失败: " + e.getMessage()); |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * 停止任务 |
| 213 | + */ |
| 214 | + @Operation(summary = "停止任务", description = "停止进行中的转码任务") |
| 215 | + @PostMapping("/{jobId}/stop") |
| 216 | + public Result<String> stopJob( |
| 217 | + @Parameter(description = "任务ID") @PathVariable String jobId) { |
| 218 | + try { |
| 219 | + String userId = getCurrentUserId(); |
| 220 | + boolean success = transcodeJobService.stopJob(jobId, userId); |
| 221 | + if (success) { |
| 222 | + return Result.success(null, "任务已停止"); |
| 223 | + } else { |
| 224 | + return Result.error(400, "任务不存在或无法停止"); |
| 225 | + } |
| 226 | + } catch (Exception e) { |
| 227 | + log.error("停止任务失败: {}", e.getMessage()); |
| 228 | + return Result.error(500, "停止任务失败: " + e.getMessage()); |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + /** |
| 233 | + * 重试任务 |
| 234 | + */ |
| 235 | + @Operation(summary = "重试任务", description = "重试失败或已取消的转码任务") |
| 236 | + @PostMapping("/{jobId}/retry") |
| 237 | + public Result<String> retryJob( |
| 238 | + @Parameter(description = "任务ID") @PathVariable String jobId) { |
| 239 | + try { |
| 240 | + String userId = getCurrentUserId(); |
| 241 | + boolean success = transcodeJobService.retryJob(jobId, userId); |
| 242 | + if (success) { |
| 243 | + return Result.success(null, "任务已重新提交"); |
| 244 | + } else { |
| 245 | + return Result.error(400, "任务不存在或无法重试"); |
| 246 | + } |
| 247 | + } catch (Exception e) { |
| 248 | + log.error("重试任务失败: {}", e.getMessage()); |
| 249 | + return Result.error(500, "重试任务失败: " + e.getMessage()); |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + /** |
| 254 | + * 删除任务 |
| 255 | + */ |
| 256 | + @Operation(summary = "删除任务", description = "删除转码任务及其相关文件") |
| 257 | + @DeleteMapping("/{jobId}") |
| 258 | + public Result<String> deleteJob( |
| 259 | + @Parameter(description = "任务ID") @PathVariable String jobId) { |
| 260 | + try { |
| 261 | + String userId = getCurrentUserId(); |
| 262 | + boolean success = transcodeJobService.deleteJob(jobId, userId); |
| 263 | + if (success) { |
| 264 | + return Result.success(null, "任务已删除"); |
| 265 | + } else { |
| 266 | + return Result.error(404, "任务不存在或无权限删除"); |
| 267 | + } |
| 268 | + } catch (Exception e) { |
| 269 | + log.error("删除任务失败: {}", e.getMessage()); |
| 270 | + return Result.error(500, "删除任务失败: " + e.getMessage()); |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + /** |
| 275 | + * 批量删除所有任务 |
| 276 | + */ |
| 277 | + @Operation(summary = "批量删除所有任务", description = "删除当前用户的所有转码任务") |
| 278 | + @DeleteMapping("/delete-all") |
| 279 | + public Result<String> deleteAllJobs() { |
| 280 | + try { |
| 281 | + String userId = getCurrentUserId(); |
| 282 | + int count = transcodeJobService.deleteAllJobsByCreator(userId); |
| 283 | + return Result.success(null, "已删除 " + count + " 个任务"); |
| 284 | + } catch (Exception e) { |
| 285 | + log.error("批量删除任务失败: {}", e.getMessage()); |
| 286 | + return Result.error(500, "批量删除任务失败: " + e.getMessage()); |
| 287 | + } |
| 288 | + } |
| 289 | + |
| 290 | + /** |
| 291 | + * 获取任务统计 |
| 292 | + */ |
| 293 | + @Operation(summary = "获取任务统计", description = "获取当前用户的转码任务统计信息") |
| 294 | + @GetMapping("/statistics") |
| 295 | + public Result<Map<String, Object>> getStatistics() { |
| 296 | + try { |
| 297 | + String userId = getCurrentUserId(); |
| 298 | + Map<String, Object> stats = transcodeJobService.getStatistics(userId); |
| 299 | + return Result.success(stats); |
| 300 | + } catch (Exception e) { |
| 301 | + log.error("获取任务统计失败: {}", e.getMessage()); |
| 302 | + return Result.error(500, "获取任务统计失败: " + e.getMessage()); |
| 303 | + } |
| 304 | + } |
| 305 | + |
| 306 | + /** |
| 307 | + * 获取任务实时进度(轮询) |
| 308 | + */ |
| 309 | + @Operation(summary = "获取任务实时进度", description = "轮询获取任务的实时进度") |
| 310 | + @GetMapping("/{jobId}/progress") |
| 311 | + public Result<Map<String,String>> getJobProgress( |
| 312 | + @Parameter(description = "任务ID") @PathVariable String jobId) { |
| 313 | + try { |
| 314 | + JobStatus jobStatus = (JobStatus) redisTemplate.opsForValue().get("job:" + jobId); |
| 315 | + if (jobStatus == null) { |
| 316 | + return Result.error(404, "任务不存在或进度未更新"); |
| 317 | + } |
| 318 | + Map<String,String> progress = new HashMap<>(); |
| 319 | + progress.put("progress", String.format("%.2f", jobStatus.getProgress())); |
| 320 | + progress.put("totalStages", String.valueOf(jobStatus.getStages())); |
| 321 | + progress.put("currentStage", String.valueOf(jobStatus.getCurrentStage())); |
| 322 | + progress.put("state", jobStatus.getState().name()); |
| 323 | + return Result.success(progress); |
| 324 | + } catch (Exception e) { |
| 325 | + log.error("获取任务进度失败: {}", e.getMessage()); |
| 326 | + return Result.error(500, "获取任务进度失败: " + e.getMessage()); |
| 327 | + } |
| 328 | + } |
| 329 | + |
| 330 | + |
| 331 | + // ==================== 请求/响应 DTO ==================== |
| 332 | + |
| 333 | + @Data |
| 334 | + public static class CreateJobRequest { |
| 335 | + private String videoPath; |
| 336 | + private Integer audioTrackIndex = 0; |
| 337 | + private Integer subtitleTrackIndex = -1; |
| 338 | + private boolean immediate = false; |
| 339 | + private String fingerprint; // 可选 |
| 340 | + } |
| 341 | + |
| 342 | + @Data |
| 343 | + public static class FingerprintCheckResult { |
| 344 | + private boolean existed; // 是否已存在可用结果 |
| 345 | + private String jobId; // 任务ID |
| 346 | + private String fingerprint; // 指纹 |
| 347 | + private String hlsPath; // HLS播放路径 |
| 348 | + private String status; // 任务状态 |
| 349 | + |
| 350 | + public FingerprintCheckResult(boolean existed, String jobId, String fingerprint, |
| 351 | + String hlsPath, String status) { |
| 352 | + this.existed = existed; |
| 353 | + this.jobId = jobId; |
| 354 | + this.fingerprint = fingerprint; |
| 355 | + this.hlsPath = hlsPath; |
| 356 | + this.status = status; |
| 357 | + } |
| 358 | + } |
| 359 | +} |
0 commit comments