diff --git a/App.config b/App.config index 3190def..7a50127 100644 --- a/App.config +++ b/App.config @@ -3,6 +3,6 @@ - + \ No newline at end of file diff --git a/App.xaml b/App.xaml index 6384de0..7b4b078 100644 --- a/App.xaml +++ b/App.xaml @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:VideoConcat" - xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers" + xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers" xmlns:local1="clr-namespace:VideoConcat.Views" StartupUri="Views/MainWindow.xaml"> diff --git a/Common/Tools/VideoCombine.cs b/Common/Tools/VideoCombine.cs index 03d97fd..a58b474 100644 --- a/Common/Tools/VideoCombine.cs +++ b/Common/Tools/VideoCombine.cs @@ -18,7 +18,7 @@ namespace VideoConcat.Common.Tools { if (index == videoLists.Count) { - result.Add(new List(currentCombination)); + result.Add([.. currentCombination]); return; } diff --git a/EnumToBooleanConverter.cs b/EnumToBooleanConverter.cs new file mode 100644 index 0000000..299b93b --- /dev/null +++ b/EnumToBooleanConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace VideoConcat +{ + public class EnumToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value.Equals(parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.Equals(true) == true ? parameter : System.Windows.Data.Binding.DoNothing; + } + } +} diff --git a/Models/VideoModel.cs b/Models/VideoModel.cs index 644cc91..eb312f4 100644 --- a/Models/VideoModel.cs +++ b/Models/VideoModel.cs @@ -4,18 +4,27 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using System.Windows.Data; using System.Windows.Input; using System.Windows.Threading; using System.Xml.Linq; using static System.Windows.Forms.VisualStyles.VisualStyleElement.Header; +using static VideoConcat.Models.VideoModel; namespace VideoConcat.Models { + + public enum Gender + { + Male, + Female + } public class VideoModel : INotifyPropertyChanged { private int _num; @@ -23,7 +32,7 @@ namespace VideoConcat.Models private string _folderPath = ""; private string _auditImagePath = ""; private bool _canStart = false; - private bool _isCanOperate=false; + private bool _isCanOperate = false; private bool _isStart = false; private ObservableCollection _FolderInfos = []; private ObservableCollection _concatVideos = []; @@ -192,14 +201,21 @@ namespace VideoConcat.Models int _temp = 1; if (FolderInfos.Count > 0) { - foreach (FolderInfo item in FolderInfos) + if (IsJoinType1Selected) { - if (item.Num > 0) + foreach (FolderInfo item in FolderInfos) { - _temp *= item.Num; + if (item.Num > 0) + { + _temp *= item.Num; + } } + MaxNum = _temp; + } + if (IsJoinType2Selected) + { + MaxNum = FolderInfos[0].Num; } - MaxNum = _temp; SetCanStart(); } else @@ -227,5 +243,29 @@ namespace VideoConcat.Models ConcatVideos = []; _dispatcher = Dispatcher.CurrentDispatcher; } + + private bool _isJoinType1Selected; + private bool _isJoinType2Selected; + + public bool IsJoinType1Selected + { + get { return _isJoinType1Selected; } + set + { + _isJoinType1Selected = value; + OnPropertyChanged(nameof(IsJoinType1Selected)); + } + } + + public bool IsJoinType2Selected + { + get { return _isJoinType2Selected; } + set + { + _isJoinType2Selected = value; + OnPropertyChanged(nameof(IsJoinType2Selected)); + } + } } + } diff --git a/Services/BaseService.cs b/Services/BaseService.cs new file mode 100644 index 0000000..9813c91 --- /dev/null +++ b/Services/BaseService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VideoConcat.Services +{ + abstract class BaseService + { + } +} diff --git a/Services/Video/VideoService.cs b/Services/Video/VideoService.cs new file mode 100644 index 0000000..e84a31d --- /dev/null +++ b/Services/Video/VideoService.cs @@ -0,0 +1,244 @@ +using FFMpegCore.Enums; +using FFMpegCore.Helpers; +using FFMpegCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VideoConcat.Common.Tools; +using System.Security.Cryptography; +using System.IO; +using VideoConcat.Models; +using static VideoConcat.Models.VideoModel; + + + +namespace VideoConcat.Services.Video +{ + internal class VideoService : BaseService + { + /// + /// 将视频文件转换为ts格式 + /// + public static string ConvertVideos(string videoPath) + { + + 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 _tempMd5Name = GetLargeFileMD5(videoPath); + + //GlobalFFOptions.Current.TemporaryFilesFolder + var destinationPath = Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); + + if (File.Exists(destinationPath)) + { + return destinationPath; + } + + //Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder); + try + { + FFMpeg.Convert(videoPath, destinationPath, VideoType.Ts); + } + catch (Exception ex) + { + + LogUtils.Info("视频转换失败!尝试另外一种转换"); + // 创建FFmpeg参数 + try + { + 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(); + } + + + /// + /// 清理临时文件 + /// + public static void Cleanup(List pathList) + { + foreach (var path in pathList) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + + public static void JoinVideos(List combination) + { + VideoModel videoModel = new(); + + if (Directory.Exists($"{videoModel.FolderPath}\\output") == false) + { + Directory.CreateDirectory($"{videoModel.FolderPath}\\output"); + } + + + Random random = new(); + string _tempFileName = $"{DateTime.Now:yyyyMMddHHmmss}{random.Next(100000, 999999)}.mp4"; + + string _outPutName = Path.Combine($"{videoModel.FolderPath}", "output", _tempFileName); ; + + + + var temporaryVideoParts = combination.Select((_videoPath) => + { + string _tempMd5Name = VideoService.GetLargeFileMD5(_videoPath); + //GlobalFFOptions.Current.TemporaryFilesFolder + return Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); + }).ToArray(); + + bool _isSuccess = false; + + + try + { + _isSuccess = FFMpegArguments + .FromConcatInput(temporaryVideoParts) + .OutputToFile(_outPutName, true, options => options + .CopyChannel() + .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc) + .WithFastStart() + .WithVideoCodec("copy") // 复制视频流 + .WithAudioCodec("aac") // 重新编码音频 + .WithAudioSamplingRate(44100) // 强制采样率 + .WithCustomArgument("-movflags +faststart -analyzeduration 100M -probesize 100M") + ) + .ProcessSynchronously(); + } + catch (Exception ex1) + { + { + LogUtils.Error("拼接视频失败", ex1); + } + + //bool _isSuccess = FFMpeg.Join(_outPutName, [.. combination]); + + videoModel.Dispatcher.Invoke(() => + { + IMediaAnalysis _mediaInfo = FFProbe.Analyse(_outPutName); + FileInfo fileInfo = new(_outPutName); + + videoModel.ConcatVideos.Add(new ConcatVideo() + { + Index = videoModel.ConcatVideos.Count + 1, + FileName = _tempFileName, + Size = $"{fileInfo.Length / 1024 / 1024}MB", + Seconds = ((int)_mediaInfo.Duration.TotalSeconds), + Status = _isSuccess ? "拼接成功" : "拼接失败", + Progress = "100%", + }); + }); + + + if (_isSuccess && videoModel.AuditImagePath != "") + { + // 使用 FFMpegCore 执行添加图片到视频的操作 + try + { + // 配置 FFmpeg 二进制文件位置(如果 FFmpeg 不在系统路径中) + // GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "path/to/ffmpeg/bin" }); + + string _outPutNameImg = $"{videoModel.FolderPath}\\output\\{DateTime.Now:yyyyMMddHHmmss}{random.Next(100000, 999999)}.mp4"; + + string _customArg = "-filter_complex \"[0:v][1:v] overlay=0:H-h\" "; + // 使用 FFMpegArguments 构建命令 + bool _isCoverSuccess = FFMpegArguments + .FromFileInput(_outPutName) + .AddFileInput(videoModel.AuditImagePath) + .OutputToFile( + _outPutNameImg, + true, + options => options + .WithCustomArgument(_customArg)// 或显式指定编码器参数 + .WithCustomArgument("-movflags +faststart") // 确保 moov atom 正确写入 + ) + .ProcessSynchronously(); + + + LogUtils.Info($"图片已成功添加到视频中,输出文件:{_outPutName}"); + } + catch (Exception ex) + { + LogUtils.Error($"图片添加到视频中失败", ex); + } + } + + + LogUtils.Info($"当前视频-[${_outPutName}]: {string.Join(";", combination)} 合并成功"); + } + } + } +} diff --git a/VideoConcat.sln b/VideoConcat.sln index ed7cba0..5093c67 100644 --- a/VideoConcat.sln +++ b/VideoConcat.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.11.35327.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoConcat", "VideoConcat.csproj", "{2FF5691C-3184-4B68-944B-C704E64C4E4E}" EndProject -Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "视频", "..\视频\视频.vdproj", "{F784558F-CA6F-E806-1DC3-7B0C364779F3}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -17,8 +15,6 @@ Global {2FF5691C-3184-4B68-944B-C704E64C4E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FF5691C-3184-4B68-944B-C704E64C4E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FF5691C-3184-4B68-944B-C704E64C4E4E}.Release|Any CPU.Build.0 = Release|Any CPU - {F784558F-CA6F-E806-1DC3-7B0C364779F3}.Debug|Any CPU.ActiveCfg = Debug - {F784558F-CA6F-E806-1DC3-7B0C364779F3}.Release|Any CPU.ActiveCfg = Release EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ViewModels/VideoViewModel.cs b/ViewModels/VideoViewModel.cs index 2860a04..f1446c6 100644 --- a/ViewModels/VideoViewModel.cs +++ b/ViewModels/VideoViewModel.cs @@ -35,6 +35,9 @@ namespace VideoConcat.ViewModels { FolderInfos = [], ConcatVideos = [], + IsJoinType1Selected = true, + IsJoinType2Selected = false, + MaxNum = 0, CanStart = false, IsStart = false, @@ -106,33 +109,52 @@ namespace VideoConcat.ViewModels List> videoLists = []; - VideoModel.FolderInfos.ForEach(folderInfo => { videoLists.Add(folderInfo.VideoPaths); }); - - VideoCombine.GenerateCombinations(videoLists, 0, currentCombination, combinations); - - + string[] _converVideoPath = []; List> result = []; Random random = new(); - - - // 复制原列表,避免修改原列表 - List> tempList = [.. combinations]; - - string[] _converVideoPath = []; - - - for (int i = 0; i < VideoModel.Num && tempList.Count > 0; i++) + if (VideoModel.IsJoinType1Selected) { - int index = random.Next(tempList.Count); - result.Add(tempList[index]); + VideoCombine.GenerateCombinations(videoLists, 0, currentCombination, combinations); - _converVideoPath = [.. _converVideoPath, .. tempList[index]]; + // 复制原列表,避免修改原列表 + List> tempList = [.. combinations]; + for (int i = 0; i < VideoModel.Num && tempList.Count > 0; i++) + { + int index = random.Next(tempList.Count); + result.Add(tempList[index]); - tempList.RemoveAt(index); + _converVideoPath = [.. _converVideoPath, .. tempList[index]]; + + tempList.RemoveAt(index); + } + } + + if (VideoModel.IsJoinType2Selected) + { + int count = videoLists[0].Count; + for (int index = 1; index < count; index++) + { + if (videoLists[0].Count != count) + { + WPFDevelopers.Controls.MessageBox.Show("请输入用户名或者密码!"); + return; + } + } + for (int index = 0; index < count; index++) + { + List list2 = []; + foreach (List list in videoLists) + { + _converVideoPath=[.. _converVideoPath, list[index]]; + list2.Add(list[index]); + } + + result.Add(list2); + } } SemaphoreSlim semaphore = new(10); // Limit to 3 threads @@ -301,8 +323,8 @@ namespace VideoConcat.ViewModels try { DirectoryInfo dirD = dir as DirectoryInfo; - DirectoryInfo[] folders = [.. dirD.GetDirectories().OrderBy(d=>d.Name)]; - + DirectoryInfo[] folders = [.. dirD.GetDirectories().OrderBy(d => d.Name)]; + VideoModel.FolderInfos.Clear(); //获取文件夹下所有视频文件 foreach (DirectoryInfo Folder in folders) diff --git a/Views/Video.xaml b/Views/Video.xaml index 47424b1..96ea3a1 100644 --- a/Views/Video.xaml +++ b/Views/Video.xaml @@ -32,6 +32,11 @@