532 lines
28 KiB
C#
532 lines
28 KiB
C#
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<ConcatVideo> 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<List<string>> combinations = [];
|
||
List<string> currentCombination = [];
|
||
List<List<string>> videoLists = [];
|
||
|
||
|
||
VideoModel.FolderInfos.ForEach(folderInfo =>
|
||
{
|
||
videoLists.Add(folderInfo.VideoPaths);
|
||
});
|
||
string[] _converVideoPath = [];
|
||
List<List<string>> result = [];
|
||
Random random = new();
|
||
if (VideoModel.IsJoinType1Selected)
|
||
{
|
||
VideoCombine.GenerateCombinations(videoLists, 0, currentCombination, combinations);
|
||
|
||
// 复制原列表,避免修改原列表
|
||
List<List<string>> 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<string> list2 = [];
|
||
foreach (List<string> list in videoLists)
|
||
{
|
||
_converVideoPath=[.. _converVideoPath, list[index]];
|
||
list2.Add(list[index]);
|
||
}
|
||
|
||
result.Add(list2);
|
||
}
|
||
}
|
||
|
||
SemaphoreSlim semaphore = new(10); // Limit to 3 threads
|
||
|
||
List<Task> _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<string> 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<Task> taskList = [];
|
||
semaphore = new(10);
|
||
|
||
for (int combinationIndex = 0; combinationIndex < result.Count; combinationIndex++)
|
||
{
|
||
List<string> 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<string>(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<Object?>? DoExcue { get; set; }
|
||
}
|
||
}
|