using System.Windows.Input; using VideoConcat.Models; using MessageBox = System.Windows.MessageBox; using VideoConcat.Common.Tools; using System.IO; using Microsoft.Expression.Drawing.Core; using FFMpegCore; using FFMpegCore.Enums; using static VideoConcat.Models.VideoModel; using System.Windows.Threading; using System.Windows; using System.Diagnostics; using System.Linq; // 明确使用 WPF 的 OpenFileDialog,避免与 Windows Forms 冲突 using OpenFileDialog = Microsoft.Win32.OpenFileDialog; using System.Windows.Forms; namespace VideoConcat.ViewModels { public class VideoViewModel { private VideoModel _videoModel; public List ConcatVideos { get; set; } = []; public VideoModel VideoModel { get { return _videoModel; } set { _videoModel = value; } } public VideoViewModel() { VideoModel = new VideoModel { FolderInfos = [], ConcatVideos = [], IsJoinType1Selected = true, IsJoinType2Selected = false, MaxNum = 0, CanStart = false, IsStart = false, BtnOpenFolderCommand = new Command() { DoExcue = obj => { System.Windows.Forms.FolderBrowserDialog folderBrowserDialog = new(); if (folderBrowserDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { VideoModel.FolderPath = folderBrowserDialog.SelectedPath; LogUtils.Info($"获取视频文件夹,视频路径:{VideoModel.FolderPath}"); ListFolder(VideoModel.FolderPath); } } }, BtnChooseAuditImageCommand = new Command() { DoExcue = obj => { // 创建一个 OpenFileDialog 实例(使用 WPF 的 OpenFileDialog) OpenFileDialog openFileDialog = new() { // 设置文件对话框的标题 Title = "选择广审图片", // 设置文件筛选器,只允许选择文本文件和图像文件 Filter = "图片文件[*.png;*.jpg;*.jpeg;*.bmp]|*.png;*.jpg;*.jpeg;*.bmp", }; // 显示文件对话框并获取结果(WPF 返回 bool?) bool? result = openFileDialog.ShowDialog(); // 检查用户是否点击了打开按钮 if (result == true) { // 获取用户选择的文件路径列表 VideoModel.AuditImagePath = openFileDialog.FileName; } } }, BtnStartVideoConcatCommand = new Command() { DoExcue = obj => { Task.Run(action: async () => { VideoModel.Dispatcher.Invoke(() => { VideoModel.ConcatVideos.Clear(); VideoModel.IsStart = true; }); if (Directory.Exists($"{VideoModel.FolderPath}\\output") == false) { Directory.CreateDirectory($"{VideoModel.FolderPath}\\output"); } //开始时间 DateTime startTime = DateTime.Now; LogUtils.Info("开始合并视频,进行视频拼接组合"); List> combinations = []; List currentCombination = []; List> videoLists = []; VideoModel.FolderInfos.ForEach(folderInfo => { videoLists.Add(folderInfo.VideoPaths); }); string[] _converVideoPath = []; List> result = []; Random random = new(); if (VideoModel.IsJoinType1Selected) { VideoCombine.GenerateCombinations(videoLists, 0, currentCombination, combinations); // 复制原列表,避免修改原列表 List> tempList = [.. combinations]; for (int i = 0; i < VideoModel.Num && tempList.Count > 0; i++) { int index = random.Next(tempList.Count); result.Add(tempList[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 List _tasks = []; foreach (var _path in _converVideoPath) { await semaphore.WaitAsync(); // Wait when more than 3 threads are running var _task = Task.Run(() => { try { VideoCombine.mustClearPath.Add(VideoCombine.ConvertVideos(_path)); } finally { semaphore.Release(); // Work is done, signal to semaphore that more work is possible } }); _tasks.Add(_task); } await Task.WhenAll(_tasks).ContinueWith((task) => { LogUtils.Info($"转换完成,用时{(DateTime.Now - startTime).TotalSeconds}秒"); }); LogUtils.Info("开始拼接视频"); // 预先创建所有ConcatVideo项,用于显示进度 VideoModel.Dispatcher.Invoke(() => { for (int i = 0; i < result.Count; i++) { List combination = result[i]; string firstVideoPath = combination[0]; string firstVideoName = Path.GetFileNameWithoutExtension(firstVideoPath); string _tempFileName = $"{firstVideoName}_{i + 1:D4}.mp4"; string _outPutName = Path.Combine($"{VideoModel.FolderPath}", "output", _tempFileName); VideoModel.ConcatVideos.Add(new ConcatVideo() { Index = i + 1, FileName = _tempFileName, FilePath = _outPutName, Size = "-", Seconds = 0, Status = "处理中", Progress = "0%", }); } }); List taskList = []; semaphore = new(10); for (int combinationIndex = 0; combinationIndex < result.Count; combinationIndex++) { List combination = result[combinationIndex]; int currentIndex = combinationIndex + 1; // 序号从1开始,在闭包外计算 await semaphore.WaitAsync(); var _task = Task.Run(() => { // 获取第一个视频的文件名(不含扩展名) string firstVideoPath = combination[0]; string firstVideoName = Path.GetFileNameWithoutExtension(firstVideoPath); string _tempFileName = $"{firstVideoName}_{currentIndex:D4}.mp4"; string _outPutName = Path.Combine($"{VideoModel.FolderPath}", "output", _tempFileName); ; // 更新进度为"准备中" VideoModel.Dispatcher.Invoke(() => { var videoItem = VideoModel.ConcatVideos.FirstOrDefault(v => v.Index == currentIndex); if (videoItem != null) { videoItem.Progress = "准备中..."; videoItem.Status = "处理中"; // 进度值会在UpdateProgressValue中自动更新 } }); try { var temporaryVideoParts = combination.Select((_videoPath) => { string _tempMd5Name = VideoCombine.GetLargeFileMD5(_videoPath); //GlobalFFOptions.Current.TemporaryFilesFolder return Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); }).ToArray(); bool _isSuccess = false; // 更新进度为"拼接中" VideoModel.Dispatcher.Invoke(() => { var videoItem = VideoModel.ConcatVideos.FirstOrDefault(v => v.Index == currentIndex); if (videoItem != null) { videoItem.Progress = "0%"; videoItem.Status = "拼接中"; } }); try { // 计算总时长用于进度计算 double totalDuration = 0; try { foreach (var videoPath in combination) { var analysis = FFProbe.Analyse(videoPath); totalDuration += analysis.Duration.TotalSeconds; } } catch { totalDuration = 100; // 默认值 } int currentIndexForProgress = currentIndex; double processedDuration = 0; _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") ) .NotifyOnOutput((output) => { // 解析FFmpeg输出获取进度 // FFmpeg输出格式: time=00:00:05.00 bitrate=... if (output.Contains("time=")) { try { int timeIndex = output.IndexOf("time="); if (timeIndex >= 0) { string timeStr = output.Substring(timeIndex + 5, 11); // time=HH:MM:SS.mm if (TimeSpan.TryParse(timeStr, out TimeSpan currentTime)) { processedDuration = currentTime.TotalSeconds; int progressPercent = totalDuration > 0 ? (int)((processedDuration / totalDuration) * 100) : 0; progressPercent = Math.Min(100, Math.Max(0, progressPercent)); VideoModel.Dispatcher.Invoke(() => { var videoItem = VideoModel.ConcatVideos.FirstOrDefault(v => v.Index == currentIndexForProgress); if (videoItem != null) { videoItem.Progress = $"{progressPercent}%"; } }); } } } catch { // 忽略解析错误 } } }) .ProcessSynchronously(); } catch (Exception ex) { _isSuccess = false; LogUtils.Error("拼接视频失败", ex); } //bool _isSuccess = FFMpeg.Join(_outPutName, [.. combination]); // 更新拼接结果 VideoModel.Dispatcher.Invoke(() => { var videoItem = VideoModel.ConcatVideos.FirstOrDefault(v => v.Index == currentIndex); if (videoItem != null && File.Exists(_outPutName)) { try { IMediaAnalysis _mediaInfo = FFProbe.Analyse(_outPutName); FileInfo fileInfo = new(_outPutName); videoItem.FilePath = _outPutName; videoItem.Size = $"{fileInfo.Length / 1024 / 1024}MB"; videoItem.Seconds = ((int)_mediaInfo.Duration.TotalSeconds); videoItem.Status = _isSuccess ? "拼接成功" : "拼接失败"; videoItem.Progress = _isSuccess ? "100%" : "失败"; } catch (Exception ex) { videoItem.Status = "拼接失败"; videoItem.Progress = "失败"; LogUtils.Error($"分析视频信息失败:{_outPutName}", ex); } } else if (videoItem != null) { videoItem.Status = "拼接失败"; videoItem.Progress = "失败"; } }); if (_isSuccess && VideoModel.AuditImagePath != "") { // 使用 FFMpegCore 执行添加图片到视频的操作 try { // 配置 FFmpeg 二进制文件位置(如果 FFmpeg 不在系统路径中) // GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "path/to/ffmpeg/bin" }); // 使用相同的命名规则,添加 _img 后缀表示带水印 string firstVideoPathName = combination[0]; string firstVideoNameHas = Path.GetFileNameWithoutExtension(firstVideoPathName); string _outPutNameImg = Path.Combine($"{VideoModel.FolderPath}", "output", $"{firstVideoNameHas}_{currentIndex:D4}_img.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)} 合并成功"); } catch (Exception ex) { LogUtils.Error($"视频[${_outPutName}]:{string.Join(";", combination)} 合并失败", ex); } finally { semaphore.Release(); } }); taskList.Add(_task); } await Task.WhenAll(taskList).ContinueWith((s) => { //结束时间 DateTime endTime = DateTime.Now; double elapsedSeconds = (endTime - startTime).TotalSeconds; // 统计成功和失败的数量 int successCount = 0; int failCount = 0; string outputDir = ""; VideoModel.Dispatcher.Invoke(() => { successCount = VideoModel.ConcatVideos.Count(v => v.Status == "拼接成功"); failCount = VideoModel.ConcatVideos.Count(v => v.Status == "拼接失败"); if (VideoModel.ConcatVideos.Count > 0) { var firstVideo = VideoModel.ConcatVideos.FirstOrDefault(v => !string.IsNullOrEmpty(v.FilePath)); if (firstVideo != null && !string.IsNullOrEmpty(firstVideo.FilePath)) { outputDir = Path.GetDirectoryName(firstVideo.FilePath) ?? ""; } } }); LogUtils.Info($"所有视频拼接完成,用时{elapsedSeconds:F2}秒,成功{successCount}个,失败{failCount}个"); VideoModel.IsStart = false; VideoCombine.Cleanup(VideoCombine.mustClearPath); // 构建提示信息 string message = $"所有视频拼接完成!\n\n"; message += $"成功:{successCount} 个\n"; if (failCount > 0) { message += $"失败:{failCount} 个\n"; } message += $"用时:{elapsedSeconds:F2} 秒\n"; if (!string.IsNullOrEmpty(outputDir)) { message += $"\n输出目录:\n{outputDir}"; } VideoModel.Dispatcher.Invoke(() => { MessageBox.Show(message, "拼接完成", MessageBoxButton.OK, MessageBoxImage.Information); }); VideoCombine.mustClearPath = []; }); }); } } }; } private void ListFolder(string path) { DirectoryInfo dir = new(path); try { DirectoryInfo dirD = dir as DirectoryInfo; DirectoryInfo[] folders = [.. dirD.GetDirectories().OrderBy(d => d.Name)]; VideoModel.FolderInfos.Clear(); //获取文件夹下所有视频文件 foreach (DirectoryInfo Folder in folders) { if (Folder.Name != "output") { string[] files = Directory.GetFiles(Folder.FullName, "*.mp4"); LogUtils.Info($"{Folder.Name}下有{files.Length}个视频文件"); VideoModel.FolderInfos.Add(new VideoModel.FolderInfo { DirectoryInfo = Folder, Num = files.Length, VideoPaths = new List(files) }); } } VideoModel.UpdateSum(); } catch (Exception ex) { MessageBox.Show(ex.Message); return; } } } class Command : ICommand { public event EventHandler? CanExecuteChanged; public bool CanExecute(object? parameter) => true; public void Execute(object? parameter) { DoExcue?.Invoke(parameter); } public Action? DoExcue { get; set; } } }