diff --git a/App.config b/App.config
index 3190def..7a50127 100644
--- a/App.config
+++ b/App.config
@@ -3,6 +3,6 @@
-
+
\ No newline at end of file
diff --git a/App.xaml b/App.xaml
index 6384de0..7b4b078 100644
--- a/App.xaml
+++ b/App.xaml
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VideoConcat"
- xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"
+ xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers" xmlns:local1="clr-namespace:VideoConcat.Views"
StartupUri="Views/MainWindow.xaml">
diff --git a/Common/Tools/VideoCombine.cs b/Common/Tools/VideoCombine.cs
index 03d97fd..a58b474 100644
--- a/Common/Tools/VideoCombine.cs
+++ b/Common/Tools/VideoCombine.cs
@@ -18,7 +18,7 @@ namespace VideoConcat.Common.Tools
{
if (index == videoLists.Count)
{
- result.Add(new List(currentCombination));
+ result.Add([.. currentCombination]);
return;
}
diff --git a/EnumToBooleanConverter.cs b/EnumToBooleanConverter.cs
new file mode 100644
index 0000000..299b93b
--- /dev/null
+++ b/EnumToBooleanConverter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+
+namespace VideoConcat
+{
+ public class EnumToBooleanConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value.Equals(parameter);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return value?.Equals(true) == true ? parameter : System.Windows.Data.Binding.DoNothing;
+ }
+ }
+}
diff --git a/Models/VideoModel.cs b/Models/VideoModel.cs
index 644cc91..eb312f4 100644
--- a/Models/VideoModel.cs
+++ b/Models/VideoModel.cs
@@ -4,18 +4,27 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
+using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
using System.Xml.Linq;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.Header;
+using static VideoConcat.Models.VideoModel;
namespace VideoConcat.Models
{
+
+ public enum Gender
+ {
+ Male,
+ Female
+ }
public class VideoModel : INotifyPropertyChanged
{
private int _num;
@@ -23,7 +32,7 @@ namespace VideoConcat.Models
private string _folderPath = "";
private string _auditImagePath = "";
private bool _canStart = false;
- private bool _isCanOperate=false;
+ private bool _isCanOperate = false;
private bool _isStart = false;
private ObservableCollection _FolderInfos = [];
private ObservableCollection _concatVideos = [];
@@ -192,14 +201,21 @@ namespace VideoConcat.Models
int _temp = 1;
if (FolderInfos.Count > 0)
{
- foreach (FolderInfo item in FolderInfos)
+ if (IsJoinType1Selected)
{
- if (item.Num > 0)
+ foreach (FolderInfo item in FolderInfos)
{
- _temp *= item.Num;
+ if (item.Num > 0)
+ {
+ _temp *= item.Num;
+ }
}
+ MaxNum = _temp;
+ }
+ if (IsJoinType2Selected)
+ {
+ MaxNum = FolderInfos[0].Num;
}
- MaxNum = _temp;
SetCanStart();
}
else
@@ -227,5 +243,29 @@ namespace VideoConcat.Models
ConcatVideos = [];
_dispatcher = Dispatcher.CurrentDispatcher;
}
+
+ private bool _isJoinType1Selected;
+ private bool _isJoinType2Selected;
+
+ public bool IsJoinType1Selected
+ {
+ get { return _isJoinType1Selected; }
+ set
+ {
+ _isJoinType1Selected = value;
+ OnPropertyChanged(nameof(IsJoinType1Selected));
+ }
+ }
+
+ public bool IsJoinType2Selected
+ {
+ get { return _isJoinType2Selected; }
+ set
+ {
+ _isJoinType2Selected = value;
+ OnPropertyChanged(nameof(IsJoinType2Selected));
+ }
+ }
}
+
}
diff --git a/Services/BaseService.cs b/Services/BaseService.cs
new file mode 100644
index 0000000..9813c91
--- /dev/null
+++ b/Services/BaseService.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace VideoConcat.Services
+{
+ abstract class BaseService
+ {
+ }
+}
diff --git a/Services/Video/VideoService.cs b/Services/Video/VideoService.cs
new file mode 100644
index 0000000..e84a31d
--- /dev/null
+++ b/Services/Video/VideoService.cs
@@ -0,0 +1,244 @@
+using FFMpegCore.Enums;
+using FFMpegCore.Helpers;
+using FFMpegCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using VideoConcat.Common.Tools;
+using System.Security.Cryptography;
+using System.IO;
+using VideoConcat.Models;
+using static VideoConcat.Models.VideoModel;
+
+
+
+namespace VideoConcat.Services.Video
+{
+ internal class VideoService : BaseService
+ {
+ ///
+ /// 将视频文件转换为ts格式
+ ///
+ public static string ConvertVideos(string videoPath)
+ {
+
+ var video = FFProbe.Analyse(videoPath);
+
+ string mediaInfo = "";
+
+ mediaInfo += $"视频: {videoPath}";
+ mediaInfo += $"时长: {video.Duration}";
+ mediaInfo += $"格式: {video.Format}";
+ // 视频流信息
+ foreach (var videoStream in video.VideoStreams)
+ {
+ mediaInfo += $"视频编码: {videoStream.CodecName}, 分辨率: {videoStream.Width}x{videoStream.Height}";
+ }
+ // 音频流信息
+ foreach (var audioStream in video.AudioStreams)
+ {
+ mediaInfo += $"音频编码: {audioStream.CodecName}, 采样率: {audioStream.SampleRateHz} Hz";
+ }
+
+ LogUtils.Info(mediaInfo);
+
+
+ FFMpegHelper.ConversionSizeExceptionCheck(video);
+
+ string _tempMd5Name = GetLargeFileMD5(videoPath);
+
+ //GlobalFFOptions.Current.TemporaryFilesFolder
+ var destinationPath = Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}");
+
+ if (File.Exists(destinationPath))
+ {
+ return destinationPath;
+ }
+
+ //Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder);
+ try
+ {
+ FFMpeg.Convert(videoPath, destinationPath, VideoType.Ts);
+ }
+ catch (Exception ex)
+ {
+
+ LogUtils.Info("视频转换失败!尝试另外一种转换");
+ // 创建FFmpeg参数
+ try
+ {
+ FFMpegArguments
+ .FromFileInput(videoPath)
+ .OutputToFile(destinationPath, true, o => o
+ .WithVideoCodec("libx264") // 设置视频编码器
+ .WithAudioCodec("aac") // 设置音频编码器
+ .WithAudioSamplingRate(44100)
+ .WithAudioBitrate(128000)
+ .WithConstantRateFactor(23) // 设置质量调整参数
+ .WithCustomArgument("-vf fps=30") // 强制指定帧率(例如 30fps)
+ .WithFastStart()
+ //.WithCustomArgument("-movflags +faststart") // 确保 moov atom 正确写入
+ //.CopyChannel()
+ //.WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB)
+ .ForceFormat(VideoType.Ts)
+ )
+ //.NotifyOnOutput(Console.WriteLine) // 打印 FFmpeg 详细日志
+ .ProcessSynchronously(true);
+ //FFMpeg.Convert(destinationPathExecp, destinationPath, VideoType.Ts);
+
+ }
+ catch (Exception e)
+ {
+ LogUtils.Error($"{videoPath} 转换失败", ex);
+ LogUtils.Error($"{videoPath} 转换再次失败", e);
+ }
+
+ }
+
+ return destinationPath;
+ }
+
+ public static string GetLargeFileMD5(string filePath)
+ {
+ using var md5 = MD5.Create();
+ using var stream = File.OpenRead(filePath);
+
+ byte[] buffer = new byte[8192]; // 8KB 缓冲区
+ int bytesRead;
+ long totalBytesRead = 0;
+
+ while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ md5.TransformBlock(buffer, 0, bytesRead, null, 0);
+ totalBytesRead += bytesRead;
+ // 可在此添加进度显示:Console.WriteLine($"已读取 {totalBytesRead / 1024 / 1024}MB");
+ }
+
+ md5.TransformFinalBlock(buffer, 0, 0);
+
+ return BitConverter.ToString(value: md5.Hash ?? []).Replace("-", "").ToLowerInvariant();
+ }
+
+
+ ///
+ /// 清理临时文件
+ ///
+ public static void Cleanup(List pathList)
+ {
+ foreach (var path in pathList)
+ {
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
+ }
+
+ public static void JoinVideos(List combination)
+ {
+ VideoModel videoModel = new();
+
+ if (Directory.Exists($"{videoModel.FolderPath}\\output") == false)
+ {
+ Directory.CreateDirectory($"{videoModel.FolderPath}\\output");
+ }
+
+
+ Random random = new();
+ string _tempFileName = $"{DateTime.Now:yyyyMMddHHmmss}{random.Next(100000, 999999)}.mp4";
+
+ string _outPutName = Path.Combine($"{videoModel.FolderPath}", "output", _tempFileName); ;
+
+
+
+ var temporaryVideoParts = combination.Select((_videoPath) =>
+ {
+ string _tempMd5Name = VideoService.GetLargeFileMD5(_videoPath);
+ //GlobalFFOptions.Current.TemporaryFilesFolder
+ return Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}");
+ }).ToArray();
+
+ bool _isSuccess = false;
+
+
+ try
+ {
+ _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")
+ )
+ .ProcessSynchronously();
+ }
+ catch (Exception ex1)
+ {
+ {
+ LogUtils.Error("拼接视频失败", ex1);
+ }
+
+ //bool _isSuccess = FFMpeg.Join(_outPutName, [.. combination]);
+
+ videoModel.Dispatcher.Invoke(() =>
+ {
+ IMediaAnalysis _mediaInfo = FFProbe.Analyse(_outPutName);
+ FileInfo fileInfo = new(_outPutName);
+
+ videoModel.ConcatVideos.Add(new ConcatVideo()
+ {
+ Index = videoModel.ConcatVideos.Count + 1,
+ FileName = _tempFileName,
+ Size = $"{fileInfo.Length / 1024 / 1024}MB",
+ Seconds = ((int)_mediaInfo.Duration.TotalSeconds),
+ Status = _isSuccess ? "拼接成功" : "拼接失败",
+ Progress = "100%",
+ });
+ });
+
+
+ if (_isSuccess && videoModel.AuditImagePath != "")
+ {
+ // 使用 FFMpegCore 执行添加图片到视频的操作
+ try
+ {
+ // 配置 FFmpeg 二进制文件位置(如果 FFmpeg 不在系统路径中)
+ // GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "path/to/ffmpeg/bin" });
+
+ string _outPutNameImg = $"{videoModel.FolderPath}\\output\\{DateTime.Now:yyyyMMddHHmmss}{random.Next(100000, 999999)}.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)} 合并成功");
+ }
+ }
+ }
+}
diff --git a/VideoConcat.sln b/VideoConcat.sln
index ed7cba0..5093c67 100644
--- a/VideoConcat.sln
+++ b/VideoConcat.sln
@@ -5,8 +5,6 @@ VisualStudioVersion = 17.11.35327.3
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VideoConcat", "VideoConcat.csproj", "{2FF5691C-3184-4B68-944B-C704E64C4E4E}"
EndProject
-Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "视频", "..\视频\视频.vdproj", "{F784558F-CA6F-E806-1DC3-7B0C364779F3}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -17,8 +15,6 @@ Global
{2FF5691C-3184-4B68-944B-C704E64C4E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FF5691C-3184-4B68-944B-C704E64C4E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FF5691C-3184-4B68-944B-C704E64C4E4E}.Release|Any CPU.Build.0 = Release|Any CPU
- {F784558F-CA6F-E806-1DC3-7B0C364779F3}.Debug|Any CPU.ActiveCfg = Debug
- {F784558F-CA6F-E806-1DC3-7B0C364779F3}.Release|Any CPU.ActiveCfg = Release
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/ViewModels/VideoViewModel.cs b/ViewModels/VideoViewModel.cs
index 2860a04..f1446c6 100644
--- a/ViewModels/VideoViewModel.cs
+++ b/ViewModels/VideoViewModel.cs
@@ -35,6 +35,9 @@ namespace VideoConcat.ViewModels
{
FolderInfos = [],
ConcatVideos = [],
+ IsJoinType1Selected = true,
+ IsJoinType2Selected = false,
+
MaxNum = 0,
CanStart = false,
IsStart = false,
@@ -106,33 +109,52 @@ namespace VideoConcat.ViewModels
List> videoLists = [];
-
VideoModel.FolderInfos.ForEach(folderInfo =>
{
videoLists.Add(folderInfo.VideoPaths);
});
-
- VideoCombine.GenerateCombinations(videoLists, 0, currentCombination, combinations);
-
-
+ string[] _converVideoPath = [];
List> result = [];
Random random = new();
-
-
- // 复制原列表,避免修改原列表
- List> tempList = [.. combinations];
-
- string[] _converVideoPath = [];
-
-
- for (int i = 0; i < VideoModel.Num && tempList.Count > 0; i++)
+ if (VideoModel.IsJoinType1Selected)
{
- int index = random.Next(tempList.Count);
- result.Add(tempList[index]);
+ VideoCombine.GenerateCombinations(videoLists, 0, currentCombination, combinations);
- _converVideoPath = [.. _converVideoPath, .. tempList[index]];
+ // 复制原列表,避免修改原列表
+ List> tempList = [.. combinations];
+ for (int i = 0; i < VideoModel.Num && tempList.Count > 0; i++)
+ {
+ int index = random.Next(tempList.Count);
+ result.Add(tempList[index]);
- tempList.RemoveAt(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 list2 = [];
+ foreach (List list in videoLists)
+ {
+ _converVideoPath=[.. _converVideoPath, list[index]];
+ list2.Add(list[index]);
+ }
+
+ result.Add(list2);
+ }
}
SemaphoreSlim semaphore = new(10); // Limit to 3 threads
@@ -301,8 +323,8 @@ namespace VideoConcat.ViewModels
try
{
DirectoryInfo dirD = dir as DirectoryInfo;
- DirectoryInfo[] folders = [.. dirD.GetDirectories().OrderBy(d=>d.Name)];
-
+ DirectoryInfo[] folders = [.. dirD.GetDirectories().OrderBy(d => d.Name)];
+
VideoModel.FolderInfos.Clear();
//获取文件夹下所有视频文件
foreach (DirectoryInfo Folder in folders)
diff --git a/Views/Video.xaml b/Views/Video.xaml
index 47424b1..96ea3a1 100644
--- a/Views/Video.xaml
+++ b/Views/Video.xaml
@@ -32,6 +32,11 @@
+
+
+
+
+
@@ -45,7 +50,7 @@
-
+
diff --git a/Views/Video.xaml.cs b/Views/Video.xaml.cs
index f6a48fe..e4dcc47 100644
--- a/Views/Video.xaml.cs
+++ b/Views/Video.xaml.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;