VideoConcat/ViewModels/ExtractWindowViewModel.cs
2026-01-15 22:06:51 +08:00

531 lines
33 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ConcatVideo> 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<Task> _tasks = [];
int totalTasks = ExtractWindowModel.videos.Length * extractCount;
int completedTasks = 0;
System.Collections.Concurrent.ConcurrentBag<string> errorMessages = new(); // 收集错误信息
System.Collections.Concurrent.ConcurrentBag<string> 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<Task> _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;
}
}
}
}