图片合成视频 尺寸优化

This commit is contained in:
jianzhichu
2026-02-12 12:30:06 +08:00
parent 3439a7ad38
commit 650e1ac3fc
6 changed files with 308 additions and 118 deletions
@@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl>
<History>True|2026-02-11T12:39:23.8084775Z||;True|2026-02-11T20:37:06.6507806+08:00||;True|2026-02-11T20:04:19.1839991+08:00||;True|2026-02-10T21:59:37.6685615+08:00||;True|2026-02-10T15:43:23.5038986+08:00||;True|2026-02-10T14:42:48.6214001+08:00||;True|2026-02-06T23:15:19.0037070+08:00||;True|2026-02-06T22:52:30.1006808+08:00||;True|2026-02-06T22:49:02.0056235+08:00||;True|2026-02-06T22:45:30.6355735+08:00||;False|2026-02-06T22:45:25.7210123+08:00||;True|2026-02-06T22:38:37.6994272+08:00||;True|2026-02-06T22:14:47.8146805+08:00||;True|2026-02-03T22:34:12.3588819+08:00||;True|2026-02-03T22:20:08.9945012+08:00||;True|2026-02-03T21:06:17.7655312+08:00||;True|2026-02-02T23:24:09.9134119+08:00||;True|2026-02-02T23:21:52.3235946+08:00||;False|2026-02-02T23:21:47.1386702+08:00||;True|2026-02-02T23:20:46.6277206+08:00||;False|2026-02-02T23:20:40.3158947+08:00||;True|2026-02-02T22:55:39.4268475+08:00||;True|2026-02-01T21:34:30.5989260+08:00||;True|2026-02-01T21:30:20.7556066+08:00||;True|2026-01-30T21:56:47.1840381+08:00||;True|2026-01-28T22:01:00.8412391+08:00||;False|2026-01-28T21:49:29.1242233+08:00||;True|2026-01-28T21:48:26.1707204+08:00||;True|2026-01-28T21:48:05.8291762+08:00||;False|2026-01-28T21:48:01.1596391+08:00||;True|2026-01-28T21:37:59.4831763+08:00||;False|2026-01-28T21:37:53.5855464+08:00||;True|2026-01-28T21:27:51.1872828+08:00||;True|2026-01-28T13:01:14.1677037+08:00||;True|2026-01-28T12:36:45.7819776+08:00||;True|2026-01-28T12:09:15.6540204+08:00||;True|2026-01-28T12:08:40.0490617+08:00||;True|2026-01-28T11:58:55.4078870+08:00||;True|2026-01-27T22:37:03.3304942+08:00||;True|2026-01-27T22:05:40.4316281+08:00||;True|2026-01-26T22:30:43.5083924+08:00||;False|2026-01-26T22:30:33.7355213+08:00||;True|2026-01-26T22:26:26.0218548+08:00||;True|2026-01-26T22:21:32.3171217+08:00||;True|2026-01-26T22:20:40.3916780+08:00||;True|2026-01-26T22:16:19.1017273+08:00||;True|2026-01-26T22:13:34.2028883+08:00||;True|2026-01-26T21:46:23.7117812+08:00||;True|2026-01-26T21:41:19.6963033+08:00||;True|2026-01-25T21:27:13.9893777+08:00||;True|2026-01-24T21:45:22.8493624+08:00||;True|2026-01-24T21:31:35.4635265+08:00||;True|2026-01-24T21:27:31.3098506+08:00||;True|2026-01-24T15:47:01.6934638+08:00||;True|2026-01-24T15:32:35.6316009+08:00||;True|2026-01-24T15:16:28.7216639+08:00||;True|2026-01-23T22:14:26.8066324+08:00||;True|2026-01-23T13:36:48.4086406+08:00||;True|2026-01-23T13:30:27.2924204+08:00||;True|2026-01-23T13:02:53.3219368+08:00||;True|2026-01-23T13:01:54.3466638+08:00||;True|2026-01-23T13:01:28.8560809+08:00||;True|2026-01-22T23:18:32.0400436+08:00||;True|2026-01-22T22:29:29.5746209+08:00||;True|2026-01-22T21:32:21.7284081+08:00||;True|2026-01-22T21:20:26.5985955+08:00||;False|2026-01-22T21:20:21.1372539+08:00||;True|2026-01-22T21:11:18.3771739+08:00||;True|2026-01-22T20:58:55.9862778+08:00||;True|2026-01-22T20:51:48.6699461+08:00||;False|2026-01-22T20:51:42.2079210+08:00||;True|2026-01-22T20:50:34.7280114+08:00||;True|2026-01-22T20:50:11.1588492+08:00||;True|2026-01-22T16:11:14.0626037+08:00||;True|2026-01-22T15:53:16.6377347+08:00||;True|2026-01-22T15:45:34.0935091+08:00||;True|2026-01-22T12:44:28.1577014+08:00||;True|2026-01-22T12:40:45.7449335+08:00||;True|2026-01-22T12:39:07.0683214+08:00||;True|2026-01-22T09:55:49.7086190+08:00||;True|2026-01-22T01:37:53.6972395+08:00||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;True|2026-01-15T22:07:14.8243754+08:00||;</History>
<History>True|2026-02-12T04:18:02.1864306Z||;True|2026-02-11T20:39:23.8084775+08:00||;True|2026-02-11T20:37:06.6507806+08:00||;True|2026-02-11T20:04:19.1839991+08:00||;True|2026-02-10T21:59:37.6685615+08:00||;True|2026-02-10T15:43:23.5038986+08:00||;True|2026-02-10T14:42:48.6214001+08:00||;True|2026-02-06T23:15:19.0037070+08:00||;True|2026-02-06T22:52:30.1006808+08:00||;True|2026-02-06T22:49:02.0056235+08:00||;True|2026-02-06T22:45:30.6355735+08:00||;False|2026-02-06T22:45:25.7210123+08:00||;True|2026-02-06T22:38:37.6994272+08:00||;True|2026-02-06T22:14:47.8146805+08:00||;True|2026-02-03T22:34:12.3588819+08:00||;True|2026-02-03T22:20:08.9945012+08:00||;True|2026-02-03T21:06:17.7655312+08:00||;True|2026-02-02T23:24:09.9134119+08:00||;True|2026-02-02T23:21:52.3235946+08:00||;False|2026-02-02T23:21:47.1386702+08:00||;True|2026-02-02T23:20:46.6277206+08:00||;False|2026-02-02T23:20:40.3158947+08:00||;True|2026-02-02T22:55:39.4268475+08:00||;True|2026-02-01T21:34:30.5989260+08:00||;True|2026-02-01T21:30:20.7556066+08:00||;True|2026-01-30T21:56:47.1840381+08:00||;True|2026-01-28T22:01:00.8412391+08:00||;False|2026-01-28T21:49:29.1242233+08:00||;True|2026-01-28T21:48:26.1707204+08:00||;True|2026-01-28T21:48:05.8291762+08:00||;False|2026-01-28T21:48:01.1596391+08:00||;True|2026-01-28T21:37:59.4831763+08:00||;False|2026-01-28T21:37:53.5855464+08:00||;True|2026-01-28T21:27:51.1872828+08:00||;True|2026-01-28T13:01:14.1677037+08:00||;True|2026-01-28T12:36:45.7819776+08:00||;True|2026-01-28T12:09:15.6540204+08:00||;True|2026-01-28T12:08:40.0490617+08:00||;True|2026-01-28T11:58:55.4078870+08:00||;True|2026-01-27T22:37:03.3304942+08:00||;True|2026-01-27T22:05:40.4316281+08:00||;True|2026-01-26T22:30:43.5083924+08:00||;False|2026-01-26T22:30:33.7355213+08:00||;True|2026-01-26T22:26:26.0218548+08:00||;True|2026-01-26T22:21:32.3171217+08:00||;True|2026-01-26T22:20:40.3916780+08:00||;True|2026-01-26T22:16:19.1017273+08:00||;True|2026-01-26T22:13:34.2028883+08:00||;True|2026-01-26T21:46:23.7117812+08:00||;True|2026-01-26T21:41:19.6963033+08:00||;True|2026-01-25T21:27:13.9893777+08:00||;True|2026-01-24T21:45:22.8493624+08:00||;True|2026-01-24T21:31:35.4635265+08:00||;True|2026-01-24T21:27:31.3098506+08:00||;True|2026-01-24T15:47:01.6934638+08:00||;True|2026-01-24T15:32:35.6316009+08:00||;True|2026-01-24T15:16:28.7216639+08:00||;True|2026-01-23T22:14:26.8066324+08:00||;True|2026-01-23T13:36:48.4086406+08:00||;True|2026-01-23T13:30:27.2924204+08:00||;True|2026-01-23T13:02:53.3219368+08:00||;True|2026-01-23T13:01:54.3466638+08:00||;True|2026-01-23T13:01:28.8560809+08:00||;True|2026-01-22T23:18:32.0400436+08:00||;True|2026-01-22T22:29:29.5746209+08:00||;True|2026-01-22T21:32:21.7284081+08:00||;True|2026-01-22T21:20:26.5985955+08:00||;False|2026-01-22T21:20:21.1372539+08:00||;True|2026-01-22T21:11:18.3771739+08:00||;True|2026-01-22T20:58:55.9862778+08:00||;True|2026-01-22T20:51:48.6699461+08:00||;False|2026-01-22T20:51:42.2079210+08:00||;True|2026-01-22T20:50:34.7280114+08:00||;True|2026-01-22T20:50:11.1588492+08:00||;True|2026-01-22T16:11:14.0626037+08:00||;True|2026-01-22T15:53:16.6377347+08:00||;True|2026-01-22T15:45:34.0935091+08:00||;True|2026-01-22T12:44:28.1577014+08:00||;True|2026-01-22T12:40:45.7449335+08:00||;True|2026-01-22T12:39:07.0683214+08:00||;True|2026-01-22T09:55:49.7086190+08:00||;True|2026-01-22T01:37:53.6972395+08:00||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
+15 -13
View File
@@ -6,6 +6,7 @@ using dy.net.service;
using dy.net.utils;
using Newtonsoft.Json;
using Quartz;
using Quartz.Util;
using Serilog;
using System.Net;
@@ -513,6 +514,7 @@ namespace dy.net.job
/// <summary>
/// 遍历视频列表,分别处理每个视频和图片集
/// </summary>
/// <param name="syncCount1"></param>
/// <param name="cookie">用户Cookie</param>
/// <param name="data">视频信息对象</param>
/// <param name="config">应用配置</param>
@@ -525,7 +527,7 @@ namespace dy.net.job
var videos = new List<DouyinVideo>();
foreach (var item in data.AwemeList)
{
//if (item.AwemeId != "7565895895016295726")
//if (item.AwemeId != "7321309610927770930")
//{
// continue;
//}
@@ -602,7 +604,7 @@ namespace dy.net.job
else
{
//处理多个视频-组合的图文视频--类似动图。
List<DouyinDynamicVideoDto> dynamicVideoUrls = new List<DouyinDynamicVideoDto>();
List<DouyinMergeVideoDto> dynamicVideoUrls = new List<DouyinMergeVideoDto>();
// 当需要下载动态视频时,获取其他URL
if (config.DownDynamicVideo && item.Images != null && item.Images.Count > 0)
{
@@ -617,7 +619,7 @@ namespace dy.net.job
{
var height = btv.PlayAddr?.Height ?? 1920;
var width = btv.PlayAddr?.Width ?? 1080;
DouyinDynamicVideoDto info = new DouyinDynamicVideoDto
DouyinMergeVideoDto info = new DouyinMergeVideoDto
{
Path = targetUrl,
Height = height,
@@ -639,7 +641,7 @@ namespace dy.net.job
{
if (!string.IsNullOrEmpty(dynamicVideo.DynamicVideos))
{
var dynamicVideos = JsonConvert.DeserializeObject<List<DouyinDynamicVideoDto>>(dynamicVideo.DynamicVideos);
var dynamicVideos = JsonConvert.DeserializeObject<List<DouyinMergeVideoDto>>(dynamicVideo.DynamicVideos);
Log.Debug($"[{cookie.UserName}][{VideoType.GetDesc()}]-动态视频[{item.Desc}],下载成功 ,共{dynamicVideos?.Count}个视频...");
if (dynamicVideos != null && dynamicVideos.Count > 0)
{
@@ -953,7 +955,7 @@ namespace dy.net.job
/// <param name="followed"></param>
/// <param name="cate"></param>
/// <returns></returns>
protected async Task<DouyinVideo> ProcessDynamicVideo(List<DouyinDynamicVideoDto> dynamicUrls, DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed = null, DouyinCollectCate cate = null)
protected async Task<DouyinVideo> ProcessDynamicVideo(List<DouyinMergeVideoDto> dynamicUrls, DouyinCookie cookie, Aweme item, AppConfig config, DouyinFollowed followed = null, DouyinCollectCate cate = null)
{
// 创建保存文件夹
var saveFolder = CreateSaveFolder(cookie, item, config, followed, cate);
@@ -968,12 +970,12 @@ namespace dy.net.job
// 随机延迟
int i = 1;
List<DouyinDynamicVideoDto> dynamicSavePaths = new List<DouyinDynamicVideoDto>();
List<DouyinMergeVideoDto> dynamicSavePaths = new List<DouyinMergeVideoDto>();
foreach (var dynamicUrl in dynamicUrls)
{
// 下载动态视频
var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4");
DouyinDynamicVideoDto v = new DouyinDynamicVideoDto() { Height = dynamicUrl.Height, Width = dynamicUrl.Width, Path = dynamicSavePath };
DouyinMergeVideoDto v = new DouyinMergeVideoDto() { Height = dynamicUrl.Height, Width = dynamicUrl.Width, Path = dynamicSavePath };
i++;
// 如果文件已存在,跳过
@@ -1087,10 +1089,10 @@ namespace dy.net.job
try
{
// 提取图片URL列表
List<string> imageUrls = item.Images?
List<DouyinMergeVideoDto> imageUrls = item.Images?
.Where(img => img.UrlList != null && img.UrlList.Any())
.Select(img => img.UrlList.FirstOrDefault())
.Where(url => !string.IsNullOrWhiteSpace(url))
.Select(img => new DouyinMergeVideoDto { Path = img.UrlList.FirstOrDefault(),Height=img.Height,Width=img.Width })
.Where(img => !string.IsNullOrWhiteSpace(img.Path))
.ToList();
// 如果没有图片,返回null
@@ -1169,8 +1171,8 @@ namespace dy.net.job
}
var coverUrl = cate is not null && cate.CateType != VideoTypeEnum.dy_custom_collect
? (item.MixInfo?.CoverUrl?.UrlList?.FirstOrDefault() ?? imageUrls.FirstOrDefault() ?? item.Music?.CoverHd?.UrlList?.FirstOrDefault())
: imageUrls.FirstOrDefault();
? (item.MixInfo?.CoverUrl?.UrlList?.FirstOrDefault() ?? imageUrls.FirstOrDefault()?.Path ?? item.Music?.CoverHd?.UrlList?.FirstOrDefault())
: imageUrls.FirstOrDefault()?.Path;
// 下载作者头像
var avatarSavePath = await DownAuthorAvatar(cookie, item);
@@ -1408,7 +1410,7 @@ namespace dy.net.job
/// <param name="cate">短剧、合集、自定义收藏夹</param>
/// <returns>创建的视频实体对象</returns>
private async Task<DouyinVideo> CreateVideoEntity(AppConfig config,
DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string coverSavePath, string avatorPath, List<DouyinDynamicVideoDto> dynamicVideos = null, DouyinCollectCate cate = null)
DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string coverSavePath, string avatorPath, List<DouyinMergeVideoDto> dynamicVideos = null, DouyinCollectCate cate = null)
{
// 获取视频标签
var (tag1, tag2, tag3) = GetVideoTags(item);
+1 -1
View File
@@ -1,6 +1,6 @@
namespace dy.net.model.dto
{
public class DouyinDynamicVideoDto
public class DouyinMergeVideoDto
{
public string Path { get; set; }
+1 -1
View File
@@ -3,7 +3,7 @@
public class MediaMergeRequest
{
/// <summary>网络图片地址数组(必填)</summary>
public List<string> ImageUrls { get; set; }
public List<DouyinMergeVideoDto> ImageUrls { get; set; }
/// <summary>网络MP3地址数组(必填)</summary>
public List<string> AudioUrls { get; set; }
+32 -26
View File
@@ -36,7 +36,7 @@ namespace dy.net.service
/// <param name="ck"></param>
/// <returns></returns>
public async Task<(string mp4Path, string mp3Path)> MergeMultipleVideosAsync(
List<DouyinDynamicVideoDto> videoFilePaths,
List<DouyinMergeVideoDto> videoFilePaths,
string audioPath,
string savePath,
string ck)
@@ -45,11 +45,11 @@ namespace dy.net.service
string mergMusicPath = GetRandomMergeMusic();
if (!string.IsNullOrWhiteSpace(audioPath))
{
var (SuccessPaths, _) = await DownloadMediaAsync(new List<string> { audioPath }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
if (SuccessPaths != null && SuccessPaths.Length > 0)
var (SuccessPaths, _) = await DownloadMediaAsync(new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= audioPath } }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
if (SuccessPaths != null && SuccessPaths.Count > 0)
{
mergMusicPath = SuccessPaths[0];
mp3Path = SuccessPaths[0];
mergMusicPath = SuccessPaths.FirstOrDefault()?.Path;
mp3Path = SuccessPaths.FirstOrDefault()?.Path;
}
}
var ffmpeg = new FFmpegHelper();
@@ -126,7 +126,7 @@ namespace dy.net.service
// 1. 下载图片
var (rawImages, imageError) = await DownloadMediaAsync(
request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp", cookie);
if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Length == 0)
if (!string.IsNullOrEmpty(imageError) || rawImages == null || rawImages.Count == 0)
{
Log.Error($"图片下载失败:{imageError ?? ""}");
return false;
@@ -138,12 +138,12 @@ namespace dy.net.service
}
// 2. 下载音频
string[] rawAudios = Array.Empty<string>();
List<DouyinMergeVideoDto> rawAudios =new List<DouyinMergeVideoDto>();
if (mergeImg2Viedo && request.AudioUrls != null && request.AudioUrls.Count > 0)
{
var (audios, audioError) = await DownloadMediaAsync(
request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Length == 0)
request.AudioUrls.Select(x => new DouyinMergeVideoDto { Path = x })?.ToList(), Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Count == 0)
{
Log.Error($"音频下载失败:{audioError ?? ""}");
return false;
@@ -152,7 +152,7 @@ namespace dy.net.service
// 保存下载的音频(如果需要)
if (downMp3)
{
var ext = Path.GetExtension(rawAudios[0]);
var ext = Path.GetExtension(rawAudios.FirstOrDefault()?.Path);
await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, ext, Path.GetFileNameWithoutExtension(outputVideoPath));
}
}
@@ -192,16 +192,16 @@ namespace dy.net.service
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300));
// 执行合成(确保每个图片都参与)
if (rawAudios.Length == 0)
if (rawAudios.Count == 0)
{
var musicPath = GetRandomMergeMusic();
rawAudios = new string[] { musicPath };
rawAudios = new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= musicPath } };
Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
}
string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync(
rawImages, // 所有下载的图片(确保无遗漏)
rawAudios[0], // 取第一个音频(可根据需求调整)
rawAudios.FirstOrDefault()?.Path, // 取第一个音频(可根据需求调整)
outputVideoPath,
ffmpegHelper.VideoWidth,
ffmpegHelper.VideoHeight,
@@ -235,23 +235,23 @@ namespace dy.net.service
/// <summary>
/// 保存下载的文件(图片/音频)到目标目录
/// </summary>
private static async Task SaveDownloadedFilesAsync(string[] sourcePaths, string targetFolder, string defaultExt, string videoFileName)
private static async Task SaveDownloadedFilesAsync(List<DouyinMergeVideoDto> sourcePaths, string targetFolder, string defaultExt, string videoFileName)
{
if (sourcePaths == null || sourcePaths.Length == 0) return;
if (sourcePaths == null || sourcePaths.Count == 0) return;
Directory.CreateDirectory(targetFolder); // 确保目标目录存在
// 异步保存(不阻塞主线程)
await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) =>
{
if (!File.Exists(sourcePath)) return;
string extension = Path.GetExtension(sourcePath) ?? $".{defaultExt}";
if (!File.Exists(sourcePath.Path)) return;
string extension = Path.GetExtension(sourcePath.Path) ?? $".{defaultExt}";
string destFileName = $"{videoFileName}{index + 1:D3}{extension}";
string destPath = Path.Combine(targetFolder, destFileName);
// 文件不存在时才复制,避免覆盖
if (!File.Exists(destPath))
{
await Task.Run(() => File.Copy(sourcePath, destPath, overwrite: false));
await Task.Run(() => File.Copy(sourcePath.Path, destPath, overwrite: false));
Log.Debug($"已保存文件:{destPath}");
}
}));
@@ -326,23 +326,29 @@ namespace dy.net.service
/// <summary>通用媒体下载方法</summary>
private async Task<(string[] SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
List<string> urls, string saveDir, string prefix, string ext, string cookie)
private async Task<(List<DouyinMergeVideoDto> SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
List<DouyinMergeVideoDto> urls, string saveDir, string prefix, string ext, string cookie)
{
var successPaths = new List<string>();
var successPaths = new List<DouyinMergeVideoDto>();
for (var i = 0; i < urls.Count; i++)
{
var url = urls[i];
var fileExt = ext ?? Path.GetExtension(url).TrimStart('.') ?? "png";
var fileExt = ext ?? Path.GetExtension(url.Path).TrimStart('.') ?? "png";
var fileName = $"{prefix}{i + 1}.{fileExt}";
var savePath = Path.Combine(saveDir, fileName);
try
{
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url, savePath, cookie);
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url.Path, savePath, cookie);
if (Success)
{
successPaths.Add(ActualSavePath);
successPaths.Add(new DouyinMergeVideoDto
{
Path = ActualSavePath,
Height = url.Height,
Width =
url.Width
});
}
else
{
@@ -354,10 +360,10 @@ namespace dy.net.service
{
var error = $"下载失败:{url},错误:{ex.Message}";
Serilog.Log.Error(error);
return (Array.Empty<string>(), error);
return (new List<DouyinMergeVideoDto>(), error);
}
}
return (successPaths.ToArray(), null);
return (successPaths, null);
}
}
+258 -76
View File
@@ -51,14 +51,186 @@ namespace dy.net.utils
/// <summary>
/// 将多张图片和一个音频文件合成为视频(最终终极版)。
/// </summary>
//public async Task<string> CreateVideoFromImagesAndAudioAsync(
// IEnumerable<DouyinMergeVideoDto> imageFilePaths,
// string audioFilePath,
// string outputVideoPath,
// int VideoWidth = 1080,
// int Height = 1920,
// IProgress<double> progress = null,
// CancellationToken cancellationToken = default)
//{
// // 输入验证
// if (imageFilePaths == null || !imageFilePaths.Any())
// throw new ArgumentException("图片路径列表不能为空。", nameof(imageFilePaths));
// if (string.IsNullOrEmpty(audioFilePath) || !File.Exists(audioFilePath))
// throw new FileNotFoundException("音频文件未找到。", audioFilePath);
// if (string.IsNullOrEmpty(outputVideoPath))
// throw new ArgumentNullException(nameof(outputVideoPath));
// foreach (var imagePath in imageFilePaths)
// {
// if (!File.Exists(imagePath.Path))
// throw new FileNotFoundException("图片文件未找到。", imagePath.Path);
// }
// var outputDirectory = Path.GetDirectoryName(outputVideoPath);
// if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
// {
// Directory.CreateDirectory(outputDirectory);
// }
// // 关键步骤 1: 创建临时目录并生成有序图片序列
// string tempImageDir = Path.Combine(AppContext.BaseDirectory, "temp", Guid.NewGuid().ToString());
// Directory.CreateDirectory(tempImageDir);
// var imageList = imageFilePaths.ToList();
// try
// {
// for (int i = 0; i < imageList.Count; i++)
// {
// string sourcePath = imageList[i].Path;
// string extension = Path.GetExtension(sourcePath);
// string destFileName = $"temp_{i + 1:D3}{extension}";
// string destPath = Path.Combine(tempImageDir, destFileName);
// File.Copy(sourcePath, destPath);
// }
// string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList.FirstOrDefault().Path));
// double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2);
// // --- 修正点 1: 整合音频时长获取逻辑 ---
// // 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环
// double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长
// try
// {
// // 使用 ffprobe 获取音频时长
// var startInfo = new ProcessStartInfo
// {
// FileName = _ffprobeExecutablePath,
// Arguments = $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{audioFilePath}\"",
// RedirectStandardOutput = true,
// RedirectStandardError = true,
// UseShellExecute = false,
// CreateNoWindow = true
// };
// using (var process = new Process { StartInfo = startInfo })
// {
// process.Start();
// string output = await process.StandardOutput.ReadToEndAsync();
// await process.WaitForExitAsync();
// if (double.TryParse(output, out double duration))
// {
// audioDurationSeconds = duration;
// //Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s");
// }
// }
// }
// catch (Exception ex)
// {
// Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
// }
// // 计算图片序列需要循环的次数
// int loopCount = 1;
// int imageCount = imageList.Count; // 要循环的图片总数(对应loop的size参数)
// double singleLoopDuration = imageCount * ImageDisplayDurationSeconds;
// if (audioDurationSeconds > singleLoopDuration)
// {
// loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration);
// //Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长");
// }
// // H264编码要求宽高必须为偶数 20260103
// if (VideoWidth % 2 != 0)
// {
// VideoWidth++;
// }
// if (VideoHeight % 2 != 0)
// {
// VideoHeight++;
// }
// var arguments = new List<string>
// {
// "-y", // 覆盖输出文件
// // 图片序列输入:保留原有逻辑,修复帧率含义(-r 是输出帧率,输入用-framerate更准确)
// "-f", "image2",
// "-framerate", imageFps.ToString(CultureInfo.InvariantCulture), // 输入帧率(图片播放速度)
// "-start_number", "0", // 序列起始编号
// "-i", imageSequencePattern, // 图片序列模板(无需引号,后续用ProcessStartInfo传参更安全)
// // 音频输入
// "-i", audioFilePath,
// // 滤镜修复:补充size参数(核心!解决Number of frames to loop is not set报错)
// // loop=循环次数:size=要循环的帧数;loop=-1表示无限循环(配合-shortest更简洁)
// "-filter_complex", $"[0:v]loop=loop={loopCount - 1}:size={imageCount}[v]",
// // 流映射
// "-map", "[v]",
// "-map", "1:a",
// // 视频编码:保留你的变量,确保兼容性
// "-c:v", VideoCodec,
// "-preset", VideoPreset,
// "-crf", $"{VideoCrf}",
// "-s", $"{VideoWidth}x{VideoHeight}",
// "-pix_fmt", "yuv420p", // 兼容所有播放器
// "-profile:v", "main",
// // 音频编码:标准化参数,避免无效值
// "-c:a", AudioCodec,
// "-b:a", $"{AudioBitrate}",
// "-ac", "2", // 立体声
// "-ar", "44100", // 标准采样率
// // 封装优化
// "-f", "mp4",
// "-movflags", "+faststart",
// // 同步参数:确保视频时长匹配音频
// "-shortest",
// // 输出路径
// outputVideoPath
// };
// // 执行命令
// await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
// if (File.Exists(outputVideoPath))
// {
// return outputVideoPath;
// }
// else
// {
// throw new InvalidOperationException("视频合成失败,未生成输出文件。");
// }
// }
// finally
// {
// // 清理临时目录
// if (Directory.Exists(tempImageDir))
// {
// Directory.Delete(tempImageDir, recursive: true);
// }
// }
//}
public async Task<string> CreateVideoFromImagesAndAudioAsync(
IEnumerable<string> imageFilePaths,
string audioFilePath,
string outputVideoPath,
int VideoWidth = 1080,
int Height = 1920,
IProgress<double> progress = null,
CancellationToken cancellationToken = default)
IEnumerable<DouyinMergeVideoDto> imageFilePaths,
string audioFilePath,
string outputVideoPath,
int VideoWidth = 1080,
int VideoHeight = 1920,
IProgress<double> progress = null,
CancellationToken cancellationToken = default)
{
// 输入验证
if (imageFilePaths == null || !imageFilePaths.Any())
@@ -72,8 +244,11 @@ namespace dy.net.utils
foreach (var imagePath in imageFilePaths)
{
if (!File.Exists(imagePath))
throw new FileNotFoundException("图片文件未找到。", imagePath);
if (!File.Exists(imagePath.Path))
throw new FileNotFoundException("图片文件未找到。", imagePath.Path);
// 验证图片宽高字段有效性
if (imagePath.Width <= 0 || imagePath.Height <= 0)
throw new ArgumentException($"图片 {imagePath.Path} 的宽高必须大于0", nameof(imageFilePaths));
}
var outputDirectory = Path.GetDirectoryName(outputVideoPath);
@@ -91,22 +266,20 @@ namespace dy.net.utils
{
for (int i = 0; i < imageList.Count; i++)
{
string sourcePath = imageList[i];
string sourcePath = imageList[i].Path;
string extension = Path.GetExtension(sourcePath);
string destFileName = $"temp_{i + 1:D3}{extension}";
string destPath = Path.Combine(tempImageDir, destFileName);
File.Copy(sourcePath, destPath);
File.Copy(sourcePath, destPath, true);
}
string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList[0]));
string imageSequencePattern = Path.Combine(tempImageDir, "temp_%03d" + Path.GetExtension(imageList.FirstOrDefault().Path));
double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2);
// --- 修正点 1: 整合音频时长获取逻辑 ---
// 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环
double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长
// 获取音频时长
double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds;
try
{
// 使用 ffprobe 获取音频时长
var startInfo = new ProcessStartInfo
{
FileName = _ffprobeExecutablePath,
@@ -126,103 +299,112 @@ namespace dy.net.utils
if (double.TryParse(output, out double duration))
{
audioDurationSeconds = duration;
//Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s");
}
}
}
catch (Exception ex)
{
Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
}
// 计算图片序列需要循环次数
// 计算图片序列循环次数
int loopCount = 1;
int imageCount = imageList.Count; // 要循环的图片总数(对应loop的size参数)
int imageCount = imageList.Count;
double singleLoopDuration = imageCount * ImageDisplayDurationSeconds;
if (audioDurationSeconds > singleLoopDuration)
{
loopCount = (int)Math.Ceiling(audioDurationSeconds / singleLoopDuration);
//Console.WriteLine($"图片序列将循环 {loopCount} 次以匹配音频时长");
}
// H264编码要求宽高必须为偶数 20260103
if (VideoWidth % 2 != 0)
{
VideoWidth++;
}
if (VideoHeight % 2 != 0)
{
VideoHeight++;
}
// H264编码要求宽高为偶数
if (VideoWidth % 2 != 0) VideoWidth++;
if (VideoHeight % 2 != 0) VideoHeight++;
// ========== 核心优化:图片适配滤镜 ==========
// scale=iw*min({VideoWidth}/iw\,{VideoHeight}/ih):ih*min({VideoWidth}/iw\,{VideoHeight}/ih) 等比例缩放
// pad={VideoWidth}:{VideoHeight}:(ow-iw)/2:(oh-ih)/2:black 居中填充黑色背景
string fitFilter = $"scale=iw*min({VideoWidth}/iw\\,{VideoHeight}/ih):ih*min({VideoWidth}/iw\\,{VideoHeight}/ih),pad={VideoWidth}:{VideoHeight}:(ow-iw)/2:(oh-ih)/2:black";
// 组合最终滤镜:先适配尺寸,再循环
string filterComplex = $"[0:v]{fitFilter},loop=loop={loopCount - 1}:size={imageCount}[v]";
var arguments = new List<string>
{
"-y", // 覆盖输出文件
{
"-y", // 覆盖输出文件
// 图片序列输入:保留原有逻辑,修复帧率含义(-r 是输出帧率,输入用-framerate更准确)
"-f", "image2",
"-framerate", imageFps.ToString(CultureInfo.InvariantCulture), // 输入帧率(图片播放速度)
"-start_number", "0", // 序列起始编号
"-i", imageSequencePattern, // 图片序列模板(无需引号,后续用ProcessStartInfo传参更安全)
// 图片序列输入
"-f", "image2",
"-framerate", imageFps.ToString(CultureInfo.InvariantCulture),
"-start_number", "1", // 修正:临时文件是 temp_001、temp_002... 所以起始编号为1
"-i", imageSequencePattern,
// 音频输入
"-i", audioFilePath,
// 音频输入
"-i", audioFilePath,
// 滤镜修复:补充size参数(核心!解决Number of frames to loop is not set报错)
// loop=循环次数:size=要循环的帧数;loop=-1表示无限循环(配合-shortest更简洁)
"-filter_complex", $"[0:v]loop=loop={loopCount - 1}:size={imageCount}[v]",
// 滤镜:尺寸适配 + 循环
"-filter_complex", filterComplex,
// 流映射
"-map", "[v]",
"-map", "1:a",
// 流映射
"-map", "[v]",
"-map", "1:a",
// 视频编码:保留你的变量,确保兼容性
"-c:v", VideoCodec,
"-preset", VideoPreset,
"-crf", $"{VideoCrf}",
"-s", $"{VideoWidth}x{VideoHeight}",
"-pix_fmt", "yuv420p", // 兼容所有播放器
"-profile:v", "main",
// 视频编码参数
"-c:v", VideoCodec,
"-preset", VideoPreset,
"-crf", $"{VideoCrf}",
"-s", $"{VideoWidth}x{VideoHeight}",
"-pix_fmt", "yuv420p",
"-profile:v", "main",
// 音频编码:标准化参数,避免无效值
"-c:a", AudioCodec,
"-b:a", $"{AudioBitrate}",
"-ac", "2", // 立体声
"-ar", "44100", // 标准采样率
// 音频编码参数
"-c:a", AudioCodec,
"-b:a", $"{AudioBitrate}",
"-ac", "2",
"-ar", "44100",
// 封装优化
"-f", "mp4",
"-movflags", "+faststart",
// 封装优化
"-f", "mp4",
"-movflags", "+faststart",
// 同步参数:确保视频时长匹配音频
"-shortest",
// 同步参数
"-shortest",
// 输出路径
outputVideoPath
};
// 输出路径
outputVideoPath
};
// 执行命令
// 执行FFmpeg命令
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
if (File.Exists(outputVideoPath))
{
return outputVideoPath;
}
else
if (!File.Exists(outputVideoPath))
{
throw new InvalidOperationException("视频合成失败,未生成输出文件。");
}
return outputVideoPath;
}
finally
{
// 清理临时目录
// 清理临时目录(增加异常捕获,避免删除失败导致程序崩溃)
if (Directory.Exists(tempImageDir))
{
Directory.Delete(tempImageDir, recursive: true);
try
{
Directory.Delete(tempImageDir, recursive: true);
}
catch (Exception ex)
{
Log.Warning($"清理临时目录失败: {ex.Message}");
}
}
}
}
/// <summary>
/// 将单个视频转封装为统一编码/分辨率的临时视频(解决concat合并兼容问题)
/// </summary>
@@ -232,7 +414,7 @@ namespace dy.net.utils
/// <param name="cancellationToken">取消令牌</param>
/// <returns>统一参数后的临时视频路径</returns>
private async Task<string> CreateUnifiedTempVideoAsync(
DouyinDynamicVideoDto videoDto,
DouyinMergeVideoDto videoDto,
int targetWidth, // 外部传入的动态目标宽(已取所有视频的最大值,偶数)
int targetHeight, // 外部传入的动态目标高(已取所有视频的最大值,偶数)
CancellationToken cancellationToken)
@@ -325,13 +507,13 @@ namespace dy.net.utils
/// <returns>输出视频路径</returns>
// 移除了 width = 1080 和 height = 1920 两个传入参数
public async Task<string> MergeMultipleVideosAsync(
List<DouyinDynamicVideoDto> videoFilePaths,
List<DouyinMergeVideoDto> videoFilePaths,
string audioPath,
string savePath,
IProgress<double> progress = null,
CancellationToken cancellationToken = default)
{
List<DouyinDynamicVideoDto> validVideoList = new List<DouyinDynamicVideoDto>();
List<DouyinMergeVideoDto> validVideoList = new List<DouyinMergeVideoDto>();
List<string> tempUnifiedVideoPaths = new List<string>();
// 输入验证