531 lines
33 KiB
C#
531 lines
33 KiB
C#
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 _tmpFileName = $"{originalFileName}_{currentIndex:D4}_{modeSuffix}{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 videoName = Path.GetFileName(currentVideo);
|
||
string errorMsg = $"视频时长太短:{videoName}({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($"{originalFileName} (第{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 = $"{originalFileName} (第{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 = $"{originalFileName} (第{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 videoName = Path.GetFileName(currentVideo);
|
||
string errorMsg = $"抽帧异常:{videoName} (第{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";
|
||
ExtractWindowModel.Dispatcher.Invoke(() =>
|
||
{
|
||
var taskItem = new Models.ExtractTaskItem
|
||
{
|
||
Index = currentIndex.ToString(),
|
||
FileName = displayFileNameForError,
|
||
FullFileName = $"{originalFileNameForError}_{currentIndex:D4}_{modeSuffixForError2}{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;
|
||
}
|
||
}
|
||
}
|
||
}
|