diff --git a/.gitignore b/.gitignore index 9491a2f..499a1f1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +/.idea # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Common/Tools/LogUtils.cs b/Common/Tools/LogUtils.cs index 54dbbe1..ecfb74b 100644 --- a/Common/Tools/LogUtils.cs +++ b/Common/Tools/LogUtils.cs @@ -96,6 +96,18 @@ namespace VideoConcat.Common.Tools } } + /// + /// Debug级 常规日志 + /// + /// 日志信息 + public static void DebugFormat(string info, params object?[]? args) + { + if (loginfo.IsDebugEnabled) + { + loginfo.DebugFormat(info, args); + } + } + /// /// Debug级 异常日志 /// diff --git a/Common/Tools/VideoCombine.cs b/Common/Tools/VideoCombine.cs index d857e20..03d97fd 100644 --- a/Common/Tools/VideoCombine.cs +++ b/Common/Tools/VideoCombine.cs @@ -2,11 +2,15 @@ using FFMpegCore.Helpers; using FFMpegCore; using System.IO; +using System.Security.Cryptography; namespace VideoConcat.Common.Tools { internal class VideoCombine { + + public static List mustClearPath = []; + /// /// 生成所有组合 /// @@ -48,12 +52,38 @@ namespace VideoConcat.Common.Tools { var video = FFProbe.Analyse(videoPath); + + string mediaInfo = ""; + + mediaInfo += $"视频: {videoPath}"; + mediaInfo += $"时长: {video.Duration}"; + mediaInfo += $"格式: {video.Format}"; + // 视频流信息 + foreach (var videoStream in video.VideoStreams) + { + mediaInfo += $"视频编码: {videoStream.CodecName}, 分辨率: {videoStream.Width}x{videoStream.Height}"; + } + // 音频流信息 + foreach (var audioStream in video.AudioStreams) + { + mediaInfo += $"音频编码: {audioStream.CodecName}, 采样率: {audioStream.SampleRateHz} Hz"; + } + + LogUtils.Info(mediaInfo); + + FFMpegHelper.ConversionSizeExceptionCheck(video); - string _tempPath = Path.GetDirectoryName(videoPath) ?? ""; + string _tempMd5Name = GetLargeFileMD5(videoPath); //GlobalFFOptions.Current.TemporaryFilesFolder - var destinationPath = Path.Combine(_tempPath, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); + var destinationPath = Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); + + if (File.Exists(destinationPath)) + { + return destinationPath; + } + //Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder); try { @@ -61,9 +91,76 @@ namespace VideoConcat.Common.Tools } catch (Exception ex) { - LogUtils.Error($"{videoPath} 转换失败", ex); + + LogUtils.Info("视频转换失败!尝试另外一种转换"); + // 创建FFmpeg参数 + try + { + // string _tempPathError = Path.GetDirectoryName(videoPath) ?? ""; + + //GlobalFFOptions.Current.TemporaryFilesFolder + //var destinationPathExecp = Path.Combine(Path.GetTempPath(), $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Mp4}"); + + + //VideoCombine.mustClearPath.Add(destinationPathExecp); + + // 配置 FFmpeg 参数 + //var options = new FFMpegArgumentOptions() + // .WithVideoCodec("libx264") // 指定支持 CRF 的编码器 + // .WithConstantRateFactor(23) // 内置方法设置 CRF + // .WithAudioCodec("aac") // 音频编码器 + // .WithFastStart(); // 流媒体优化 + + + FFMpegArguments + .FromFileInput(videoPath) + .OutputToFile(destinationPath, true, o => o + .WithVideoCodec("libx264") // 设置视频编码器 + .WithAudioCodec("aac") // 设置音频编码器 + .WithAudioSamplingRate(44100) + .WithAudioBitrate(128000) + .WithConstantRateFactor(23) // 设置质量调整参数 + .WithCustomArgument("-vf fps=30") // 强制指定帧率(例如 30fps) + .WithFastStart() + //.WithCustomArgument("-movflags +faststart") // 确保 moov atom 正确写入 + //.CopyChannel() + //.WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + .ForceFormat(VideoType.Ts) + ) + //.NotifyOnOutput(Console.WriteLine) // 打印 FFmpeg 详细日志 + .ProcessSynchronously(true); + //FFMpeg.Convert(destinationPathExecp, destinationPath, VideoType.Ts); + + } + catch (Exception e) + { + LogUtils.Error($"{videoPath} 转换失败", ex); + LogUtils.Error($"{videoPath} 转换再次失败", e); + } + } + return destinationPath; } + + public static string GetLargeFileMD5(string filePath) + { + using var md5 = MD5.Create(); + using var stream = File.OpenRead(filePath); + + byte[] buffer = new byte[8192]; // 8KB 缓冲区 + int bytesRead; + long totalBytesRead = 0; + + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) + { + md5.TransformBlock(buffer, 0, bytesRead, null, 0); + totalBytesRead += bytesRead; + // 可在此添加进度显示:Console.WriteLine($"已读取 {totalBytesRead / 1024 / 1024}MB"); + } + + md5.TransformFinalBlock(buffer, 0, 0); + return BitConverter.ToString(value: md5.Hash).Replace("-", "").ToLowerInvariant(); + } } } diff --git a/ViewModels/VideoViewModel.cs b/ViewModels/VideoViewModel.cs index 07734f1..2860a04 100644 --- a/ViewModels/VideoViewModel.cs +++ b/ViewModels/VideoViewModel.cs @@ -91,7 +91,7 @@ namespace VideoConcat.ViewModels VideoModel.ConcatVideos.Clear(); VideoModel.IsStart = true; }); - + if (Directory.Exists($"{VideoModel.FolderPath}\\output") == false) { Directory.CreateDirectory($"{VideoModel.FolderPath}\\output"); @@ -120,10 +120,10 @@ namespace VideoConcat.ViewModels // 复制原列表,避免修改原列表 - List> tempList = new(combinations); + List> tempList = [.. combinations]; string[] _converVideoPath = []; - List _clearPath = []; + for (int i = 0; i < VideoModel.Num && tempList.Count > 0; i++) { @@ -146,7 +146,7 @@ namespace VideoConcat.ViewModels { try { - _clearPath.Add(VideoCombine.ConvertVideos(_path)); + VideoCombine.mustClearPath.Add(VideoCombine.ConvertVideos(_path)); } finally { @@ -172,17 +172,17 @@ namespace VideoConcat.ViewModels await semaphore.WaitAsync(); var _task = Task.Run(() => { + string _tempFileName = $"{DateTime.Now:yyyyMMddHHmmss}{random.Next(100000, 999999)}.mp4"; + string _outPutName = Path.Combine($"{VideoModel.FolderPath}", "output", _tempFileName); ; + + try { - string _tempFileName = $"{DateTime.Now:yyyyMMddHHmmss}{random.Next(100000, 999999)}.mp4"; - string _outPutName = Path.Combine($"{VideoModel.FolderPath}", "output", _tempFileName); ; - - var temporaryVideoParts = combination.Select((_videoPath) => { - string _tempPath = Path.GetDirectoryName(_videoPath) ?? ""; + string _tempMd5Name = VideoCombine.GetLargeFileMD5(_videoPath); //GlobalFFOptions.Current.TemporaryFilesFolder - return Path.Combine(_tempPath, $"{Path.GetFileNameWithoutExtension(_videoPath)}{FileExtension.Ts}"); + return Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); }).ToArray(); bool _isSuccess = false; @@ -194,7 +194,13 @@ namespace VideoConcat.ViewModels .FromConcatInput(temporaryVideoParts) .OutputToFile(_outPutName, true, options => options .CopyChannel() - .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc)) + .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc) + .WithFastStart() + .WithVideoCodec("copy") // 复制视频流 + .WithAudioCodec("aac") // 重新编码音频 + .WithAudioSamplingRate(44100) // 强制采样率 + .WithCustomArgument("-movflags +faststart -analyzeduration 100M -probesize 100M") + ) .ProcessSynchronously(); } catch (Exception ex) @@ -240,7 +246,9 @@ namespace VideoConcat.ViewModels .OutputToFile( _outPutNameImg, true, - options => options.WithCustomArgument(_customArg) + options => options + .WithCustomArgument(_customArg)// 或显式指定编码器参数 + .WithCustomArgument("-movflags +faststart") // 确保 moov atom 正确写入 ) .ProcessSynchronously(); @@ -254,11 +262,11 @@ namespace VideoConcat.ViewModels } - LogUtils.Info($"当前视频: {string.Join(";", combination)} 合并成功"); + LogUtils.Info($"当前视频-[${_outPutName}]: {string.Join(";", combination)} 合并成功"); } catch (Exception ex) { - LogUtils.Error($"视频:{string.Join(";", combination)} 合并失败", ex); + LogUtils.Error($"视频[${_outPutName}]:{string.Join(";", combination)} 合并失败", ex); } finally { @@ -277,8 +285,9 @@ namespace VideoConcat.ViewModels LogUtils.Info($"所有视频拼接完成,用时{(endTime - startTime).TotalSeconds}秒"); VideoModel.IsStart = false; - VideoCombine.Cleanup(_clearPath); + VideoCombine.Cleanup(VideoCombine.mustClearPath); MessageBox.Show("所有视频拼接完成"); + VideoCombine.mustClearPath = []; }); }); } @@ -292,7 +301,8 @@ namespace VideoConcat.ViewModels try { DirectoryInfo dirD = dir as DirectoryInfo; - DirectoryInfo[] folders = dirD.GetDirectories(); + DirectoryInfo[] folders = [.. dirD.GetDirectories().OrderBy(d=>d.Name)]; + VideoModel.FolderInfos.Clear(); //获取文件夹下所有视频文件 foreach (DirectoryInfo Folder in folders) diff --git a/Views/MainWindow.xaml.cs b/Views/MainWindow.xaml.cs index 8285f03..2321cfc 100644 --- a/Views/MainWindow.xaml.cs +++ b/Views/MainWindow.xaml.cs @@ -14,14 +14,7 @@ namespace VideoConcat.Views InitializeComponent(); Username.Text = Config.GetSettingString("userName"); Password.Password = Config.GetSettingString("password"); - if (Config.GetSettingString("isRemember") == "true") - { - ckbRemember.IsChecked = true; - } - else - { - ckbRemember.IsChecked = false; - } + ckbRemember.IsChecked = Config.GetSettingString("isRemember") == "true"; } private void BtnExit_Click(object sender, RoutedEventArgs e)