From dd2827dc7198b4c17c88e94c03136c91232c6a6b Mon Sep 17 00:00:00 2001 From: xiangbing Date: Tue, 12 Aug 2025 00:13:45 +0800 Subject: [PATCH] update --- App.xaml | 4 +- Common/Api/Base/SystemApi.cs | 22 +- .../EnumToBooleanConverter.cs | 2 +- Models/ExtractWindowModel.cs | 146 ++++++++++++ Models/MainWindowModel.cs | 56 +++++ Services/Video/VideoProcess.cs | 109 +++++++++ VideoConcat.csproj | 60 ++--- ViewModels/ExtractWindowViewModel.cs | 99 ++++++++ ViewModels/MainWindowViewModel.cs | 19 ++ Views/ExtractWindow.xaml | 43 ++++ Views/ExtractWindow.xaml.cs | 29 +++ Views/LoginWindow.xaml | 94 ++++++++ Views/LoginWindow.xaml.cs | 67 ++++++ Views/MainWindow.xaml | 217 +++++++++++------- Views/MainWindow.xaml.cs | 105 ++++++--- Views/{Video.xaml => VideoWindow.xaml} | 16 +- Views/{Video.xaml.cs => VideoWindow.xaml.cs} | 4 +- 17 files changed, 931 insertions(+), 161 deletions(-) rename EnumToBooleanConverter.cs => Conversions/EnumToBooleanConverter.cs (94%) create mode 100644 Models/ExtractWindowModel.cs create mode 100644 Models/MainWindowModel.cs create mode 100644 Services/Video/VideoProcess.cs create mode 100644 ViewModels/ExtractWindowViewModel.cs create mode 100644 ViewModels/MainWindowViewModel.cs create mode 100644 Views/ExtractWindow.xaml create mode 100644 Views/ExtractWindow.xaml.cs create mode 100644 Views/LoginWindow.xaml create mode 100644 Views/LoginWindow.xaml.cs rename Views/{Video.xaml => VideoWindow.xaml} (94%) rename Views/{Video.xaml.cs => VideoWindow.xaml.cs} (86%) diff --git a/App.xaml b/App.xaml index 7b4b078..9944b06 100644 --- a/App.xaml +++ b/App.xaml @@ -2,14 +2,16 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:VideoConcat" + xmlns:conv="clr-namespace:VideoConcat.Conversions" xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers" xmlns:local1="clr-namespace:VideoConcat.Views" - StartupUri="Views/MainWindow.xaml"> + StartupUri="Views/LoginWindow.xaml"> + diff --git a/Common/Api/Base/SystemApi.cs b/Common/Api/Base/SystemApi.cs index aa5786d..406479c 100644 --- a/Common/Api/Base/SystemApi.cs +++ b/Common/Api/Base/SystemApi.cs @@ -1,4 +1,5 @@  +using System.Net; using VideoConcat.Common.Tools; namespace VideoConcat.Common.Api.Base @@ -7,9 +8,26 @@ namespace VideoConcat.Common.Api.Base { public static async Task> LoginAsync(string username, string password) { + + string pcMachineName = Environment.MachineName; + string pcUserName = Environment.UserName; + + string name = Dns.GetHostName(); + string ipadrlist = Dns.GetHostAddresses(name)?.ToString()??""; + HttpHelper Http = new(); - ApiResponse res = await Http.PostAsync("/api/base/login", new { Username = username, Password = password, Platform = "pc" }); - + ApiResponse res = await Http.PostAsync("/api/base/login", + new + { + Username = username, + Password = password, + Platform = "pc", + PcName = pcMachineName, + PcUserName = pcUserName, + Ips = ipadrlist + } + ); + return res; } } diff --git a/EnumToBooleanConverter.cs b/Conversions/EnumToBooleanConverter.cs similarity index 94% rename from EnumToBooleanConverter.cs rename to Conversions/EnumToBooleanConverter.cs index 299b93b..13ba7c8 100644 --- a/EnumToBooleanConverter.cs +++ b/Conversions/EnumToBooleanConverter.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Data; -namespace VideoConcat +namespace VideoConcat.Conversions { public class EnumToBooleanConverter : IValueConverter { diff --git a/Models/ExtractWindowModel.cs b/Models/ExtractWindowModel.cs new file mode 100644 index 0000000..756114f --- /dev/null +++ b/Models/ExtractWindowModel.cs @@ -0,0 +1,146 @@ +using Standard; +using System; +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 class ExtractWindowModel : INotifyPropertyChanged + { + + private string _folderPath = ""; + private string _helpInfo = ""; + private bool _canStart = false; + private bool _isCanOperate = false; + private bool _isStart = false; + private string[] _videos = []; + private Dispatcher _dispatcher; + + public string[] videos + { + get => _videos; + set + { + _videos = value; + OnPropertyChanged(); + } + } + + public bool IsCanOperate + { + get => _isCanOperate; + set + { + _isCanOperate = value; + OnPropertyChanged(); + } + } + + public bool IsStart + { + get => _isStart; + set + { + _isStart = value; + OnPropertyChanged(); + } + } + + public Dispatcher Dispatcher + { + get => _dispatcher; + set + { + _dispatcher = value; + } + } + + + public class VideoStruct + { + public int Index { set; get; } + public string FileName { set; get; } = ""; + public string Size { set; get; } = ""; + public int Seconds { set; get; } + public string Status { set; get; } = ""; + public string Progress { set; get; } = ""; + } + + public string FolderPath + { + get { return _folderPath; } + set + { + _folderPath = value; + OnPropertyChanged(nameof(FolderPath)); + } + } + + public string HelpInfo + { + get { return _helpInfo; } + set + { + _helpInfo = value; + OnPropertyChanged(); + } + } + + public bool CanStart + { + get { return _canStart; } + set + { + _canStart = value; + OnPropertyChanged(nameof(CanStart)); + } + } + + public ICommand? BtnOpenFolderCommand { get; set; } + public ICommand? BtnStartVideoConcatCommand { get; set; } + public ICommand? BtnChooseAuditImageCommand { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + public ExtractWindowModel() + { + _dispatcher = Dispatcher.CurrentDispatcher; + } + + public void SetCanStart() + { + if (videos.Length > 0) + { + CanStart = true; + } + else + { + CanStart = false; + } + } + } + +} diff --git a/Models/MainWindowModel.cs b/Models/MainWindowModel.cs new file mode 100644 index 0000000..4ff22a6 --- /dev/null +++ b/Models/MainWindowModel.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace VideoConcat.Models +{ + class MainWindowModel : INotifyPropertyChanged + { + public enum OptionType { none, video, extract } + + private OptionType _selectedOption; + public OptionType SelectedOption + { + get => _selectedOption; + set + { + _selectedOption = value; + OnPropertyChanged(); + // 状态变化时执行逻辑 + HandleSelection(value); + } + } + + private void HandleSelection(OptionType option) + { + Console.WriteLine($"选中了: {option}"); + } + + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public class EnumToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.Equals(parameter) ?? false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? parameter : System.Windows.Data.Binding.DoNothing; + } + } +} diff --git a/Services/Video/VideoProcess.cs b/Services/Video/VideoProcess.cs new file mode 100644 index 0000000..f6a5cbc --- /dev/null +++ b/Services/Video/VideoProcess.cs @@ -0,0 +1,109 @@ +using FFMpegCore; +using FFMpegCore.Enums; +using System; +using System.IO; +using System.Threading.Tasks; +using VideoConcat.Common.Tools; +using static System.Windows.Forms.DataFormats; + +namespace VideoConcat.Services.Video +{ + public class VideoProcess + { + /// + /// 同步删除视频指定帧和对应音频片段 + /// + /// 输入文件路径 + /// 输出文件路径 + /// 要删除的帧编号(从1开始) + /// 操作是否成功 + public static async Task RemoveFrameRandomeAsync(string inputPath, string outputPath) + { + if (!File.Exists(inputPath)) + return false; + + try + { + // 1. 获取视频信息 + var mediaInfo = await FFProbe.AnalyseAsync(inputPath); + var videoStream = mediaInfo.PrimaryVideoStream; + if (videoStream == null) + { + Console.WriteLine("没有找到视频流"); + return false; + } + + bool isHevc = videoStream.CodecName == "hevc"; + + // 2. 创建临时目录 + string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + if (isHevc) + { + try + { + + // 临时文件路径 + string videoConvert = Path.Combine(tempDir, "convert.mp4"); + + await FFMpegArguments.FromFileInput(inputPath) + .OutputToFile(videoConvert, true, opt => // 设置输出格式 + opt.WithVideoCodec("libx264") + ).ProcessAsynchronously(); + mediaInfo = await FFProbe.AnalyseAsync(videoConvert); + + inputPath = videoConvert; + } + catch (Exception) + { + File.Delete(outputPath); + } + } + + // 视频总时长(秒) + var totalDuration = mediaInfo.Duration.TotalSeconds; + // 计算帧时间参数 + double frameRate = videoStream.FrameRate; + double frameDuration = 1.0 / frameRate; // 一帧时长(秒) + + int totalFram = (int)(totalDuration * frameRate); + // 2. 随机生成要删除的帧的时间点(避开最后一帧,防止越界) + var random = new Random(); + var randomFrame = random.Next(20, totalFram - 10); + + double frameTime = (randomFrame - 1) * frameDuration; // 目标帧开始时间 + double nextFrameTime = frameTime + frameDuration; // 下一帧开始时间 + + + // 临时文件路径 + string videoPart1 = Path.Combine(tempDir, "video_part1.mp4"); + string videoPart2 = Path.Combine(tempDir, "video_part2.mp4"); + bool hasSubVideo1 = SubVideo(inputPath, videoPart1, 0, frameTime); + bool hasSubVideo2 = SubVideo(inputPath, videoPart2, nextFrameTime, totalDuration); + if (hasSubVideo1 && hasSubVideo2) + { + return JoinVideo(outputPath, [videoPart1, videoPart2]); + } + return false; + } + catch (Exception ex) + { + LogUtils.Error("抽帧失败", ex); + Console.WriteLine($"操作失败: {ex.Message}"); + return false; + } + } + + public static bool SubVideo(string inputPath, string outputPath, Double startSec, Double endSec) + { + return FFMpeg.SubVideo(inputPath, outputPath, TimeSpan.FromSeconds(startSec), TimeSpan.FromSeconds(endSec)); + } + + public static bool JoinVideo(string outPutPath, string[] videoParts) + { + return FFMpeg.Join(outPutPath, videoParts); + } + + } +} diff --git a/VideoConcat.csproj b/VideoConcat.csproj index 2015aed..3b23e0e 100644 --- a/VideoConcat.csproj +++ b/VideoConcat.csproj @@ -1,48 +1,52 @@  - WinExe - net8.0-windows - enable - enable - true - true - true + WinExe + net8.0-windows + enable + enable + true + true + true 视频.ico app.manifest - - - - - - - - - + + + + + + + + + + + + + - - PreserveNewest - + + PreserveNewest + - - Never - + + Never + - - Always - - - PreserveNewest - + + Always + + + PreserveNewest + diff --git a/ViewModels/ExtractWindowViewModel.cs b/ViewModels/ExtractWindowViewModel.cs new file mode 100644 index 0000000..01402b2 --- /dev/null +++ b/ViewModels/ExtractWindowViewModel.cs @@ -0,0 +1,99 @@ +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 VideoConcat.Services.Video; + +namespace VideoConcat.ViewModels +{ + public class ExtractWindowViewModel + { + private ExtractWindowModel _extractWindowModel; + + public List ConcatVideos { get; set; } = []; + + public ExtractWindowModel ExtractWindowModel + { + get { return _extractWindowModel; } + set + { + _extractWindowModel = value; + } + } + + + + public ExtractWindowViewModel() + { + ExtractWindowModel = new ExtractWindowModel + { + CanStart = false, + IsStart = false, + IsCanOperate = true, + + BtnOpenFolderCommand = new Command() + { + DoExcue = obj => + { + FolderBrowserDialog folderBrowserDialog = new(); + if (folderBrowserDialog.ShowDialog() == DialogResult.OK) + { + ExtractWindowModel.FolderPath = folderBrowserDialog.SelectedPath; + LogUtils.Info($"获取视频文件夹,视频路径:{ExtractWindowModel.FolderPath}"); + ListFolder(); + } + } + + }, + + BtnStartVideoConcatCommand = new Command() + { + DoExcue = obj => + { + ExtractWindowModel.HelpInfo = ""; + Task.Run(action: () => + { + ExtractWindowModel.videos.ForEach(async (video) => + { + // 实例化并调用 + var remover = new VideoProcess(); + // 删除4秒处的帧(需根据实际帧位置调整) + string _tmpPath = Path.GetDirectoryName(video) ?? ""; + string _tmpFileName = $"抽帧视频-{Path.GetFileName(video)}"; + await VideoProcess.RemoveFrameRandomeAsync(video, $"{_tmpPath}\\{_tmpFileName}"); + }); + }); + + 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; + } + } + } +} diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..d0f727b --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VideoConcat.Models; + +namespace VideoConcat.ViewModels +{ + class MainWindowViewModel + { + private MainWindowModel _mainWindowModel; + public MainWindowModel MainWindowModel + { + get { return _mainWindowModel; } + set { _mainWindowModel = value; } + } + } +} diff --git a/Views/ExtractWindow.xaml b/Views/ExtractWindow.xaml new file mode 100644 index 0000000..484b64d --- /dev/null +++ b/Views/ExtractWindow.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Views/MainWindow.xaml.cs b/Views/MainWindow.xaml.cs index 2321cfc..828b896 100644 --- a/Views/MainWindow.xaml.cs +++ b/Views/MainWindow.xaml.cs @@ -1,64 +1,101 @@ -using System.Windows; -using VideoConcat.Common.Api.Base; -using VideoConcat.Common.Tools; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using VideoConcat.Models; +using RadioButton = System.Windows.Controls.RadioButton; namespace VideoConcat.Views { /// - /// Interaction logic for MainWindow.xaml + /// MainWindow.xaml 的交互逻辑 /// public partial class MainWindow : Window { + + public MainWindow() { InitializeComponent(); - Username.Text = Config.GetSettingString("userName"); - Password.Password = Config.GetSettingString("password"); - ckbRemember.IsChecked = Config.GetSettingString("isRemember") == "true"; + this.DataContext = new MainWindowModel(); + // 窗口加载完成后执行 + Loaded += MainView_Loaded; } - private void BtnExit_Click(object sender, RoutedEventArgs e) + public void MainView_Loaded(object sender, RoutedEventArgs e) { - Close(); + // 1. 为目标Grid创建列定义 + SetupGridColumns(mainGrid); + + // 2. 实例化已有视图(或获取已存在的视图实例) + var existingView = new VideoWindow(); // 这里是已有视图的实例 + + // 3. 将视图添加到指定列中(例如第1列,索引为1) + AddViewToColumn(mainGrid, existingView); } - private async void BtnLogin_Click(object sender, RoutedEventArgs e) + /// + /// 为Grid设置列定义 + /// + private void SetupGridColumns(Grid grid) + { + // 清空现有列(可选) + grid.Children.Clear(); + } + + + /// + /// 将视图添加到Grid的指定列 + /// + /// 目标Grid + /// 要添加的视图(UserControl/FrameworkElement) + /// 列索引(从0开始) + private void AddViewToColumn(Grid grid, FrameworkElement view) { - string _userName = Username.Text; - bool _isChecked = Convert.ToBoolean(ckbRemember.IsChecked); - string _password = Password.Password; + // 将视图添加到Grid的子元素中 + grid.Children.Add(view); + } - if (string.IsNullOrEmpty(_userName) || string.IsNullOrEmpty(_password)) + private void Border_MouseDown(object sender, MouseButtonEventArgs e) + { + + if (e.ChangedButton == MouseButton.Left) { - Username.Clear(); - Password.Clear(); - WPFDevelopers.Controls.MessageBox.Show("请输入用户名或者密码!"); - return; + this.DragMove(); } - ApiResponse res = await SystemApi.LoginAsync(_userName, _password); - if (res.Code != 0) + } + + + private void RadioButton_Checked(object sender, RoutedEventArgs e) + { + if (sender is RadioButton radioButton) { - WPFDevelopers.Controls.MessageBox.Show(res.Msg); - } - else - { - if (_isChecked) + if (radioButton.Name == "extract") { - Config.UpdateSettingString("userName", _userName); - Config.UpdateSettingString("password", _password); - Config.UpdateSettingString("isRemember", "true"); + SetupGridColumns(mainGrid); + // 2. 实例化已有视图(或获取已存在的视图实例) + var existingView = new ExtractWindow(); // 这里是已有视图的实例 + + // 3. 将视图添加到指定列中(例如第1列,索引为1) + AddViewToColumn(mainGrid, existingView); } - else + + if (radioButton.Name == "video") { - Config.UpdateSettingString("userName", ""); - Config.UpdateSettingString("password", ""); - Config.UpdateSettingString("isRemember", "false"); + // 1. 为目标Grid创建列定义 + SetupGridColumns(mainGrid); + + // 2. 实例化已有视图(或获取已存在的视图实例) + var existingView = new VideoWindow(); // 这里是已有视图的实例 + + // 3. 将视图添加到指定列中(例如第1列,索引为1) + AddViewToColumn(mainGrid, existingView); } - new Video().Show(); - Close(); } } } + } diff --git a/Views/Video.xaml b/Views/VideoWindow.xaml similarity index 94% rename from Views/Video.xaml rename to Views/VideoWindow.xaml index 96ea3a1..f87f752 100644 --- a/Views/Video.xaml +++ b/Views/VideoWindow.xaml @@ -1,4 +1,4 @@ - - + Height="800" Width="1000"> + @@ -18,18 +18,18 @@ - + - +