feat: 实现视频生成完成后自动下载并上传到云端服务
新增功能: 1. 视频生成完成后自动下载到本地临时文件 2. 自动上传到云端S3服务 3. 将S3 URL转换为CDN HTTPS地址 4. 支持防盗链绕过的视频下载 5. 完善的错误处理和fallback机制 技术实现: - download_and_upload_video(): 主要流程控制 - download_video_to_file(): 下载视频到本地临时文件 - 使用CloudUploadService上传到S3 - convert_s3_to_cdn_url(): S3到CDN URL转换 - 临时文件自动清理机制 用户体验: - 视频生成完成后自动获得可访问的CDN链接 - 无需手动下载和上传操作 - 支持原始URL作为fallback保证可用性 - 详细的日志记录便于问题排查 安全特性: - 防盗链绕过HTTP头设置 - 临时文件安全清理 - 错误情况下的资源释放 - 超时控制防止长时间阻塞
This commit is contained in:
parent
d4b9e77020
commit
6c43e6f08b
|
|
@ -325,10 +325,22 @@ impl VolcanoVideoService {
|
||||||
match data.status.as_str() {
|
match data.status.as_str() {
|
||||||
"done" => {
|
"done" => {
|
||||||
if let Some(video_url) = data.video_url {
|
if let Some(video_url) = data.video_url {
|
||||||
record.mark_as_completed(video_url, None); // 火山云API没有缩略图
|
// 下载视频并上传到云端
|
||||||
|
match self.download_and_upload_video(&video_url, &record_id).await {
|
||||||
|
Ok(cdn_url) => {
|
||||||
|
record.mark_as_completed(cdn_url, None); // 使用CDN URL
|
||||||
self.repository.update(&record).await?;
|
self.repository.update(&record).await?;
|
||||||
info!("视频生成任务完成: {}", record_id);
|
info!("视频生成任务完成,已上传到CDN: {}", record_id);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("视频上传到CDN失败,使用原始URL: {} - {}", record_id, e);
|
||||||
|
record.mark_as_completed(video_url, None); // 使用原始URL作为fallback
|
||||||
|
self.repository.update(&record).await?;
|
||||||
|
info!("视频生成任务完成(使用原始URL): {}", record_id);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
record.mark_as_failed("视频URL为空".to_string(), Some("EMPTY_VIDEO_URL".to_string()));
|
record.mark_as_failed("视频URL为空".to_string(), Some("EMPTY_VIDEO_URL".to_string()));
|
||||||
self.repository.update(&record).await?;
|
self.repository.update(&record).await?;
|
||||||
|
|
@ -594,6 +606,100 @@ impl VolcanoVideoService {
|
||||||
Ok(hex::encode(signature))
|
Ok(hex::encode(signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 下载视频并上传到云端服务
|
||||||
|
async fn download_and_upload_video(&self, video_url: &str, record_id: &str) -> Result<String> {
|
||||||
|
info!("开始下载并上传视频: {} -> {}", video_url, record_id);
|
||||||
|
|
||||||
|
// 创建临时文件路径
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let temp_filename = format!("volcano_video_{}_{}.mp4", record_id, chrono::Utc::now().timestamp());
|
||||||
|
let temp_file_path = temp_dir.join(&temp_filename);
|
||||||
|
let temp_file_str = temp_file_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
// 下载视频到本地临时文件
|
||||||
|
info!("正在下载视频到临时文件: {}", temp_file_str);
|
||||||
|
match self.download_video_to_file(video_url, &temp_file_str).await {
|
||||||
|
Ok(_) => {
|
||||||
|
info!("视频下载成功: {}", temp_file_str);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("视频下载失败: {} - {}", video_url, e);
|
||||||
|
return Err(anyhow!("视频下载失败: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传到云端服务
|
||||||
|
info!("正在上传视频到云端: {}", temp_file_str);
|
||||||
|
let upload_result = match self.cloud_upload_service.upload_file(&temp_file_str, None, None).await {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(e) => {
|
||||||
|
// 清理临时文件
|
||||||
|
let _ = tokio::fs::remove_file(&temp_file_path).await;
|
||||||
|
error!("视频上传失败: {} - {}", temp_file_str, e);
|
||||||
|
return Err(anyhow!("视频上传失败: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
if let Err(e) = tokio::fs::remove_file(&temp_file_path).await {
|
||||||
|
warn!("清理临时文件失败: {} - {}", temp_file_str, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !upload_result.success {
|
||||||
|
let error_msg = upload_result.error_message.unwrap_or_default();
|
||||||
|
error!("视频上传失败: {}", error_msg);
|
||||||
|
return Err(anyhow!("视频上传失败: {}", error_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
let s3_url = upload_result.remote_url
|
||||||
|
.ok_or_else(|| anyhow!("上传成功但未返回S3 URL"))?;
|
||||||
|
|
||||||
|
// 转换S3 URL为CDN URL
|
||||||
|
let cdn_url = Self::convert_s3_to_cdn_url(&s3_url);
|
||||||
|
info!("视频上传成功: {} -> {} -> {}", video_url, s3_url, cdn_url);
|
||||||
|
|
||||||
|
Ok(cdn_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 下载视频文件到本地
|
||||||
|
async fn download_video_to_file(&self, video_url: &str, file_path: &str) -> Result<()> {
|
||||||
|
// 创建带有防盗链绕过的HTTP客户端
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
.timeout(std::time::Duration::from_secs(300)) // 5分钟超时
|
||||||
|
.build()
|
||||||
|
.map_err(|e| anyhow!("创建HTTP客户端失败: {}", e))?;
|
||||||
|
|
||||||
|
// 构建请求,添加必要的头部信息绕过防盗链
|
||||||
|
let request = client
|
||||||
|
.get(video_url)
|
||||||
|
.header("Referer", "https://www.volcengine.com/")
|
||||||
|
.header("Origin", "https://www.volcengine.com")
|
||||||
|
.header("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
|
||||||
|
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||||
|
.header("Cache-Control", "no-cache")
|
||||||
|
.header("Pragma", "no-cache")
|
||||||
|
.header("Sec-Fetch-Dest", "video")
|
||||||
|
.header("Sec-Fetch-Mode", "no-cors")
|
||||||
|
.header("Sec-Fetch-Site", "cross-site");
|
||||||
|
|
||||||
|
let response = request.send().await
|
||||||
|
.map_err(|e| anyhow!("请求视频失败: {}", e))?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow!("下载视频失败,HTTP状态码: {}", response.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes().await
|
||||||
|
.map_err(|e| anyhow!("读取视频数据失败: {}", e))?;
|
||||||
|
|
||||||
|
tokio::fs::write(file_path, &bytes).await
|
||||||
|
.map_err(|e| anyhow!("保存视频文件失败: {}", e))?;
|
||||||
|
|
||||||
|
info!("视频文件保存成功: {} ({} bytes)", file_path, bytes.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 将S3 URL转换为可访问的CDN地址
|
/// 将S3 URL转换为可访问的CDN地址
|
||||||
fn convert_s3_to_cdn_url(s3_url: &str) -> String {
|
fn convert_s3_to_cdn_url(s3_url: &str) -> String {
|
||||||
if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") {
|
if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue