VideoConcat/ViewModels/ExtractWindowViewModel.cs
2026-01-01 15:39:54 +08:00

343 lines
18 KiB
C#
Raw 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;
}
ExtractWindowModel.Dispatcher.Invoke(() =>
{
ExtractWindowModel.HelpInfo = $"开始处理,每个视频将生成 {extractCount} 个抽帧视频...";
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(); // 收集错误信息
// 对每个视频生成指定数量的抽帧视频
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 _tmpFileName = $"{originalFileName}_{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}");
// 如果文件已存在,跳过
if (File.Exists(outputPath))
{
LogUtils.Info($"文件已存在,跳过:{outputPath}");
Interlocked.Increment(ref completedTasks);
ExtractWindowModel.Dispatcher.Invoke(() =>
{
ExtractWindowModel.HelpInfo = $"处理中... ({completedTasks}/{totalTasks})";
});
return;
}
// 先检查视频时长
try
{
var mediaInfo = await FFProbe.AnalyseAsync(currentVideo);
double totalDuration = mediaInfo.Duration.TotalSeconds;
if (totalDuration < 20)
{
string videoName = Path.GetFileName(currentVideo);
string errorMsg = $"视频时长太短:{videoName}{totalDuration:F2}秒无法抽帧需要至少20秒";
LogUtils.Error(errorMsg);
errorMessages.Add(errorMsg);
Interlocked.Increment(ref completedTasks);
ExtractWindowModel.Dispatcher.Invoke(() =>
{
ExtractWindowModel.HelpInfo = $"处理中... ({completedTasks}/{totalTasks})";
});
return;
}
}
catch (Exception ex)
{
LogUtils.Error($"检查视频时长失败:{currentVideo}", ex);
// 继续处理,让 RemoveFrameRandomeAsync 来处理错误
}
bool success = await VideoProcess.RemoveFrameRandomeAsync(currentVideo, outputPath);
// 再次检查文件是否存在
if (File.Exists(outputPath))
{
FileInfo fileInfo = new FileInfo(outputPath);
LogUtils.Info($"抽帧成功:{currentVideo} -> {outputPath}, 文件大小={fileInfo.Length / 1024 / 1024}MB");
}
else if (success)
{
LogUtils.Warn($"抽帧返回成功但文件不存在:{outputPath}");
}
else
{
string videoName = Path.GetFileName(currentVideo);
string errorMsg = $"抽帧失败:{videoName}";
LogUtils.Error($"{errorMsg} -> {outputPath}");
errorMessages.Add(errorMsg);
}
// 更新完成计数
Interlocked.Increment(ref completedTasks);
ExtractWindowModel.Dispatcher.Invoke(() =>
{
ExtractWindowModel.HelpInfo = $"处理中... ({completedTasks}/{totalTasks})";
});
}
catch (Exception ex)
{
string videoName = Path.GetFileName(currentVideo);
string errorMsg = $"抽帧异常:{videoName} (第{currentIndex}个) - {ex.Message}";
LogUtils.Error($"抽帧失败:{currentVideo} (第{currentIndex}个)", ex);
errorMessages.Add(errorMsg);
Interlocked.Increment(ref completedTasks);
ExtractWindowModel.Dispatcher.Invoke(() =>
{
ExtractWindowModel.HelpInfo = $"处理中... ({completedTasks}/{totalTasks})";
});
}
finally
{
semaphore.Release();
}
});
_tasks.Add(_task);
}
}
await Task.WhenAll(_tasks);
// 统计实际生成的文件数量
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} 个视频文件");
}
}
// 构建最终信息
string summaryInfo = $"全部完成! 共处理 {completedTasks} 个任务,实际生成 {actualFileCount} 个视频文件\n输出目录{outputDir}";
ExtractWindowModel.Dispatcher.Invoke(() =>
{
// 如果有错误信息,显示汇总
if (errorMessages.Count > 0)
{
string errorSummary = string.Join("\n", errorMessages);
ExtractWindowModel.HelpInfo = $"{summaryInfo}\n\n错误信息共{errorMessages.Count}个):\n{errorSummary}";
}
else
{
ExtractWindowModel.HelpInfo = summaryInfo;
}
ExtractWindowModel.IsStart = false;
ExtractWindowModel.IsCanOperate = true;
});
LogUtils.Info($"抽帧处理完成,共处理 {completedTasks} 个任务,实际生成 {actualFileCount} 个视频文件,输出目录:{outputDir}");
});
}
},
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;
}
}
}
}