239 lines
10 KiB
C#
239 lines
10 KiB
C#
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<TidySlideHandle>();
|
||
services.AddTransient<Repository<TidySlideTaskResult>>();
|
||
}
|
||
}
|
||
|
||
public class TidySlideHandle
|
||
{
|
||
private readonly Client _vodClient;
|
||
private readonly Repository<VideoTask> _videoTaskDB;
|
||
private readonly Repository<TidySlideTaskResult> _tidySlideTaskResultDB;
|
||
private readonly RedisManager _redisManager;
|
||
private readonly OssClient _ossClient; // 使用系统统一注入的 OSS Client
|
||
private readonly TidySlideWorkflowManager _workflowManager; // 注入工作流管理器
|
||
|
||
public TidySlideHandle(Client vodClient, Repository<VideoTask> videoTaskDB, Repository<TidySlideTaskResult> 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;
|
||
}
|
||
}
|
||
}
|
||
}
|