图片合成视频 尺寸优化

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> <Project>
<PropertyGroup> <PropertyGroup>
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\</_PublishTargetUrl> <_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 /> <LastFailureDetails />
</PropertyGroup> </PropertyGroup>
</Project> </Project>
+15 -13
View File
@@ -6,6 +6,7 @@ using dy.net.service;
using dy.net.utils; using dy.net.utils;
using Newtonsoft.Json; using Newtonsoft.Json;
using Quartz; using Quartz;
using Quartz.Util;
using Serilog; using Serilog;
using System.Net; using System.Net;
@@ -513,6 +514,7 @@ namespace dy.net.job
/// <summary> /// <summary>
/// 遍历视频列表,分别处理每个视频和图片集 /// 遍历视频列表,分别处理每个视频和图片集
/// </summary> /// </summary>
/// <param name="syncCount1"></param>
/// <param name="cookie">用户Cookie</param> /// <param name="cookie">用户Cookie</param>
/// <param name="data">视频信息对象</param> /// <param name="data">视频信息对象</param>
/// <param name="config">应用配置</param> /// <param name="config">应用配置</param>
@@ -525,7 +527,7 @@ namespace dy.net.job
var videos = new List<DouyinVideo>(); var videos = new List<DouyinVideo>();
foreach (var item in data.AwemeList) foreach (var item in data.AwemeList)
{ {
//if (item.AwemeId != "7565895895016295726") //if (item.AwemeId != "7321309610927770930")
//{ //{
// continue; // continue;
//} //}
@@ -602,7 +604,7 @@ namespace dy.net.job
else else
{ {
//处理多个视频-组合的图文视频--类似动图。 //处理多个视频-组合的图文视频--类似动图。
List<DouyinDynamicVideoDto> dynamicVideoUrls = new List<DouyinDynamicVideoDto>(); List<DouyinMergeVideoDto> dynamicVideoUrls = new List<DouyinMergeVideoDto>();
// 当需要下载动态视频时,获取其他URL // 当需要下载动态视频时,获取其他URL
if (config.DownDynamicVideo && item.Images != null && item.Images.Count > 0) 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 height = btv.PlayAddr?.Height ?? 1920;
var width = btv.PlayAddr?.Width ?? 1080; var width = btv.PlayAddr?.Width ?? 1080;
DouyinDynamicVideoDto info = new DouyinDynamicVideoDto DouyinMergeVideoDto info = new DouyinMergeVideoDto
{ {
Path = targetUrl, Path = targetUrl,
Height = height, Height = height,
@@ -639,7 +641,7 @@ namespace dy.net.job
{ {
if (!string.IsNullOrEmpty(dynamicVideo.DynamicVideos)) 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}个视频..."); Log.Debug($"[{cookie.UserName}][{VideoType.GetDesc()}]-动态视频[{item.Desc}],下载成功 ,共{dynamicVideos?.Count}个视频...");
if (dynamicVideos != null && dynamicVideos.Count > 0) if (dynamicVideos != null && dynamicVideos.Count > 0)
{ {
@@ -953,7 +955,7 @@ namespace dy.net.job
/// <param name="followed"></param> /// <param name="followed"></param>
/// <param name="cate"></param> /// <param name="cate"></param>
/// <returns></returns> /// <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); var saveFolder = CreateSaveFolder(cookie, item, config, followed, cate);
@@ -968,12 +970,12 @@ namespace dy.net.job
// 随机延迟 // 随机延迟
int i = 1; int i = 1;
List<DouyinDynamicVideoDto> dynamicSavePaths = new List<DouyinDynamicVideoDto>(); List<DouyinMergeVideoDto> dynamicSavePaths = new List<DouyinMergeVideoDto>();
foreach (var dynamicUrl in dynamicUrls) foreach (var dynamicUrl in dynamicUrls)
{ {
// 下载动态视频 // 下载动态视频
var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4"); 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++; i++;
// 如果文件已存在,跳过 // 如果文件已存在,跳过
@@ -1087,10 +1089,10 @@ namespace dy.net.job
try try
{ {
// 提取图片URL列表 // 提取图片URL列表
List<string> imageUrls = item.Images? List<DouyinMergeVideoDto> imageUrls = item.Images?
.Where(img => img.UrlList != null && img.UrlList.Any()) .Where(img => img.UrlList != null && img.UrlList.Any())
.Select(img => img.UrlList.FirstOrDefault()) .Select(img => new DouyinMergeVideoDto { Path = img.UrlList.FirstOrDefault(),Height=img.Height,Width=img.Width })
.Where(url => !string.IsNullOrWhiteSpace(url)) .Where(img => !string.IsNullOrWhiteSpace(img.Path))
.ToList(); .ToList();
// 如果没有图片,返回null // 如果没有图片,返回null
@@ -1169,8 +1171,8 @@ namespace dy.net.job
} }
var coverUrl = cate is not null && cate.CateType != VideoTypeEnum.dy_custom_collect var coverUrl = cate is not null && cate.CateType != VideoTypeEnum.dy_custom_collect
? (item.MixInfo?.CoverUrl?.UrlList?.FirstOrDefault() ?? imageUrls.FirstOrDefault() ?? item.Music?.CoverHd?.UrlList?.FirstOrDefault()) ? (item.MixInfo?.CoverUrl?.UrlList?.FirstOrDefault() ?? imageUrls.FirstOrDefault()?.Path ?? item.Music?.CoverHd?.UrlList?.FirstOrDefault())
: imageUrls.FirstOrDefault(); : imageUrls.FirstOrDefault()?.Path;
// 下载作者头像 // 下载作者头像
var avatarSavePath = await DownAuthorAvatar(cookie, item); var avatarSavePath = await DownAuthorAvatar(cookie, item);
@@ -1408,7 +1410,7 @@ namespace dy.net.job
/// <param name="cate">短剧、合集、自定义收藏夹</param> /// <param name="cate">短剧、合集、自定义收藏夹</param>
/// <returns>创建的视频实体对象</returns> /// <returns>创建的视频实体对象</returns>
private async Task<DouyinVideo> CreateVideoEntity(AppConfig config, 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); var (tag1, tag2, tag3) = GetVideoTags(item);
+1 -1
View File
@@ -1,6 +1,6 @@
namespace dy.net.model.dto namespace dy.net.model.dto
{ {
public class DouyinDynamicVideoDto public class DouyinMergeVideoDto
{ {
public string Path { get; set; } public string Path { get; set; }
+1 -1
View File
@@ -3,7 +3,7 @@
public class MediaMergeRequest public class MediaMergeRequest
{ {
/// <summary>网络图片地址数组(必填)</summary> /// <summary>网络图片地址数组(必填)</summary>
public List<string> ImageUrls { get; set; } public List<DouyinMergeVideoDto> ImageUrls { get; set; }
/// <summary>网络MP3地址数组(必填)</summary> /// <summary>网络MP3地址数组(必填)</summary>
public List<string> AudioUrls { get; set; } public List<string> AudioUrls { get; set; }
+32 -26
View File
@@ -36,7 +36,7 @@ namespace dy.net.service
/// <param name="ck"></param> /// <param name="ck"></param>
/// <returns></returns> /// <returns></returns>
public async Task<(string mp4Path, string mp3Path)> MergeMultipleVideosAsync( public async Task<(string mp4Path, string mp3Path)> MergeMultipleVideosAsync(
List<DouyinDynamicVideoDto> videoFilePaths, List<DouyinMergeVideoDto> videoFilePaths,
string audioPath, string audioPath,
string savePath, string savePath,
string ck) string ck)
@@ -45,11 +45,11 @@ namespace dy.net.service
string mergMusicPath = GetRandomMergeMusic(); string mergMusicPath = GetRandomMergeMusic();
if (!string.IsNullOrWhiteSpace(audioPath)) if (!string.IsNullOrWhiteSpace(audioPath))
{ {
var (SuccessPaths, _) = await DownloadMediaAsync(new List<string> { audioPath }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck); var (SuccessPaths, _) = await DownloadMediaAsync(new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= audioPath } }, Path.GetDirectoryName(savePath), "audio_", "mp3", ck);
if (SuccessPaths != null && SuccessPaths.Length > 0) if (SuccessPaths != null && SuccessPaths.Count > 0)
{ {
mergMusicPath = SuccessPaths[0]; mergMusicPath = SuccessPaths.FirstOrDefault()?.Path;
mp3Path = SuccessPaths[0]; mp3Path = SuccessPaths.FirstOrDefault()?.Path;
} }
} }
var ffmpeg = new FFmpegHelper(); var ffmpeg = new FFmpegHelper();
@@ -126,7 +126,7 @@ namespace dy.net.service
// 1. 下载图片 // 1. 下载图片
var (rawImages, imageError) = await DownloadMediaAsync( var (rawImages, imageError) = await DownloadMediaAsync(
request.ImageUrls, Path.Combine(tempDir, "raw-images"), "image_", "webp", cookie); 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 ?? ""}"); Log.Error($"图片下载失败:{imageError ?? ""}");
return false; return false;
@@ -138,12 +138,12 @@ namespace dy.net.service
} }
// 2. 下载音频 // 2. 下载音频
string[] rawAudios = Array.Empty<string>(); List<DouyinMergeVideoDto> rawAudios =new List<DouyinMergeVideoDto>();
if (mergeImg2Viedo && request.AudioUrls != null && request.AudioUrls.Count > 0) if (mergeImg2Viedo && request.AudioUrls != null && request.AudioUrls.Count > 0)
{ {
var (audios, audioError) = await DownloadMediaAsync( var (audios, audioError) = await DownloadMediaAsync(
request.AudioUrls, Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie); request.AudioUrls.Select(x => new DouyinMergeVideoDto { Path = x })?.ToList(), Path.Combine(tempDir, "raw-audios"), "audio_", "mp3", cookie);
if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Length == 0) if (!string.IsNullOrEmpty(audioError) || audios == null || audios.Count == 0)
{ {
Log.Error($"音频下载失败:{audioError ?? ""}"); Log.Error($"音频下载失败:{audioError ?? ""}");
return false; return false;
@@ -152,7 +152,7 @@ namespace dy.net.service
// 保存下载的音频(如果需要) // 保存下载的音频(如果需要)
if (downMp3) if (downMp3)
{ {
var ext = Path.GetExtension(rawAudios[0]); var ext = Path.GetExtension(rawAudios.FirstOrDefault()?.Path);
await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, ext, Path.GetFileNameWithoutExtension(outputVideoPath)); await SaveDownloadedFilesAsync(rawAudios, fileNamefolder, ext, Path.GetFileNameWithoutExtension(outputVideoPath));
} }
} }
@@ -192,16 +192,16 @@ namespace dy.net.service
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300)); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300));
// 执行合成(确保每个图片都参与) // 执行合成(确保每个图片都参与)
if (rawAudios.Length == 0) if (rawAudios.Count == 0)
{ {
var musicPath = GetRandomMergeMusic(); var musicPath = GetRandomMergeMusic();
rawAudios = new string[] { musicPath }; rawAudios = new List<DouyinMergeVideoDto> { new DouyinMergeVideoDto { Path= musicPath } };
Log.Debug("版权原因无法下载音频,使用默认无声音频文件"); Log.Debug("版权原因无法下载音频,使用默认无声音频文件");
} }
string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync( string resultPath = await ffmpegHelper.CreateVideoFromImagesAndAudioAsync(
rawImages, // 所有下载的图片(确保无遗漏) rawImages, // 所有下载的图片(确保无遗漏)
rawAudios[0], // 取第一个音频(可根据需求调整) rawAudios.FirstOrDefault()?.Path, // 取第一个音频(可根据需求调整)
outputVideoPath, outputVideoPath,
ffmpegHelper.VideoWidth, ffmpegHelper.VideoWidth,
ffmpegHelper.VideoHeight, ffmpegHelper.VideoHeight,
@@ -235,23 +235,23 @@ namespace dy.net.service
/// <summary> /// <summary>
/// 保存下载的文件(图片/音频)到目标目录 /// 保存下载的文件(图片/音频)到目标目录
/// </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); // 确保目标目录存在 Directory.CreateDirectory(targetFolder); // 确保目标目录存在
// 异步保存(不阻塞主线程) // 异步保存(不阻塞主线程)
await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) => await Task.WhenAll(sourcePaths.Select(async (sourcePath, index) =>
{ {
if (!File.Exists(sourcePath)) return; if (!File.Exists(sourcePath.Path)) return;
string extension = Path.GetExtension(sourcePath) ?? $".{defaultExt}"; string extension = Path.GetExtension(sourcePath.Path) ?? $".{defaultExt}";
string destFileName = $"{videoFileName}{index + 1:D3}{extension}"; string destFileName = $"{videoFileName}{index + 1:D3}{extension}";
string destPath = Path.Combine(targetFolder, destFileName); string destPath = Path.Combine(targetFolder, destFileName);
// 文件不存在时才复制,避免覆盖 // 文件不存在时才复制,避免覆盖
if (!File.Exists(destPath)) 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}"); Log.Debug($"已保存文件:{destPath}");
} }
})); }));
@@ -326,23 +326,29 @@ namespace dy.net.service
/// <summary>通用媒体下载方法</summary> /// <summary>通用媒体下载方法</summary>
private async Task<(string[] SuccessPaths, string ErrorMsg)> DownloadMediaAsync( private async Task<(List<DouyinMergeVideoDto> SuccessPaths, string ErrorMsg)> DownloadMediaAsync(
List<string> urls, string saveDir, string prefix, string ext, string cookie) 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++) for (var i = 0; i < urls.Count; i++)
{ {
var url = urls[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 fileName = $"{prefix}{i + 1}.{fileExt}";
var savePath = Path.Combine(saveDir, fileName); var savePath = Path.Combine(saveDir, fileName);
try try
{ {
var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url, savePath, cookie); var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(url.Path, savePath, cookie);
if (Success) if (Success)
{ {
successPaths.Add(ActualSavePath); successPaths.Add(new DouyinMergeVideoDto
{
Path = ActualSavePath,
Height = url.Height,
Width =
url.Width
});
} }
else else
{ {
@@ -354,10 +360,10 @@ namespace dy.net.service
{ {
var error = $"下载失败:{url},错误:{ex.Message}"; var error = $"下载失败:{url},错误:{ex.Message}";
Serilog.Log.Error(error); 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>
/// 将多张图片和一个音频文件合成为视频(最终终极版)。 /// 将多张图片和一个音频文件合成为视频(最终终极版)。
/// </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( public async Task<string> CreateVideoFromImagesAndAudioAsync(
IEnumerable<string> imageFilePaths, IEnumerable<DouyinMergeVideoDto> imageFilePaths,
string audioFilePath, string audioFilePath,
string outputVideoPath, string outputVideoPath,
int VideoWidth = 1080, int VideoWidth = 1080,
int Height = 1920, int VideoHeight = 1920,
IProgress<double> progress = null, IProgress<double> progress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
// 输入验证 // 输入验证
if (imageFilePaths == null || !imageFilePaths.Any()) if (imageFilePaths == null || !imageFilePaths.Any())
@@ -72,8 +244,11 @@ namespace dy.net.utils
foreach (var imagePath in imageFilePaths) foreach (var imagePath in imageFilePaths)
{ {
if (!File.Exists(imagePath)) if (!File.Exists(imagePath.Path))
throw new FileNotFoundException("图片文件未找到。", imagePath); 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); var outputDirectory = Path.GetDirectoryName(outputVideoPath);
@@ -91,22 +266,20 @@ namespace dy.net.utils
{ {
for (int i = 0; i < imageList.Count; i++) for (int i = 0; i < imageList.Count; i++)
{ {
string sourcePath = imageList[i]; string sourcePath = imageList[i].Path;
string extension = Path.GetExtension(sourcePath); string extension = Path.GetExtension(sourcePath);
string destFileName = $"temp_{i + 1:D3}{extension}"; string destFileName = $"temp_{i + 1:D3}{extension}";
string destPath = Path.Combine(tempImageDir, destFileName); 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); double imageFps = Math.Round(1.0 / ImageDisplayDurationSeconds, 2);
// --- 修正点 1: 整合音频时长获取逻辑 --- // 获取音频时长
// 我们不再需要 GetAudioFilterAsync,而是直接获取时长用于计算视频循环 double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds;
double audioDurationSeconds = imageList.Count * ImageDisplayDurationSeconds; // 默认值为图片总时长
try try
{ {
// 使用 ffprobe 获取音频时长
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = _ffprobeExecutablePath, FileName = _ffprobeExecutablePath,
@@ -126,103 +299,112 @@ namespace dy.net.utils
if (double.TryParse(output, out double duration)) if (double.TryParse(output, out double duration))
{ {
audioDurationSeconds = duration; audioDurationSeconds = duration;
//Console.WriteLine($"成功获取音频时长: {audioDurationSeconds:F2}s");
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}"); Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
} }
// 计算图片序列需要循环次数 // 计算图片序列循环次数
int loopCount = 1; int loopCount = 1;
int imageCount = imageList.Count; // 要循环的图片总数(对应loop的size参数) int imageCount = imageList.Count;
double singleLoopDuration = imageCount * ImageDisplayDurationSeconds; double singleLoopDuration = imageCount * ImageDisplayDurationSeconds;
if (audioDurationSeconds > singleLoopDuration) if (audioDurationSeconds > singleLoopDuration)
{ {
loopCount = (int)Math.Ceiling(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> var arguments = new List<string>
{ {
"-y", // 覆盖输出文件 "-y", // 覆盖输出文件
// 图片序列输入:保留原有逻辑,修复帧率含义(-r 是输出帧率,输入用-framerate更准确) // 图片序列输入
"-f", "image2", "-f", "image2",
"-framerate", imageFps.ToString(CultureInfo.InvariantCulture), // 输入帧率(图片播放速度) "-framerate", imageFps.ToString(CultureInfo.InvariantCulture),
"-start_number", "0", // 序列起始编号 "-start_number", "1", // 修正:临时文件是 temp_001、temp_002... 所以起始编号为1
"-i", imageSequencePattern, // 图片序列模板(无需引号,后续用ProcessStartInfo传参更安全) "-i", imageSequencePattern,
// 音频输入 // 音频输入
"-i", audioFilePath, "-i", audioFilePath,
// 滤镜修复:补充size参数(核心!解决Number of frames to loop is not set报错) // 滤镜:尺寸适配 + 循环
// loop=循环次数:size=要循环的帧数;loop=-1表示无限循环(配合-shortest更简洁) "-filter_complex", filterComplex,
"-filter_complex", $"[0:v]loop=loop={loopCount - 1}:size={imageCount}[v]",
// 流映射 // 流映射
"-map", "[v]", "-map", "[v]",
"-map", "1:a", "-map", "1:a",
// 视频编码:保留你的变量,确保兼容性 // 视频编码参数
"-c:v", VideoCodec, "-c:v", VideoCodec,
"-preset", VideoPreset, "-preset", VideoPreset,
"-crf", $"{VideoCrf}", "-crf", $"{VideoCrf}",
"-s", $"{VideoWidth}x{VideoHeight}", "-s", $"{VideoWidth}x{VideoHeight}",
"-pix_fmt", "yuv420p", // 兼容所有播放器 "-pix_fmt", "yuv420p",
"-profile:v", "main", "-profile:v", "main",
// 音频编码:标准化参数,避免无效值 // 音频编码参数
"-c:a", AudioCodec, "-c:a", AudioCodec,
"-b:a", $"{AudioBitrate}", "-b:a", $"{AudioBitrate}",
"-ac", "2", // 立体声 "-ac", "2",
"-ar", "44100", // 标准采样率 "-ar", "44100",
// 封装优化 // 封装优化
"-f", "mp4", "-f", "mp4",
"-movflags", "+faststart", "-movflags", "+faststart",
// 同步参数:确保视频时长匹配音频 // 同步参数
"-shortest", "-shortest",
// 输出路径 // 输出路径
outputVideoPath outputVideoPath
}; };
// 执行命令 // 执行FFmpeg命令
await ExecuteFFmpegAsync(arguments, progress, cancellationToken); await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
if (File.Exists(outputVideoPath))
{ if (!File.Exists(outputVideoPath))
return outputVideoPath;
}
else
{ {
throw new InvalidOperationException("视频合成失败,未生成输出文件。"); throw new InvalidOperationException("视频合成失败,未生成输出文件。");
} }
return outputVideoPath;
} }
finally finally
{ {
// 清理临时目录 // 清理临时目录(增加异常捕获,避免删除失败导致程序崩溃)
if (Directory.Exists(tempImageDir)) if (Directory.Exists(tempImageDir))
{ {
Directory.Delete(tempImageDir, recursive: true); try
{
Directory.Delete(tempImageDir, recursive: true);
}
catch (Exception ex)
{
Log.Warning($"清理临时目录失败: {ex.Message}");
}
} }
} }
} }
/// <summary> /// <summary>
/// 将单个视频转封装为统一编码/分辨率的临时视频(解决concat合并兼容问题) /// 将单个视频转封装为统一编码/分辨率的临时视频(解决concat合并兼容问题)
/// </summary> /// </summary>
@@ -232,7 +414,7 @@ namespace dy.net.utils
/// <param name="cancellationToken">取消令牌</param> /// <param name="cancellationToken">取消令牌</param>
/// <returns>统一参数后的临时视频路径</returns> /// <returns>统一参数后的临时视频路径</returns>
private async Task<string> CreateUnifiedTempVideoAsync( private async Task<string> CreateUnifiedTempVideoAsync(
DouyinDynamicVideoDto videoDto, DouyinMergeVideoDto videoDto,
int targetWidth, // 外部传入的动态目标宽(已取所有视频的最大值,偶数) int targetWidth, // 外部传入的动态目标宽(已取所有视频的最大值,偶数)
int targetHeight, // 外部传入的动态目标高(已取所有视频的最大值,偶数) int targetHeight, // 外部传入的动态目标高(已取所有视频的最大值,偶数)
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -325,13 +507,13 @@ namespace dy.net.utils
/// <returns>输出视频路径</returns> /// <returns>输出视频路径</returns>
// 移除了 width = 1080 和 height = 1920 两个传入参数 // 移除了 width = 1080 和 height = 1920 两个传入参数
public async Task<string> MergeMultipleVideosAsync( public async Task<string> MergeMultipleVideosAsync(
List<DouyinDynamicVideoDto> videoFilePaths, List<DouyinMergeVideoDto> videoFilePaths,
string audioPath, string audioPath,
string savePath, string savePath,
IProgress<double> progress = null, IProgress<double> progress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
List<DouyinDynamicVideoDto> validVideoList = new List<DouyinDynamicVideoDto>(); List<DouyinMergeVideoDto> validVideoList = new List<DouyinMergeVideoDto>();
List<string> tempUnifiedVideoPaths = new List<string>(); List<string> tempUnifiedVideoPaths = new List<string>();
// 输入验证 // 输入验证