using FFMpegCore; using FFMpegCore.Enums; using Microsoft.Expression.Drawing.Core; using System.IO; using System.Threading; using System.Windows; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Threading; using VideoConcat.Common.Tools; using VideoConcat.Models; using VideoConcat.Services.Video; using static VideoConcat.Models.VideoModel; using MessageBox = System.Windows.MessageBox; namespace VideoConcat.ViewModels { public class ExtractWindowViewModel { private ExtractWindowModel _extractWindowModel; public List ConcatVideos { get; set; } = []; public ExtractWindowModel ExtractWindowModel { get { return _extractWindowModel; } set { _extractWindowModel = value; } } public ExtractWindowViewModel() { ExtractWindowModel = new ExtractWindowModel { CanExtractFrame = false, CanModify = false, IsStart = false, IsCanOperate = true, BtnOpenFolderCommand = new Command() { DoExcue = obj => { System.Windows.Forms.FolderBrowserDialog folderBrowserDialog = new(); if (folderBrowserDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { ExtractWindowModel.FolderPath = folderBrowserDialog.SelectedPath; LogUtils.Info($"获取视频文件夹,视频路径:{ExtractWindowModel.FolderPath}"); ListFolder(); } } }, BtnStartVideoConcatCommand = new Command() { DoExcue = obj => { // 在后台任务中执行异步操作 Task.Run(async () => { int extractCount = ExtractWindowModel.ExtractCount; if (extractCount <= 0) { ExtractWindowModel.Dispatcher.Invoke(() => { MessageBox.Show("请输入有效的生成个数(大于0)!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); }); return; } string modeText = ExtractWindowModel.ExtractFrameMode == Models.ExtractFrameMode.DeleteFrame ? "方案1" : "方案2"; DateTime startTime = DateTime.Now; ExtractWindowModel.Dispatcher.Invoke(() => { ExtractWindowModel.HelpInfo = $"处理方案:{modeText} | 视频数量:{ExtractWindowModel.videos.Length} 个 | 每个生成:{extractCount} 个 | 总任务数:{ExtractWindowModel.videos.Length * extractCount} 个 | 开始时间:{startTime:yyyy-MM-dd HH:mm:ss}"; // 清空任务列表,以便重新开始 ExtractWindowModel.TaskItems.Clear(); ExtractWindowModel.IsStart = true; ExtractWindowModel.IsCanOperate = false; }); SemaphoreSlim semaphore = new(10); // 限制并发数量 List _tasks = []; int totalTasks = ExtractWindowModel.videos.Length * extractCount; int completedTasks = 0; System.Collections.Concurrent.ConcurrentBag errorMessages = new(); // 收集错误信息 System.Collections.Concurrent.ConcurrentBag successMessages = new(); // 收集成功信息 long totalOriginalSize = 0; // 原始文件总大小 long totalOutputSize = 0; // 输出文件总大小 int successCount = 0; // 成功数量 int skipCount = 0; // 跳过数量(已存在) // 对每个视频生成指定数量的抽帧视频 foreach (var video in ExtractWindowModel.videos) { for (int i = 1; i <= extractCount; i++) { int currentIndex = i; // 闭包变量 string currentVideo = video; // 闭包变量 await semaphore.WaitAsync(); var _task = Task.Run(async () => { try { string _tmpPath = Path.GetDirectoryName(currentVideo) ?? ""; if (string.IsNullOrEmpty(_tmpPath)) { LogUtils.Error($"无法获取视频目录:{currentVideo}"); Interlocked.Increment(ref completedTasks); ExtractWindowModel.Dispatcher.Invoke(() => { ExtractWindowModel.HelpInfo = $"处理中... ({completedTasks}/{totalTasks})"; }); return; } string originalFileName = Path.GetFileNameWithoutExtension(currentVideo); string extension = Path.GetExtension(currentVideo); // 生成唯一的文件名:原始名称+方案号+生成日期+序号 string modeSuffix = ExtractWindowModel.ExtractFrameMode == Models.ExtractFrameMode.DeleteFrame ? "1" : "2"; string dateStr = DateTime.Now.ToString("MMdd"); string _tmpFileName = $"{originalFileName}_{dateStr}{modeSuffix}{currentIndex:D4}{extension}"; string outPath = Path.Combine(_tmpPath, "out"); LogUtils.Info($"准备创建输出目录:{outPath}"); try { if (!Directory.Exists(outPath)) { Directory.CreateDirectory(outPath); LogUtils.Info($"已创建输出目录:{outPath}"); } } catch (Exception ex) { LogUtils.Error($"创建输出目录失败:{outPath}", ex); Interlocked.Increment(ref completedTasks); ExtractWindowModel.Dispatcher.Invoke(() => { ExtractWindowModel.HelpInfo = $"处理中... ({completedTasks}/{totalTasks})"; }); return; } string outputPath = Path.Combine(outPath, _tmpFileName); LogUtils.Info($"开始抽帧:输入={currentVideo}, 输出={outputPath}"); // 定义显示用的变量(提前定义,避免作用域问题) // 显示完整的文件名,让DataGrid的列宽和ToolTip来处理显示 string displayFileName = originalFileName; int currentCompleted; double progressPercent; // 获取原始文件大小 long originalSize = 0; if (File.Exists(currentVideo)) { FileInfo originalFileInfo = new FileInfo(currentVideo); originalSize = originalFileInfo.Length; Interlocked.Add(ref totalOriginalSize, originalSize); } // 如果文件已存在,跳过(不覆盖) if (File.Exists(outputPath)) { LogUtils.Info($"文件已存在,跳过:{outputPath}"); Interlocked.Increment(ref skipCount); Interlocked.Increment(ref completedTasks); currentCompleted = completedTasks; progressPercent = currentCompleted * 100.0 / totalTasks; ExtractWindowModel.Dispatcher.Invoke(() => { var taskItem = new Models.ExtractTaskItem { Index = currentIndex.ToString(), FileName = displayFileName, FullFileName = _tmpFileName, Status = "跳过", OriginalSize = "--", OutputSize = "--", SizeChange = "--", Duration = "--", Progress = $"{progressPercent:F1}%" }; ExtractWindowModel.TaskItems.Add(taskItem); }); return; } // 先检查视频时长和获取视频信息 IMediaAnalysis? mediaInfo = null; double totalDuration = 0; try { mediaInfo = await FFProbe.AnalyseAsync(currentVideo); totalDuration = mediaInfo.Duration.TotalSeconds; if (totalDuration < 20 && ExtractWindowModel.ExtractFrameMode == Models.ExtractFrameMode.DeleteFrame) { string errorMsg = $"视频时长太短:{currentVideo}({totalDuration:F2}秒),需要至少20秒"; LogUtils.Error(errorMsg); errorMessages.Add(errorMsg); Interlocked.Increment(ref completedTasks); currentCompleted = completedTasks; progressPercent = currentCompleted * 100.0 / totalTasks; ExtractWindowModel.Dispatcher.Invoke(() => { var taskItem = new Models.ExtractTaskItem { Index = currentIndex.ToString(), FileName = displayFileName, FullFileName = _tmpFileName, Status = "失败", OriginalSize = "--", OutputSize = "--", SizeChange = "--", Duration = "--", Progress = $"{progressPercent:F1}%" }; ExtractWindowModel.TaskItems.Add(taskItem); }); return; } } catch (Exception ex) { LogUtils.Error($"检查视频时长失败:{currentVideo}", ex); // 继续处理,让后续方法来处理错误 } // 记录开始时间 DateTime taskStartTime = DateTime.Now; // 根据选择的处理方式调用相应方法 bool success = false; if (ExtractWindowModel.ExtractFrameMode == Models.ExtractFrameMode.DeleteFrame) { // 方案1:随机删除一个非关键帧(传入index确保每次位置不同) success = await VideoProcess.RemoveNonKeyFrameAsync(currentVideo, outputPath, currentIndex); } else if (ExtractWindowModel.ExtractFrameMode == Models.ExtractFrameMode.AddTransparentImage) { // 方案2:在随机位置添加透明图 success = await VideoProcess.AddTransparentImageAsync(currentVideo, outputPath); } else { // 默认使用原来的方法(保留兼容性) success = await VideoProcess.RemoveFrameRandomeAsync(currentVideo, outputPath); } // 计算处理时间 TimeSpan taskDuration = DateTime.Now - taskStartTime; // 更新完成计数 Interlocked.Increment(ref completedTasks); currentCompleted = completedTasks; progressPercent = currentCompleted * 100.0 / totalTasks; // 再次检查文件是否存在 if (File.Exists(outputPath)) { FileInfo outputFileInfo = new FileInfo(outputPath); long outputSize = outputFileInfo.Length; Interlocked.Add(ref totalOutputSize, outputSize); Interlocked.Increment(ref successCount); double originalMB = originalSize / 1024.0 / 1024.0; double outputMB = outputSize / 1024.0 / 1024.0; double sizeDiff = outputSize - originalSize; double sizeDiffMB = Math.Abs(sizeDiff) / 1024.0 / 1024.0; string sizeChange = sizeDiff > 0 ? $"+{sizeDiffMB:F2}" : sizeDiff < 0 ? $"-{sizeDiffMB:F2}" : "0"; string sizeChangeMB = sizeChange + "MB"; string originalSizeStr = $"{originalMB:F2}MB"; string outputSizeStr = $"{outputMB:F2}MB"; string durationStr = $"{taskDuration.TotalSeconds:F1}s"; successMessages.Add($"{currentVideo} (第{currentIndex}个) - 成功"); LogUtils.Info($"处理成功:{currentVideo} -> {outputPath}, 大小={outputMB:F2}MB, 耗时={taskDuration.TotalSeconds:F2}秒"); // 更新表格行 ExtractWindowModel.Dispatcher.Invoke(() => { var taskItem = new Models.ExtractTaskItem { Index = currentIndex.ToString(), FileName = displayFileName, FullFileName = _tmpFileName, Status = "成功", OriginalSize = originalSizeStr, OutputSize = outputSizeStr, SizeChange = sizeChangeMB, Duration = durationStr, Progress = $"{progressPercent:F1}%" }; ExtractWindowModel.TaskItems.Add(taskItem); }); } else if (success) { LogUtils.Warn($"处理返回成功但文件不存在:{outputPath}"); string errorMsg = $"{currentVideo} (第{currentIndex}个) - 返回成功但文件不存在"; errorMessages.Add(errorMsg); ExtractWindowModel.Dispatcher.Invoke(() => { var taskItem = new Models.ExtractTaskItem { Index = currentIndex.ToString(), FileName = displayFileName, FullFileName = _tmpFileName, Status = "警告", OriginalSize = "--", OutputSize = "--", SizeChange = "--", Duration = "--", Progress = $"{progressPercent:F1}%" }; ExtractWindowModel.TaskItems.Add(taskItem); }); } else { string errorMsg = $"{currentVideo} (第{currentIndex}个) - 处理失败"; LogUtils.Error($"{errorMsg} -> {outputPath}"); errorMessages.Add(errorMsg); ExtractWindowModel.Dispatcher.Invoke(() => { var taskItem = new Models.ExtractTaskItem { Index = currentIndex.ToString(), FileName = displayFileName, FullFileName = _tmpFileName, Status = "失败", OriginalSize = "--", OutputSize = "--", SizeChange = "--", Duration = "--", Progress = $"{progressPercent:F1}%" }; ExtractWindowModel.TaskItems.Add(taskItem); }); } } catch (Exception ex) { // 在异常处理中重新获取文件名等信息 string originalFileNameForError = Path.GetFileNameWithoutExtension(currentVideo); string errorMsg = $"抽帧异常:{currentVideo} (第{currentIndex}个) - {ex.Message}"; LogUtils.Error($"抽帧失败:{currentVideo} (第{currentIndex}个)", ex); errorMessages.Add(errorMsg); Interlocked.Increment(ref completedTasks); int currentCompletedForError = completedTasks; double progressPercentForError = currentCompletedForError * 100.0 / totalTasks; string displayFileNameForError = originalFileNameForError.Length > 15 ? originalFileNameForError.Substring(0, 12) + "..." : originalFileNameForError.PadRight(15); string modeSuffixForError2 = ExtractWindowModel.ExtractFrameMode == Models.ExtractFrameMode.DeleteFrame ? "方案1" : "方案2"; string dateStrForError2 = DateTime.Now.ToString("yyyyMMdd"); ExtractWindowModel.Dispatcher.Invoke(() => { var taskItem = new Models.ExtractTaskItem { Index = currentIndex.ToString(), FileName = displayFileNameForError, FullFileName = $"{originalFileNameForError}_{modeSuffixForError2}_{dateStrForError2}_{currentIndex:D4}{Path.GetExtension(currentVideo)}", Status = "异常", OriginalSize = "--", OutputSize = "--", SizeChange = "--", Duration = "--", Progress = $"{progressPercentForError:F1}%" }; ExtractWindowModel.TaskItems.Add(taskItem); }); } finally { semaphore.Release(); } }); _tasks.Add(_task); } } await Task.WhenAll(_tasks); // 计算总耗时 TimeSpan totalDuration = DateTime.Now - startTime; // 统计实际生成的文件数量 int actualFileCount = 0; string outputDir = ""; if (ExtractWindowModel.videos.Length > 0) { string firstVideo = ExtractWindowModel.videos[0]; string videoDir = Path.GetDirectoryName(firstVideo) ?? ""; outputDir = Path.Combine(videoDir, "out"); if (Directory.Exists(outputDir)) { actualFileCount = Directory.GetFiles(outputDir, "*.mp4").Length; LogUtils.Info($"输出目录 {outputDir} 中共有 {actualFileCount} 个视频文件"); } } // 构建最终详细信息 double totalOriginalMB = totalOriginalSize / 1024.0 / 1024.0; double totalOutputMB = totalOutputSize / 1024.0 / 1024.0; double totalSizeDiff = totalOutputSize - totalOriginalSize; double totalSizeDiffMB = Math.Abs(totalSizeDiff) / 1024.0 / 1024.0; string totalSizeChange = totalSizeDiff > 0 ? $"+{totalSizeDiffMB:F2}MB" : totalSizeDiff < 0 ? $"-{totalSizeDiffMB:F2}MB" : "0MB"; string summaryInfo = $"\n处理完成!\n" + $"处理方案:{modeText} | 总任务数:{totalTasks} 个\n" + $"成功数量:{successCount} 个 | 失败数量:{errorMessages.Count} 个 | 跳过数量:{skipCount} 个\n" + $"实际生成:{actualFileCount} 个文件\n" + $"总原始大小:{totalOriginalMB:F2}MB | 总输出大小:{totalOutputMB:F2}MB | 大小变化:{totalSizeChange}\n" + $"总耗时:{totalDuration.TotalSeconds:F1}秒 ({totalDuration.TotalMinutes:F1}分钟) | 平均速度:{(successCount > 0 ? totalDuration.TotalSeconds / successCount : 0):F2}秒/个\n" + $"输出目录:{outputDir}\n" + $"完成时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}"; ExtractWindowModel.Dispatcher.Invoke(() => { ExtractWindowModel.HelpInfo += $"\n{summaryInfo}"; // 如果有错误信息,显示汇总 if (errorMessages.Count > 0) { ExtractWindowModel.HelpInfo += $"\n错误信息(共{errorMessages.Count}个):"; foreach (var error in errorMessages) { ExtractWindowModel.HelpInfo += $"\n • {error}"; } } ExtractWindowModel.IsStart = false; ExtractWindowModel.IsCanOperate = true; }); LogUtils.Info($"处理完成,成功={successCount}, 失败={errorMessages.Count}, 跳过={skipCount}, 总耗时={totalDuration.TotalSeconds:F1}秒"); }); } }, BtnStartVideoModifyCommand = new Command() { DoExcue = obj => { ExtractWindowModel.HelpInfo = ""; SemaphoreSlim semaphore = new(10); // Limit to 3 threads List _tasks = []; ExtractWindowModel.videos.ForEach(async (video) => { await semaphore.WaitAsync(); // Wait when more than 3 threads are running var _task = Task.Run(async () => { try { // 实例化并调用 var remover = new VideoProcess(); // 删除4秒处的帧(需根据实际帧位置调整) string _tmpPath = Path.GetDirectoryName(video) ?? ""; string _tmpFileName = $"{(new Random()).Next(10000, 99999)}{Path.GetFileName(video)}"; string outPath = Path.Combine(_tmpPath, "out"); if (!Path.Exists(outPath)) { Directory.CreateDirectory(outPath); } VideoProcess.ModifyByMetadata(video, $"{_tmpPath}\\out\\modify{_tmpFileName}"); } finally { semaphore.Release(); // Work is done, signal to semaphore that more work is possible } }); _tasks.Add(_task); }); Task.WhenAll(_tasks).ContinueWith((task) => { ExtractWindowModel.HelpInfo = "全部完成!"; }); } } }; } private void ListFolder() { DirectoryInfo dir = new(ExtractWindowModel.FolderPath); try { DirectoryInfo dirD = dir as DirectoryInfo; //获取文件夹下所有视频文件 ExtractWindowModel.videos = Directory.GetFiles(dirD.FullName, "*.mp4"); ExtractWindowModel.SetCanStart(); } catch (Exception ex) { MessageBox.Show(ex.Message); return; } } } }