using AlibabaCloud.SDK.Vod20170321; using AlibabaCloud.SDK.Vod20170321.Models; using Aliyun.OSS; using Microsoft.Extensions.DependencyInjection; using SqlSugar.IOC; using System; using System.IO; using System.Text; using System.Threading.Tasks; using VideoAnalysisCore.Model; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Collections.Generic; using System.Security.Cryptography; using Yitter.IdGenerator; namespace VideoAnalysisCore.Common.Expand { public static class TidySlideExpand { public static void AddTidySlideExpand(this IServiceCollection services) { services.AddSingleton(); services.AddTransient>(); } } public class TidySlideHandle { private readonly Client _vodClient; private readonly Repository _videoTaskDB; private readonly Repository _tidySlideTaskResultDB; private readonly RedisManager _redisManager; private readonly OssClient _ossClient; // 使用系统统一注入的 OSS Client private readonly TidySlideWorkflowManager _workflowManager; // 注入工作流管理器 public TidySlideHandle(Client vodClient, Repository videoTaskDB, Repository tidySlideTaskResultDB, RedisManager redisManager, OssClient ossClient, TidySlideWorkflowManager workflowManager) { _vodClient = vodClient; _videoTaskDB = videoTaskDB; _tidySlideTaskResultDB = tidySlideTaskResultDB; _redisManager = redisManager; _ossClient = ossClient; _workflowManager = workflowManager; } public async Task RunAsync(string task) { var taskId = long.Parse(task); var localPath = task.LocalPath(); var m3u8Path = Path.Combine(localPath, "out.m3u8"); if (!File.Exists(m3u8Path)) { await _workflowManager.AddTaskLog(task, "未找到 m3u8 文件,无法进行切片上传"); throw new FileNotFoundException("M3U8文件未找到", m3u8Path); } // 获取所有切片文件 (out*.ts) var tsFiles = Directory.GetFiles(localPath, "out*.ts"); if (tsFiles.Length == 0) { await _workflowManager.AddTaskLog(task, "未找到 ts 切片文件"); throw new FileNotFoundException("TS切片文件未找到"); } var title = $"Task_{taskId}_{DateTime.Now:yyyyMMddHHmmss}"; await _workflowManager.AddTaskLog(task, "正在获取VOD上传凭证..."); // 1. 获取上传凭证和地址 // 注意:VOD上传m3u8时,FileName必须以 .m3u8 结尾 var request = new CreateUploadVideoRequest { Title = title, FileName = "out.m3u8", // 必须是 m3u8 文件名 Description = "视频分析_PPT清洗 ", Tags = "PPT清洗", // 可选:设置标签 CateId = 1000709090, }; var response = await _vodClient.CreateUploadVideoAsync(request); if (response.Body == null || string.IsNullOrEmpty(response.Body.UploadAddress) || string.IsNullOrEmpty(response.Body.UploadAuth)) { throw new Exception($"获取上传凭证失败: RequestId={response.Body?.RequestId}"); } var videoId = response.Body.VideoId; var uploadAddressStr = response.Body.UploadAddress; var uploadAuthStr = response.Body.UploadAuth; await _workflowManager.AddTaskLog(task, $"获取凭证成功,VideoId: {videoId}"); // 2. 解析凭证 (Base64 -> JSON) var addressJson = JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(uploadAddressStr))); var authJson = JObject.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(uploadAuthStr))); var endpoint = addressJson["Endpoint"]?.ToString(); var bucket = addressJson["Bucket"]?.ToString(); // 这是 VOD 分配的 m3u8 存储路径,例如 "sv/243d.../out.m3u8" var objectName = addressJson["FileName"]?.ToString(); var accessKeyId = authJson["AccessKeyId"]?.ToString(); var accessKeySecret = authJson["AccessKeySecret"]?.ToString(); var securityToken = authJson["SecurityToken"]?.ToString(); if (string.IsNullOrEmpty(endpoint) || string.IsNullOrEmpty(bucket) || string.IsNullOrEmpty(objectName)) throw new Exception("解析上传地址失败"); // 修正 Endpoint 格式 (如果缺少协议头) if (!endpoint.StartsWith("http")) endpoint = "https://" + endpoint; // 3. 构造 OSS 客户端 (使用临时凭证) var ossClient = new OssClient(endpoint, accessKeyId, accessKeySecret, securityToken); // 4. 确定 OSS 目录前缀 // VOD 返回的 objectName 是完整的文件路径,我们需要提取目录部分来存放 .ts 文件 // 例如: objectName = "sv/5903240e-19544975a64/out.m3u8" // 则 prefix = "sv/5903240e-19544975a64/" var ossPrefix = objectName.Substring(0, objectName.LastIndexOf('/') + 1); await _workflowManager.AddTaskLog(task, $"开始上传文件到 VOD OSS (Bucket: {bucket}, Prefix: {ossPrefix})..."); try { // A. 上传所有 TS 切片 await _workflowManager.AddTaskLog(task, $"开始上传 TS 切片 (共 {tsFiles.Length} 个)..."); for (int i = 0; i < tsFiles.Length; i++) { var tsFile = tsFiles[i]; var fileName = Path.GetFileName(tsFile); var tsObjectKey = ossPrefix + fileName; var tsRetryCount = 0; var tsMaxRetries = 10; while (true) { try { using var fs = File.OpenRead(tsFile); ossClient.PutObject(bucket, tsObjectKey, fs); break; // Upload successful, break retry loop } catch (Exception ex) { tsRetryCount++; if (tsRetryCount >= tsMaxRetries) { await _workflowManager.AddTaskLog(task, $"上传 TS 切片 {fileName} 失败,已重试 {tsMaxRetries} 次: {ex.Message}"); throw; // Re-throw exception to stop the process } else { await _workflowManager.AddTaskLog(task, $"上传 TS 切片 {fileName} 失败 (第 {tsRetryCount} 次重试): {ex.Message},1秒后重试..."); await Task.Delay(1000); } } } // 更新上传进度 if (i % 5 == 0) // 每5个文件更新一次进度 { var progress = Math.Round((double)i / tsFiles.Length * 100, 1); _workflowManager.SetTaskProgress(taskId, $"Upload->{progress}%"); } } // B. 上传 m3u8 索引文件 // 必须使用 VOD 指定的 objectName await _workflowManager.AddTaskLog(task, "开始上传 m3u8 索引文件..."); var m3u8RetryCount = 0; var m3u8MaxRetries = 3; while (true) { try { using (var fs = File.OpenRead(m3u8Path)) { ossClient.PutObject(bucket, objectName, fs); } break; // Upload successful } catch (Exception ex) { m3u8RetryCount++; if (m3u8RetryCount >= m3u8MaxRetries) { await _workflowManager.AddTaskLog(task, $"上传 m3u8 文件失败,已重试 {m3u8MaxRetries} 次: {ex.Message}"); throw; } else { await _workflowManager.AddTaskLog(task, $"上传 m3u8 文件失败 (第 {m3u8RetryCount} 次重试): {ex.Message},1秒后重试..."); await Task.Delay(1000); } } } await _workflowManager.AddTaskLog(task, "上传成功"); // 5. 更新数据库 // 对于 VOD 托管视频,我们主要存储 VideoId (TagId),播放地址通常由前端调用 VOD 接口获取 // 或者我们可以尝试获取播放地址存入 MediaUrl await _tidySlideTaskResultDB.InsertAsync(new TidySlideTaskResult() { Id = YitIdHelper.NextId(), VideoTaskId = long.Parse(task), VideoId = videoId, }); } catch (Exception ex) { await _workflowManager.AddTaskLog(task, $"上传 VOD OSS 异常: {ex.Message}"); // 如果已获取 VideoId 但上传失败,则删除 VOD 记录 if (!string.IsNullOrEmpty(videoId)) { try { await _workflowManager.AddTaskLog(task, $"正在回滚删除 VOD 视频记录 (VideoId: {videoId})..."); var deleteRequest = new DeleteVideoRequest { VideoIds = videoId }; await _vodClient.DeleteVideoAsync(deleteRequest); await _workflowManager.AddTaskLog(task, "VOD 视频记录删除成功"); } catch (Exception deleteEx) { await _workflowManager.AddTaskLog(task, $"回滚删除 VOD 视频记录失败: {deleteEx.Message}"); } } throw; } } } }