diff --git a/Properties/PublishProfiles/docker.pubxml.user b/Properties/PublishProfiles/docker.pubxml.user
index c931ce2..a31a472 100644
--- a/Properties/PublishProfiles/docker.pubxml.user
+++ b/Properties/PublishProfiles/docker.pubxml.user
@@ -3,7 +3,7 @@
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\
- True|2026-01-16T13:00:57.7239379Z||;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||;True|2026-01-15T21:40:13.2613927+08:00||;True|2026-01-15T20:57:56.6291682+08:00||;True|2026-01-15T20:57:47.1363536+08:00||;True|2026-01-15T20:08:44.4710883+08:00||;True|2026-01-15T20:04:51.8284314+08:00||;False|2026-01-15T20:04:45.3258155+08:00||;True|2026-01-15T19:59:11.7907672+08:00||;True|2026-01-15T01:10:48.9066941+08:00||;True|2026-01-15T01:03:23.6216975+08:00||;True|2026-01-15T01:03:16.3364591+08:00||;False|2026-01-15T01:03:10.5654436+08:00||;True|2026-01-15T00:49:21.6559622+08:00||;True|2026-01-15T00:10:42.7288439+08:00||;True|2026-01-15T00:06:28.4241682+08:00||;True|2026-01-13T22:34:15.2372411+08:00||;True|2026-01-13T22:26:46.4522947+08:00||;True|2026-01-13T22:15:56.9735179+08:00||;True|2026-01-13T22:15:51.8250677+08:00||;False|2026-01-13T22:15:46.6608690+08:00||;True|2026-01-13T22:03:06.4683237+08:00||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;True|2026-01-13T15:07:50.0836745+08:00||;False|2026-01-13T15:06:35.5929685+08:00||;True|2026-01-13T10:21:41.6012776+08:00||;True|2026-01-13T09:57:25.4788847+08:00||;True|2026-01-13T09:53:14.4900360+08:00||;True|2026-01-13T09:23:53.6465295+08:00||;True|2026-01-12T22:04:14.5886955+08:00||;False|2026-01-12T22:04:08.2008101+08:00||;True|2026-01-12T21:53:46.9947176+08:00||;True|2026-01-12T21:11:15.8358024+08:00||;True|2026-01-12T21:09:51.8663228+08:00||;True|2026-01-11T21:25:07.5052845+08:00||;False|2026-01-11T21:24:27.5744557+08:00||;True|2026-01-11T19:59:15.4734611+08:00||;False|2026-01-11T19:59:04.9543339+08:00||;True|2026-01-11T18:51:30.6649269+08:00||;True|2026-01-08T21:07:57.5094466+08:00||;True|2026-01-08T14:51:18.2266231+08:00||;True|2026-01-08T14:31:55.1137120+08:00||;True|2026-01-08T00:16:24.4451490+08:00||;True|2026-01-08T00:14:20.7430793+08:00||;True|2026-01-08T00:07:46.7228194+08:00||;True|2026-01-07T23:52:13.3290082+08:00||;True|2026-01-07T23:49:28.4838650+08:00||;True|2026-01-07T23:47:45.1936189+08:00||;True|2026-01-07T23:35:09.7818611+08:00||;True|2026-01-07T23:20:43.7462863+08:00||;True|2026-01-07T23:04:39.5140429+08:00||;False|2026-01-07T23:04:36.5367611+08:00||;True|2026-01-07T22:53:14.6013449+08:00||;True|2026-01-07T22:49:59.9549006+08:00||;True|2026-01-07T22:36:40.0254856+08:00||;False|2026-01-07T22:36:28.8275903+08:00||;True|2026-01-07T21:59:51.4355171+08:00||;True|2026-01-03T21:38:51.4307034+08:00||;True|2026-01-02T19:09:16.9668906+08:00||;False|2026-01-02T19:09:11.7496369+08:00||;True|2026-01-02T15:42:08.2215697+08:00||;True|2026-01-02T09:46:56.1654861+08:00||;True|2026-01-02T09:35:28.0211225+08:00||;True|2026-01-01T23:07:01.9022045+08:00||;False|2026-01-01T23:06:56.0537216+08:00||;True|2026-01-01T22:16:10.2974067+08:00||;True|2026-01-01T22:16:06.2123787+08:00||;False|2026-01-01T22:15:33.3626979+08:00||;True|2026-01-01T22:01:30.7161900+08:00||;True|2026-01-01T21:10:19.3664263+08:00||;True|2026-01-01T21:09:38.6071080+08:00||;True|2025-12-30T11:45:54.5543034+08:00||;True|2025-12-30T09:19:08.3178124+08:00||;True|2025-12-29T18:57:29.7032246+08:00||;True|2025-12-27T14:52:24.4780776+08:00||;False|2025-12-27T14:52:19.5635794+08:00||;True|2025-12-27T14:48:01.6252748+08:00||;False|2025-12-27T14:47:55.7976192+08:00||;True|2025-12-27T14:38:23.9723838+08:00||;True|2025-12-27T13:00:29.8583858+08:00||;True|2025-12-26T22:18:42.4015637+08:00||;True|2025-12-26T22:10:58.5274572+08:00||;True|2025-12-26T22:06:26.3129600+08:00||;True|2025-12-26T22:03:55.6718618+08:00||;False|2025-12-26T22:03:48.3809954+08:00||;True|2025-12-26T22:02:33.1840390+08:00||;True|2025-12-26T22:01:16.1660100+08:00||;True|2025-12-25T00:50:26.1116465+08:00||;True|2025-12-25T00:48:27.3087708+08:00||;True|2025-12-25T00:47:38.4835720+08:00||;
+ True|2026-01-16T16:45:04.0498090Z||;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||;True|2026-01-15T21:40:13.2613927+08:00||;True|2026-01-15T20:57:56.6291682+08:00||;True|2026-01-15T20:57:47.1363536+08:00||;True|2026-01-15T20:08:44.4710883+08:00||;True|2026-01-15T20:04:51.8284314+08:00||;False|2026-01-15T20:04:45.3258155+08:00||;True|2026-01-15T19:59:11.7907672+08:00||;True|2026-01-15T01:10:48.9066941+08:00||;True|2026-01-15T01:03:23.6216975+08:00||;True|2026-01-15T01:03:16.3364591+08:00||;False|2026-01-15T01:03:10.5654436+08:00||;True|2026-01-15T00:49:21.6559622+08:00||;True|2026-01-15T00:10:42.7288439+08:00||;True|2026-01-15T00:06:28.4241682+08:00||;True|2026-01-13T22:34:15.2372411+08:00||;True|2026-01-13T22:26:46.4522947+08:00||;True|2026-01-13T22:15:56.9735179+08:00||;True|2026-01-13T22:15:51.8250677+08:00||;False|2026-01-13T22:15:46.6608690+08:00||;True|2026-01-13T22:03:06.4683237+08:00||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;True|2026-01-13T15:07:50.0836745+08:00||;False|2026-01-13T15:06:35.5929685+08:00||;True|2026-01-13T10:21:41.6012776+08:00||;True|2026-01-13T09:57:25.4788847+08:00||;True|2026-01-13T09:53:14.4900360+08:00||;True|2026-01-13T09:23:53.6465295+08:00||;True|2026-01-12T22:04:14.5886955+08:00||;False|2026-01-12T22:04:08.2008101+08:00||;True|2026-01-12T21:53:46.9947176+08:00||;True|2026-01-12T21:11:15.8358024+08:00||;True|2026-01-12T21:09:51.8663228+08:00||;True|2026-01-11T21:25:07.5052845+08:00||;False|2026-01-11T21:24:27.5744557+08:00||;True|2026-01-11T19:59:15.4734611+08:00||;False|2026-01-11T19:59:04.9543339+08:00||;True|2026-01-11T18:51:30.6649269+08:00||;True|2026-01-08T21:07:57.5094466+08:00||;True|2026-01-08T14:51:18.2266231+08:00||;True|2026-01-08T14:31:55.1137120+08:00||;True|2026-01-08T00:16:24.4451490+08:00||;True|2026-01-08T00:14:20.7430793+08:00||;True|2026-01-08T00:07:46.7228194+08:00||;True|2026-01-07T23:52:13.3290082+08:00||;True|2026-01-07T23:49:28.4838650+08:00||;True|2026-01-07T23:47:45.1936189+08:00||;True|2026-01-07T23:35:09.7818611+08:00||;True|2026-01-07T23:20:43.7462863+08:00||;True|2026-01-07T23:04:39.5140429+08:00||;False|2026-01-07T23:04:36.5367611+08:00||;True|2026-01-07T22:53:14.6013449+08:00||;True|2026-01-07T22:49:59.9549006+08:00||;True|2026-01-07T22:36:40.0254856+08:00||;False|2026-01-07T22:36:28.8275903+08:00||;True|2026-01-07T21:59:51.4355171+08:00||;True|2026-01-03T21:38:51.4307034+08:00||;True|2026-01-02T19:09:16.9668906+08:00||;False|2026-01-02T19:09:11.7496369+08:00||;True|2026-01-02T15:42:08.2215697+08:00||;True|2026-01-02T09:46:56.1654861+08:00||;True|2026-01-02T09:35:28.0211225+08:00||;True|2026-01-01T23:07:01.9022045+08:00||;False|2026-01-01T23:06:56.0537216+08:00||;True|2026-01-01T22:16:10.2974067+08:00||;True|2026-01-01T22:16:06.2123787+08:00||;False|2026-01-01T22:15:33.3626979+08:00||;True|2026-01-01T22:01:30.7161900+08:00||;True|2026-01-01T21:10:19.3664263+08:00||;True|2026-01-01T21:09:38.6071080+08:00||;True|2025-12-30T11:45:54.5543034+08:00||;True|2025-12-30T09:19:08.3178124+08:00||;True|2025-12-29T18:57:29.7032246+08:00||;True|2025-12-27T14:52:24.4780776+08:00||;False|2025-12-27T14:52:19.5635794+08:00||;True|2025-12-27T14:48:01.6252748+08:00||;False|2025-12-27T14:47:55.7976192+08:00||;True|2025-12-27T14:38:23.9723838+08:00||;True|2025-12-27T13:00:29.8583858+08:00||;True|2025-12-26T22:18:42.4015637+08:00||;True|2025-12-26T22:10:58.5274572+08:00||;True|2025-12-26T22:06:26.3129600+08:00||;True|2025-12-26T22:03:55.6718618+08:00||;False|2025-12-26T22:03:48.3809954+08:00||;True|2025-12-26T22:02:33.1840390+08:00||;True|2025-12-26T22:01:16.1660100+08:00||;True|2025-12-25T00:50:26.1116465+08:00||;True|2025-12-25T00:48:27.3087708+08:00||;True|2025-12-25T00:47:38.4835720+08:00||;
\ No newline at end of file
diff --git a/dto/DouyinDynamicVideoDto.cs b/dto/DouyinDynamicVideoDto.cs
new file mode 100644
index 0000000..5feeb8e
--- /dev/null
+++ b/dto/DouyinDynamicVideoDto.cs
@@ -0,0 +1,11 @@
+namespace dy.net.dto
+{
+ public class DouyinDynamicVideoDto
+ {
+ public string Path { get; set; }
+
+ public int Height { get; set; }
+
+ public int Width { get; set; }
+ }
+}
diff --git a/dy.net.csproj.user b/dy.net.csproj.user
index c0a06db..b37b1e2 100644
--- a/dy.net.csproj.user
+++ b/dy.net.csproj.user
@@ -2,7 +2,7 @@
tiny.ddns
- E:\code\dysync\Properties\PublishProfiles\fn.pubxml
+ E:\code\dysync\Properties\PublishProfiles\docker.pubxml
ApiControllerEmptyScaffolder
root/Common/Api
diff --git a/job/DouyinBasicSyncJob.cs b/job/DouyinBasicSyncJob.cs
index 3312e50..bdd16aa 100644
--- a/job/DouyinBasicSyncJob.cs
+++ b/job/DouyinBasicSyncJob.cs
@@ -448,10 +448,10 @@ namespace dy.net.job
var videos = new List();
foreach (var item in data.AwemeList)
{
- //if (item.AwemeId != "7440444948844956985")
- //{
- // continue;
- //}
+ if (item.AwemeId != "7440444948844956985")
+ {
+ continue;
+ }
//if (!item.Desc.Contains("小房车正式启程"))
//{
// continue;
@@ -495,7 +495,7 @@ namespace dy.net.job
{
//处理多个视频-组合的图文视频--类似动图。
- List dynamicVideoUrls = new List();
+ List dynamicVideoUrls = new List();
// 当需要下载动态视频时,获取其他URL
if (config.DownDynamicVideo && item.Images != null && item.Images.Count > 0)
{
@@ -507,7 +507,17 @@ namespace dy.net.job
{
var targetUrl = btv.PlayAddr?.UrlList?.FirstOrDefault(x => x.StartsWith("https://www.douyin.com/aweme/v1/play"));
if (targetUrl != null)
- dynamicVideoUrls.Add(targetUrl);
+ {
+ var height = btv.PlayAddr?.Height ?? 1920;
+ var width = btv.PlayAddr?.Width ?? 1080;
+ DouyinDynamicVideoDto info = new DouyinDynamicVideoDto
+ {
+ Path = targetUrl,
+ Height = height,
+ Width = width
+ };
+ dynamicVideoUrls.Add(info);
+ }
}
}
}
@@ -522,37 +532,34 @@ namespace dy.net.job
{
if (!string.IsNullOrEmpty(dynamicVideo.DynamicVideos))
{
- var dynamicVideos = JsonConvert.DeserializeObject>(dynamicVideo.DynamicVideos);
+ var dynamicVideos = JsonConvert.DeserializeObject>(dynamicVideo.DynamicVideos);
Log.Debug($"{VideoType}-动态视频[{item.Desc}],下载成功 ,共{dynamicVideos?.Count}个视频...");
if (dynamicVideos != null && dynamicVideos.Count > 0)
{
- int width = 1080;
- int height = 1920;
- var bit = item?.Video?.BitRate?.FirstOrDefault();
- if (bit != null)
- {
- width = bit.PlayAddr.Width;
- height = bit.PlayAddr.Height;
- }
+
var savePath = DouyinFileNameHelper.RemoveNumberSuffix(dynamicVideo.VideoSavePath);
- //下载音频文件
+ //音频文件下载地址
var mp3Url= item?.Music?.PlayUrl?.UrlList?.FirstOrDefault();
- var outPath = await douyinMergeVideoService.MergeMultipleVideosAsync(dynamicVideos,mp3Url, savePath,cookie.Cookies, width, height);
- if(!string.IsNullOrWhiteSpace(outPath))
+ var outPath = await douyinMergeVideoService.MergeMultipleVideosAsync(dynamicVideos,mp3Url, savePath,cookie.Cookies);
+ if(!string.IsNullOrWhiteSpace(outPath.mp4Path))
{
- if (File.Exists(outPath))
+ if (File.Exists(outPath.mp4Path))
{
- dynamicVideo.VideoSavePath = outPath;
+ dynamicVideo.VideoSavePath = outPath.mp4Path;
if (!config.KeepDynamicVideo)
{
+ if (File.Exists(outPath.mp3Path))
+ {
+ File.Delete(outPath.mp3Path);
+ }
//不保留原视频-删除
foreach (var opath in dynamicVideos)
{
- if (File.Exists(opath))
- File.Delete(opath);
+ if (File.Exists(opath.Path))
+ File.Delete(opath.Path);
}
}
}
@@ -829,7 +836,7 @@ namespace dy.net.job
- protected async Task ProcessDynamicVideo(List dynamicUrls, DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null)
+ protected async Task ProcessDynamicVideo(List dynamicUrls, DouyinCookie cookie, Aweme item, DouyinVideoInfo data, AppConfig config, DouyinFollowed followed = null)
{
@@ -848,28 +855,29 @@ namespace dy.net.job
// 随机延迟
int i = 1;
- List dynamicSavePaths = new List();
+ List dynamicSavePaths = new List();
foreach (var dynamicUrl in dynamicUrls)
{
// 下载动态视频
var dynamicSavePath = savePath.Replace(".mp4", $"_00{i}.mp4");
- i++;
+ DouyinDynamicVideoDto v = new DouyinDynamicVideoDto() { Height = dynamicUrl.Height, Width = dynamicUrl.Width,Path= dynamicSavePath };
+ i++;
// 如果文件已存在,跳过
if (File.Exists(dynamicSavePath))
{
//Log.Debug($"{VideoType}-视频[{DouyinFileNameHelper.SanitizePath(item.Desc)}]已存在,跳过下载.");
- dynamicSavePaths.Add(dynamicSavePath);
+ dynamicSavePaths.Add(v);
continue;
}
- var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(dynamicUrl, dynamicSavePath, cookie.Cookies);
+ var (Success, ActualSavePath) = await douyinHttpClientService.DownloadAsync(dynamicUrl.Path, dynamicSavePath, cookie.Cookies);
if (!Success)
{
Log.Error($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{dynamicSavePath}]-00{i},下载失败!!!");
}
else
{
- dynamicSavePaths.Add(dynamicSavePath);
+ dynamicSavePaths.Add(v);
Log.Debug($"{VideoType}-{item?.Author?.Nickname ?? ""}-视频[{dynamicSavePath}]-00{i},下载完成.");
}
await Task.Delay(_random.Next(1, 4) * 1000);
@@ -890,10 +898,10 @@ namespace dy.net.job
{
Width = item?.Images?.FirstOrDefault()?.Width ?? 0,
Height = item?.Images?.FirstOrDefault()?.Height ?? 0,
- DataSize = DouyinFileUtils.GetTotalFileSize(dynamicSavePaths) // 合成视频的文件大小
+ DataSize = DouyinFileUtils.GetTotalFileSize(dynamicSavePaths.Select(x=>x.Path).ToList()) // 合成视频的文件大小
}
};
- return await CreateVideoEntity(config, cookie, item, virtualBitRate, dynamicSavePaths.FirstOrDefault(), saveFolder, tag1, tag2, tag3, "", "", data, dynamicSavePaths);
+ return await CreateVideoEntity(config, cookie, item, virtualBitRate, dynamicSavePaths.FirstOrDefault()?.Path, saveFolder, tag1, tag2, tag3, "", "", data, dynamicSavePaths);
}
@@ -1285,7 +1293,7 @@ namespace dy.net.job
/// 创建的视频实体对象
private async Task CreateVideoEntity(AppConfig config,
DouyinCookie cookie, Aweme item, VideoBitRate bitRate, string savePath, string saveFolder,
- string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data, List dynamicVideos = null)
+ string tag1, string tag2, string tag3, string avatarSavePath, string avatarUrl, DouyinVideoInfo data, List dynamicVideos = null)
{
var diffs = GetVideoEntityDifferences(cookie, item);
diff --git a/service/DouyinMergeVideoService.cs b/service/DouyinMergeVideoService.cs
index 09a8ba2..878294f 100644
--- a/service/DouyinMergeVideoService.cs
+++ b/service/DouyinMergeVideoService.cs
@@ -36,17 +36,14 @@ namespace dy.net.service
///
///
///
- ///
- ///
///
- public async Task MergeMultipleVideosAsync(
- List videoFilePaths,
+ public async Task<(string mp4Path,string mp3Path)> MergeMultipleVideosAsync(
+ List videoFilePaths,
string audioPath,
string savePath,
- string ck,
- int width = 1080,
- int height = 1920)
+ string ck)
{
+ string mp3Path = string.Empty;
string mergMusicPath = GetRandomMergeMusic();
if (!string.IsNullOrWhiteSpace(audioPath))
{
@@ -54,10 +51,12 @@ namespace dy.net.service
if (SuccessPaths != null && SuccessPaths.Length > 0)
{
mergMusicPath = SuccessPaths[0];
+ mp3Path= SuccessPaths[0];
}
}
var ffmpeg = new FFmpegHelper();
- return await ffmpeg.MergeMultipleVideosAsync(videoFilePaths, mergMusicPath, savePath, width, height);
+ var mp4Path= await ffmpeg.MergeMultipleVideosAsync(videoFilePaths, mergMusicPath, savePath);
+ return (mp4Path, mp3Path);
}
diff --git a/utils/FFmpegHelper.cs b/utils/FFmpegHelper.cs
index 41d09eb..669faa3 100644
--- a/utils/FFmpegHelper.cs
+++ b/utils/FFmpegHelper.cs
@@ -1,18 +1,19 @@
-using Serilog;
+using dy.net.dto;
+using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
namespace dy.net.utils
{
public class FFmpegHelper : IDisposable
{
-
-
private string _ffmpegExecutablePath;
private string _ffprobeExecutablePath;
@@ -53,7 +54,6 @@ namespace dy.net.utils
public string AudioCodec { get; set; } = "aac";
public string AudioBitrate { get; set; } = "192k";
-
///
/// 将多张图片和一个音频文件合成为视频(最终终极版)。
///
@@ -141,7 +141,6 @@ namespace dy.net.utils
Serilog.Log.Error($"获取音频时长失败,将使用图片总时长 ({audioDurationSeconds:F2}s) 作为视频时长: {ex.Message}");
}
-
// 计算图片序列需要循环的次数
int loopCount = 1;
int imageCount = imageList.Count; // 要循环的图片总数(对应loop的size参数)
@@ -218,7 +217,6 @@ namespace dy.net.utils
{
throw new InvalidOperationException("视频合成失败,未生成输出文件。");
}
-
}
finally
{
@@ -232,48 +230,173 @@ namespace dy.net.utils
///
- /// 合并多个视频文件为一个MP4视频(嵌入指定音频文件)
+ /// 将单个视频转封装为统一编码/分辨率的临时视频(解决concat合并兼容问题)
+ ///
+ /// 待转封装的视频Dto
+ /// 目标输出宽度
+ /// 目标输出高度
+ /// 取消令牌
+ /// 统一参数后的临时视频路径
+ private async Task CreateUnifiedTempVideoAsync(
+ DouyinDynamicVideoDto videoDto,
+ int targetWidth, // 外部传入的动态目标宽(已取所有视频的最大值,偶数)
+ int targetHeight, // 外部传入的动态目标高(已取所有视频的最大值,偶数)
+ CancellationToken cancellationToken)
+ {
+ // 1. 生成临时视频路径
+ string tempVideoDir = Path.Combine(AppContext.BaseDirectory, "temp");
+ if (!Directory.Exists(tempVideoDir))
+ {
+ Directory.CreateDirectory(tempVideoDir);
+ }
+ string tempVideoPath = Path.Combine(tempVideoDir, $"{Guid.NewGuid()}.mp4");
+
+ // 2. 构建【动态适配不固定尺寸】的极简滤镜字符串(核心修改)
+ // 无需写死尺寸,直接使用传入的动态目标分辨率(已保证为偶数)
+ // scale=targetWidth:-2:宽度最大为动态目标宽,高度按原比例自动缩放,-2强制高度为偶数(满足H264)
+ // pad=targetWidth:targetHeight:居中补黑边到动态目标分辨率,缩放后尺寸≤目标尺寸,无矛盾
+ // 核心:动态双向限制滤镜(不写死任何尺寸,使用传入的targetWidth/targetHeight)
+ // scale={0}:{1}:force_original_aspect_ratio=decrease:双向限制不超出动态目标尺寸,保持原比例
+ // pad={0}:{1}:动态填充到目标尺寸,无尺寸矛盾,无硬编码
+ string videoFilter = string.Format(CultureInfo.InvariantCulture,
+ "scale={0}:{1}:force_original_aspect_ratio=decrease,pad={0}:{1}:(ow-iw)/2:(oh-ih)/2:black",
+ targetWidth, // 动态传入的目标宽度(无硬编码)
+ targetHeight // 动态传入的目标高度(无硬编码)
+ );
+
+ // 3. 打印滤镜字符串(用于调试,验证动态尺寸是否正确)
+ //Log.Debug($"待执行的动态滤镜(适配不固定尺寸):{videoFilter}");
+
+ // 4. 构建FFmpeg参数(无需对滤镜做任何转义,保留原有编码配置)
+ var arguments = new List
+ {
+ "-y",
+ "-hide_banner",
+ "-loglevel", "error",
+ "-i", videoDto.Path,
+ "-vf", // 滤镜参数标识(独立参数)
+ videoFilter, // 动态滤镜字符串(独立参数,ArgumentList自动处理逗号)
+ "-c:v", VideoCodec,
+ "-preset", VideoPreset,
+ "-crf", $"{VideoCrf}",
+ "-pix_fmt", "yuv420p",
+ "-profile:v", "main",
+ // 关键帧计算也适配动态目标宽,保留原有逻辑
+ "-g", $"{(targetWidth < 1080 ? 60 : 120)}",
+ "-sc_threshold", "0",
+ "-c:a", AudioCodec,
+ "-b:a", AudioBitrate,
+ "-ac", "2",
+ "-ar", "44100",
+ "-strict", "-2",
+ "-f", "mp4",
+ "-movflags", "+faststart",
+ tempVideoPath
+ };
+
+ try
+ {
+ // 5. 复用现有ExecuteFFmpegAsync(已使用ArgumentList,自动处理特殊字符)
+ await ExecuteFFmpegAsync(arguments, null, cancellationToken);
+
+ if (File.Exists(tempVideoPath))
+ {
+ //Log.Debug($"视频转封装为统一参数成功,临时路径:{tempVideoPath}");
+ return tempVideoPath;
+ }
+ else
+ {
+ //Log.Error($"视频转封装失败,未生成临时文件,原路径:{videoDto.Path}");
+ return string.Empty;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"视频转封装异常,原路径:{videoDto.Path}");
+ if (File.Exists(tempVideoPath))
+ {
+ File.Delete(tempVideoPath);
+ }
+ return string.Empty;
+ }
+ }
+ ///
+ /// 合并多个视频文件为一个MP4视频(嵌入指定音频文件)【已优化:移除冗余width/height参数,自动提取基准分辨率】
///
/// 待合并的视频路径列表(按合并顺序排列)
/// 音频文件(可为null/空,为空则保留视频原音频(若有))
/// 输出视频的保存路径
- /// 输出视频宽度(自动修正为偶数)
- /// 输出视频高度(自动修正为偶数)
/// 进度回调
/// 取消令牌
/// 输出视频路径
+ // 移除了 width = 1080 和 height = 1920 两个传入参数
public async Task MergeMultipleVideosAsync(
- List videoFilePaths,
+ List videoFilePaths,
string audioPath,
string savePath,
- int width = 1080,
- int height = 1920,
IProgress progress = null,
CancellationToken cancellationToken = default)
{
+ List validVideoList = new List();
+ List tempUnifiedVideoPaths = new List();
+
// 输入验证
if (videoFilePaths == null || !videoFilePaths.Any())
- throw new ArgumentException("视频路径列表不能为空。", nameof(videoFilePaths));
-
- foreach (var videoPath in videoFilePaths)
{
- if (!File.Exists(videoPath))
- throw new FileNotFoundException("视频文件未找到。", videoPath);
+ //Serilog.Log.Error("待合并视频列表为空。");
+ return string.Empty;
+ }
+
+ // 筛选有效视频 + 修正Dto中的视频自身宽高为偶数(H264编码要求)
+ foreach (var videoDto in videoFilePaths)
+ {
+ if (videoDto == null || string.IsNullOrEmpty(videoDto.Path) || !File.Exists(videoDto.Path))
+ {
+ //Serilog.Log.Error($"视频文件未找到:{videoDto?.Path ?? "空路径"}");
+ continue;
+ }
+
+ // 修正视频自身宽高(无效值赋予类默认分辨率,偶数修正)
+ videoDto.Width = videoDto.Width <= 0 ? this.VideoWidth : (videoDto.Width % 2 != 0 ? videoDto.Width + 1 : videoDto.Width);
+ videoDto.Height = videoDto.Height <= 0 ? this.VideoHeight : (videoDto.Height % 2 != 0 ? videoDto.Height + 1 : videoDto.Height);
+
+ validVideoList.Add(videoDto);
+ }
+
+ // 无有效视频直接返回
+ if (!validVideoList.Any())
+ {
+ Serilog.Log.Error("无可用的有效视频文件,无法进行合并。");
+ return string.Empty;
}
if (string.IsNullOrEmpty(savePath))
- throw new ArgumentNullException(nameof(savePath));
+ {
+ //Serilog.Log.Error("合并视频输出文件名为空");
+ return string.Empty;
+ }
- // 音频文件验证(若传入非空路径,则校验文件是否存在)
+ // 自动创建输出目录
+ var saveDir = Path.GetDirectoryName(savePath);
+ if (!string.IsNullOrEmpty(saveDir) && !Directory.Exists(saveDir))
+ {
+ Directory.CreateDirectory(saveDir);
+ }
+
+ // 音频文件验证
bool hasCustomAudio = !string.IsNullOrEmpty(audioPath) && File.Exists(audioPath);
- //if (!string.IsNullOrEmpty(audioPath) && !hasCustomAudio)
- // throw new FileNotFoundException("指定的音频文件未找到。", audioPath);
- // 自动修正分辨率为偶数(H264编码要求)
- if (width % 2 != 0) width++;
- if (height % 2 != 0) height++;
+ // 核心修改:提取第一个有效视频的分辨率作为基准输出分辨率(替代传入的width/height)
+ // 进阶:动态计算最优分辨率(取所有有效视频的最大宽、最大高)
+ int targetWidth = validVideoList.Max(v => v.Width);
+ int targetHeight = validVideoList.Max(v => v.Height);
+ // 再次确认偶数(双重保障,避免Dto修正逻辑遗漏)
+ targetWidth = targetWidth % 2 != 0 ? targetWidth + 1 : targetWidth;
+ targetHeight = targetHeight % 2 != 0 ? targetHeight + 1 : targetHeight;
- // 步骤1:创建临时文件列表(FFmpeg合并视频需要先生成文件列表)
+ //Log.Debug($"动态计算最优分辨率:{targetWidth}x{targetHeight}(适配所有视频)");
+
+ // 步骤1:创建临时文件列表
string tempListFile = Path.Combine(AppContext.BaseDirectory, "temp", $"{Guid.NewGuid()}.txt");
var tempDir = Path.GetDirectoryName(tempListFile);
if (!Directory.Exists(tempDir))
@@ -283,87 +406,97 @@ namespace dy.net.utils
try
{
- // 生成FFmpeg识别的文件列表(格式:file '绝对路径')
- var fileListContent = new StringBuilder();
- foreach (var videoPath in videoFilePaths)
+ // 转封装所有有效视频为统一参数的临时视频(使用基准分辨率)
+ List unifiedVideoPaths = new List();
+ foreach (var videoDto in validVideoList)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ //Log.Error("视频合并流程被取消,终止临时视频转封装");
+ return string.Empty;
+ }
+
+ // 传入基准分辨率(替代原来的传入参数width/height)
+ string tempUnifiedVideo = await CreateUnifiedTempVideoAsync(videoDto, targetWidth, targetHeight, cancellationToken);
+ if (string.IsNullOrEmpty(tempUnifiedVideo) || !File.Exists(tempUnifiedVideo))
+ {
+ //Log.Error($"视频转封装失败,跳过该视频:{videoDto.Path}");
+ continue;
+ }
+
+ tempUnifiedVideoPaths.Add(tempUnifiedVideo);
+ unifiedVideoPaths.Add(tempUnifiedVideo);
+ }
+
+ if (!unifiedVideoPaths.Any())
+ {
+ Log.Error("统一转码成功的视频为空,无法进行下一步合并操作");
+ return string.Empty;
+ }
+
+ // 生成FFmpeg识别的文件列表
+ var fileListContent = new StringBuilder();
+ foreach (var videoPath in unifiedVideoPaths)
{
- // 处理路径中的特殊字符,确保跨平台兼容
string escapedPath = videoPath.Replace("\\", "/").Replace("'", "\\'");
fileListContent.AppendLine($"file '{escapedPath}'");
}
File.WriteAllText(tempListFile, fileListContent.ToString(), new UTF8Encoding(false));
- // 步骤2:构建FFmpeg合并+音频嵌入参数
+ // 构建FFmpeg合并参数(使用基准分辨率targetWidth/targetHeight)
var arguments = new List();
- // 核心修改:支持双输入(视频列表 + 自定义音频)
if (hasCustomAudio)
{
- // 输入1:视频文件列表(仅读取视频流,忽略原音频)
arguments.AddRange(new[]
{
- "-y", // 覆盖输出文件
- "-f", "concat",
- "-safe", "0",
- "-i", tempListFile,
- // 输入2:自定义音频文件
- "-i", audioPath,
- // 映射流:输入0的视频流 → 输出视频流;输入1的音频流 → 输出音频流
- "-map", "0:v",
- "-map", "1:a",
- // 音频同步:保证视频与音频时长匹配(若音频较短,循环音频;若较长,截断音频)
- "-shortest", // 以较短的流时长为准,避免无意义的空帧/静音
- "-filter:a", "apad=pad_len=0" // 音频补全,防止音频时长略短于视频导致的截断
+ "-y", "-f", "concat", "-safe", "0",
+ "-i", tempListFile, "-i", audioPath,
+ "-map", "0:v", "-map", "1:a",
+ "-shortest", "-filter:a", "apad=pad_len=0"
});
}
else
{
- // 无自定义音频:仅合并视频,保留原视频音频(若有)
arguments.AddRange(new[]
{
- "-y", // 覆盖输出文件
- "-f", "concat",
- "-safe", "0",
+ "-y", "-f", "concat", "-safe", "0",
"-i", tempListFile,
- // 映射流:保留原视频的视频流和音频流
- "-map", "0:v",
- "-map", "0:a?", // "?" 表示若存在音频流则映射,不存在则忽略,避免报错
+ "-map", "0:v", "-map", "0:a?",
"-shortest"
});
}
- // 视频编码参数(复用现有类的编码配置,保证输出格式统一)
+ // 核心:动态双向限制滤镜(不写死任何尺寸,使用传入的targetWidth/targetHeight)
+ // scale={0}:{1}:force_original_aspect_ratio=decrease:双向限制不超出动态目标尺寸,保持原比例
+ // pad={0}:{1}:动态填充到目标尺寸,无尺寸矛盾,无硬编码
+ string videoFilter = string.Format(CultureInfo.InvariantCulture,
+ "scale={0}:{1}:force_original_aspect_ratio=decrease,pad={0}:{1}:(ow-iw)/2:(oh-ih)/2:black",
+ targetWidth, // 动态传入的目标宽度(无硬编码)
+ targetHeight // 动态传入的目标高度(无硬编码)
+ );
+
+ arguments.AddRange(new[] { "-vf", videoFilter });
+
+ // 后续编码参数、执行逻辑均不变(使用基准分辨率)
arguments.AddRange(new[]
{
- "-c:v", VideoCodec,
- "-preset", VideoPreset,
- "-crf", $"{VideoCrf}",
- "-s", $"{width}x{height}", // 统一输出分辨率
- "-pix_fmt", "yuv420p", // 兼容所有播放器
- "-profile:v", "main"
+ "-c:v", VideoCodec, "-preset", VideoPreset, "-crf", $"{VideoCrf}",
+ "-pix_fmt", "yuv420p", "-profile:v", "main"
});
- // 音频编码参数(无论是否自定义音频,统一编码格式保证兼容性)
arguments.AddRange(new[]
{
- "-c:a", AudioCodec,
- "-b:a", AudioBitrate,
- "-ac", "2", // 立体声
- "-ar", "44100" // 标准采样率
+ "-c:a", AudioCodec, "-b:a", AudioBitrate,
+ "-ac", "2", "-ar", "44100"
});
- // 封装优化
arguments.AddRange(new[]
{
- "-f", "mp4",
- "-movflags", "+faststart" // 适合网络播放(将moov原子移至文件头部)
+ "-f", "mp4", "-movflags", "+faststart", savePath
});
- // 输出路径
- arguments.Add(savePath);
-
- //Log.Error($"savePath={savePath}");
- // 执行FFmpeg合并+音频嵌入命令
+ // 执行FFmpeg命令
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
// 验证输出文件
@@ -375,15 +508,25 @@ namespace dy.net.utils
else
{
Log.Error("视频合成失败,未生成输出文件。");
- return "";
+ return string.Empty;
}
}
finally
{
- // 清理临时文件
+ // 清理临时文件(原有逻辑不变)
if (File.Exists(tempListFile))
{
- File.Delete(tempListFile);
+ try { File.Delete(tempListFile); }
+ catch (IOException ex) { Log.Warning($"清理临时文件列表失败:{ex.Message}"); }
+ }
+
+ foreach (var tempVideoPath in tempUnifiedVideoPaths)
+ {
+ if (File.Exists(tempVideoPath))
+ {
+ try { File.Delete(tempVideoPath); }
+ catch (IOException ex) { Log.Warning($"清理临时视频失败:{ex.Message}"); }
+ }
}
}
}
@@ -463,5 +606,4 @@ namespace dy.net.utils
_ffmpegProcess?.Dispose();
}
}
-
-}
+}
\ No newline at end of file