修复动态视频多个小动态图视频 合成失败bug
其他优化
This commit is contained in:
@@ -28,7 +28,13 @@ namespace dy.net.utils
|
||||
// 2. 过滤 Linux 非法字符:
|
||||
// - 禁止:/(路径分隔符)、\0(空字符)
|
||||
// - 替换:其他特殊字符(如 :*?"<>|\\ )为下划线 _,避免创建失败
|
||||
var invalidChars = new[] { '/', '\0', ':', '*', '?', '"', '<', '>', '|', '\\' };
|
||||
//var invalidChars = new[] { '/', '\0', ':', '*', '?', '"', '<', '>', '|', '\\' };
|
||||
|
||||
var invalidChars = new[] {
|
||||
'/', '\0', // Linux 核心非法字符
|
||||
'\n', '\r', '\v', '\f', // 所有换行/回车/制表类控制字符
|
||||
':', '*', '?', '"', '<', '>', '|', '\\' // 其他常见非法字符
|
||||
};
|
||||
string sanitizedName = originalName;
|
||||
foreach (var c in invalidChars)
|
||||
{
|
||||
@@ -82,19 +88,20 @@ namespace dy.net.utils
|
||||
/// <returns>true:无特殊字符(仅允许字符);false:含有特殊字符</returns>
|
||||
public static bool IsValidWithoutSpecialChars(string input)
|
||||
{
|
||||
// 空字符串默认返回 true(若需禁止空字符串,可先判断 string.IsNullOrWhiteSpace(input) 并返回 false)
|
||||
if (string.IsNullOrEmpty(input))
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return true;
|
||||
|
||||
// 正则表达式说明:
|
||||
// ^ :匹配字符串开头
|
||||
// $ :匹配字符串结尾
|
||||
// [a-zA-Z0-9\u4E00-\u9FA5] :允许的字符范围
|
||||
// [a-zA-Z0-9\u4E00-\u9FA5 _] :允许的字符范围(新增空格和下划线)
|
||||
// a-zA-Z:大小写字母
|
||||
// 0-9:数字
|
||||
// \u4E00-\u9FA5:简体中文 Unicode 核心范围(覆盖99%+简体中文常用字)
|
||||
// \u4E00-\u9FA5:简体中文 Unicode 核心范围
|
||||
// 空格:普通空格字符
|
||||
// _:下划线
|
||||
// * :匹配 0 个或多个允许的字符(若需至少1个字符,可改为 +)
|
||||
const string pattern = @"^[a-zA-Z0-9\u4E00-\u9FA5]*$";
|
||||
const string pattern = @"^[a-zA-Z0-9\u4E00-\u9FA5 _]*$";
|
||||
|
||||
// 忽略文化差异,仅按字符编码匹配
|
||||
return Regex.IsMatch(input, pattern, RegexOptions.None);
|
||||
|
||||
+78
-27
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
@@ -13,7 +14,7 @@ namespace dy.net.utils
|
||||
|
||||
|
||||
private string _ffmpegExecutablePath;
|
||||
private string _ffprobeExecutablePath ;
|
||||
private string _ffprobeExecutablePath;
|
||||
|
||||
public FFmpegHelper()
|
||||
{
|
||||
@@ -24,15 +25,15 @@ namespace dy.net.utils
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
// Debug 环境,通常是 Windows
|
||||
_ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg.exe";
|
||||
_ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe.exe";
|
||||
#else
|
||||
_ffmpegExecutablePath = "E:\\down\\ffmpeg\\bin\\ffmpeg.exe";
|
||||
_ffprobeExecutablePath = "E:\\down\\ffmpeg\\bin\\ffprobe.exe";
|
||||
#else
|
||||
// Release 环境,通常是 Docker Linux
|
||||
_ffmpegExecutablePath = "ffmpeg";
|
||||
_ffprobeExecutablePath = "ffprobe";
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,10 +230,12 @@ namespace dy.net.utils
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 合并多个视频文件为一个MP4视频
|
||||
/// 合并多个视频文件为一个MP4视频(嵌入指定音频文件)
|
||||
/// </summary>
|
||||
/// <param name="videoFilePaths">待合并的视频路径列表(按合并顺序排列)</param>
|
||||
/// <param name="audioPath">音频文件(可为null/空,为空则保留视频原音频(若有))</param>
|
||||
/// <param name="savePath">输出视频的保存路径</param>
|
||||
/// <param name="width">输出视频宽度(自动修正为偶数)</param>
|
||||
/// <param name="height">输出视频高度(自动修正为偶数)</param>
|
||||
@@ -241,6 +244,7 @@ namespace dy.net.utils
|
||||
/// <returns>输出视频路径</returns>
|
||||
public async Task<string> MergeMultipleVideosAsync(
|
||||
List<string> videoFilePaths,
|
||||
string audioPath,
|
||||
string savePath,
|
||||
int width = 1080,
|
||||
int height = 1920,
|
||||
@@ -260,6 +264,11 @@ namespace dy.net.utils
|
||||
if (string.IsNullOrEmpty(savePath))
|
||||
throw new ArgumentNullException(nameof(savePath));
|
||||
|
||||
// 音频文件验证(若传入非空路径,则校验文件是否存在)
|
||||
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++;
|
||||
@@ -282,49 +291,91 @@ namespace dy.net.utils
|
||||
string escapedPath = videoPath.Replace("\\", "/").Replace("'", "\\'");
|
||||
fileListContent.AppendLine($"file '{escapedPath}'");
|
||||
}
|
||||
File.WriteAllText(tempListFile, fileListContent.ToString(), Encoding.UTF8);
|
||||
File.WriteAllText(tempListFile, fileListContent.ToString(), new UTF8Encoding(false));
|
||||
|
||||
// 步骤2:构建FFmpeg合并参数
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-y", // 覆盖输出文件
|
||||
"-f", "concat", // 指定合并格式
|
||||
"-safe", "0", // 允许访问绝对路径
|
||||
"-i", tempListFile, // 输入文件列表
|
||||
// 步骤2:构建FFmpeg合并+音频嵌入参数
|
||||
var arguments = new List<string>();
|
||||
|
||||
// 视频编码参数(复用现有类的编码配置,保证输出格式统一)
|
||||
// 核心修改:支持双输入(视频列表 + 自定义音频)
|
||||
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" // 音频补全,防止音频时长略短于视频导致的截断
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无自定义音频:仅合并视频,保留原视频音频(若有)
|
||||
arguments.AddRange(new[]
|
||||
{
|
||||
"-y", // 覆盖输出文件
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", tempListFile,
|
||||
// 映射流:保留原视频的视频流和音频流
|
||||
"-map", "0:v",
|
||||
"-map", "0:a?", // "?" 表示若存在音频流则映射,不存在则忽略,避免报错
|
||||
"-shortest"
|
||||
});
|
||||
}
|
||||
|
||||
// 视频编码参数(复用现有类的编码配置,保证输出格式统一)
|
||||
arguments.AddRange(new[]
|
||||
{
|
||||
"-c:v", VideoCodec,
|
||||
"-preset", VideoPreset,
|
||||
"-crf", $"{VideoCrf}",
|
||||
"-s", $"{width}x{height}", // 统一输出分辨率
|
||||
"-pix_fmt", "yuv420p", // 兼容所有播放器
|
||||
"-profile:v", "main",
|
||||
"-profile:v", "main"
|
||||
});
|
||||
|
||||
// 音频编码参数
|
||||
// 音频编码参数(无论是否自定义音频,统一编码格式保证兼容性)
|
||||
arguments.AddRange(new[]
|
||||
{
|
||||
"-c:a", AudioCodec,
|
||||
"-b:a", AudioBitrate,
|
||||
"-ac", "2", // 立体声
|
||||
"-ar", "44100", // 标准采样率
|
||||
"-ar", "44100" // 标准采样率
|
||||
});
|
||||
|
||||
// 封装优化
|
||||
// 封装优化
|
||||
arguments.AddRange(new[]
|
||||
{
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // 适合网络播放
|
||||
"-movflags", "+faststart" // 适合网络播放(将moov原子移至文件头部)
|
||||
});
|
||||
|
||||
// 输出路径
|
||||
savePath
|
||||
};
|
||||
// 输出路径
|
||||
arguments.Add(savePath);
|
||||
|
||||
// 执行FFmpeg合并命令
|
||||
//Log.Error($"savePath={savePath}");
|
||||
// 执行FFmpeg合并+音频嵌入命令
|
||||
await ExecuteFFmpegAsync(arguments, progress, cancellationToken);
|
||||
|
||||
// 验证输出文件
|
||||
if (File.Exists(savePath))
|
||||
{
|
||||
Log.Debug($"视频合成成功:{savePath}");
|
||||
return savePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("视频合并失败,未生成输出文件。");
|
||||
Log.Error("视频合成失败,未生成输出文件。");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
+15
-12
@@ -27,7 +27,7 @@ namespace dy.net.utils
|
||||
string videoDirectory = Path.GetDirectoryName(video.VideoSavePath); // 视频所在目录
|
||||
string videoFileNameWithoutExt = Path.GetFileNameWithoutExtension(video.VideoSavePath); // 无扩展名的文件名
|
||||
string nfoFullPath = Path.Combine(videoDirectory, $"{videoFileNameWithoutExt}.nfo"); // NFO文件完整路径
|
||||
string postFullPath = Path.Combine(videoDirectory, "poster.jpg"); // NFO文件完整路径
|
||||
//string postFullPath = Path.Combine(videoDirectory, "poster.jpg"); // NFO文件完整路径
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(video.AuthorAvatar))
|
||||
{
|
||||
@@ -38,8 +38,12 @@ namespace dy.net.utils
|
||||
if (File.Exists(video.AuthorAvatar))
|
||||
{
|
||||
var fileExt = Path.GetExtension(video.AuthorAvatar);
|
||||
|
||||
var nfoActorFullPath = Path.Combine(videoDirectory, $"{video.Author}{fileExt}");
|
||||
var actorsDir = Path.Combine(videoDirectory, ".actors");
|
||||
if(!Directory.Exists(actorsDir))
|
||||
{
|
||||
Directory.CreateDirectory(actorsDir);
|
||||
}
|
||||
var nfoActorFullPath = Path.Combine(actorsDir, $"{video.Author}{fileExt}");
|
||||
|
||||
if (File.Exists(nfoActorFullPath))
|
||||
{
|
||||
@@ -61,9 +65,9 @@ namespace dy.net.utils
|
||||
}
|
||||
},
|
||||
Author = video.Author,
|
||||
Poster = postFullPath,
|
||||
Poster = "poster.jpg",
|
||||
Title = video.VideoTitle,
|
||||
Thumbnail = postFullPath,// 使用poster作为缩略图
|
||||
Thumbnail = "poster.jpg",// 使用poster作为缩略图
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { video.Tag1, video.Tag2, video.Tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoFullPath);
|
||||
@@ -86,19 +90,18 @@ namespace dy.net.utils
|
||||
|
||||
// 创建根元素
|
||||
XElement root = new XElement("movie");
|
||||
root.Add(new XElement("outline"));
|
||||
//root.Add(new XElement("outline"));
|
||||
root.Add(new XElement("lockdata", true));
|
||||
root.Add(new XElement("director", videoInfo.Author));
|
||||
root.Add(new XElement("plot", $"<![CDATA[{videoInfo.Title}]]>"));
|
||||
//root.Add(new XElement("director", videoInfo.Author));
|
||||
//root.Add(new XElement("plot", $"<![CDATA[{videoInfo.Title}]]>"));
|
||||
|
||||
// 添加视频信息(先清理无效字符)
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Title))
|
||||
root.Add(new XElement("title", CleanInvalidXmlChars(videoInfo.Title)));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(videoInfo.Author))
|
||||
root.Add(new XElement("author", CleanInvalidXmlChars(videoInfo.Author)));
|
||||
//if (!string.IsNullOrWhiteSpace(videoInfo.Author))
|
||||
// root.Add(new XElement("author", CleanInvalidXmlChars(videoInfo.Author)));
|
||||
|
||||
// 发布时间(无需清理,因为是格式化的日期字符串)
|
||||
if (videoInfo.ReleaseDate.HasValue)
|
||||
{
|
||||
root.Add(new XElement("releasedate", videoInfo.ReleaseDate.Value.ToString("yyyy-MM-dd")));
|
||||
@@ -128,7 +131,7 @@ namespace dy.net.utils
|
||||
if (!string.IsNullOrWhiteSpace(actor.Role))
|
||||
actorElement.Add(new XElement("role", CleanInvalidXmlChars(actor.Role)));
|
||||
|
||||
actorElement.Add(new XElement("tmdbid", "3141592610000"));//写死一个反正不存在的ID,防止被媒体管理软件误认
|
||||
actorElement.Add(new XElement("tmdbid", ""));
|
||||
|
||||
root.Add(actorElement);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user