# VideoConcat 架构说明文档 ## 架构概述 VideoConcat 采用经典的 MVVM(Model-View-ViewModel)架构模式,结合服务层和工具层,实现清晰的职责分离。 ## 架构层次 ``` ┌─────────────────────────────────────┐ │ View Layer │ (Views/) │ (XAML + Code-Behind) │ └──────────────┬──────────────────────┘ │ Data Binding ┌──────────────▼──────────────────────┐ │ ViewModel Layer │ (ViewModels/) │ (业务逻辑 + 命令处理) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ Model Layer │ (Models/) │ (数据模型 + 业务状态) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ Service Layer │ (Services/) │ (核心业务逻辑处理) │ └──────────────┬──────────────────────┘ │ ┌──────────────▼──────────────────────┐ │ Tools Layer │ (Common/Tools/) │ (工具类 + 辅助功能) │ └─────────────────────────────────────┘ ``` ## 各层详细说明 ### 1. View Layer(视图层) **位置**: `Views/` **职责**: - 定义用户界面(XAML) - 处理 UI 交互事件 - 数据绑定到 ViewModel **主要文件**: #### MainWindow.xaml / MainWindow.xaml.cs - 主窗口,包含导航功能 - 通过 RadioButton 切换不同功能视图 - 动态加载 VideoWindow 或 ExtractWindow **关键代码**: ```csharp private void RadioButton_Checked(object sender, RoutedEventArgs e) { if (radioButton.Name == "extract") { var existingView = new ExtractWindow(); AddViewToColumn(mainGrid, existingView); } if (radioButton.Name == "video") { var existingView = new VideoWindow(); AddViewToColumn(mainGrid, existingView); } } ``` #### VideoWindow.xaml / VideoWindow.xaml.cs - 视频拼接功能界面 - 显示文件夹列表、视频列表、拼接结果 #### ExtractWindow.xaml / ExtractWindow.xaml.cs - 视频抽帧和元数据修改功能界面 - 显示视频文件列表和处理结果 #### LoginWindow.xaml / LoginWindow.xaml.cs - 用户登录界面 ### 2. ViewModel Layer(视图模型层) **位置**: `ViewModels/` **职责**: - 处理用户交互逻辑 - 协调 Model 和 Service - 实现命令模式 - 管理 UI 状态 **主要文件**: #### VideoViewModel.cs - 视频拼接功能的视图模型 - 实现文件夹选择、视频组合生成、拼接执行等逻辑 **核心流程**: 1. 用户选择文件夹 → `ListFolder()` 扫描视频文件 2. 生成视频组合 → `GenerateCombinations()` 或顺序组合 3. 转换视频格式 → 并发转换为 TS 格式 4. 拼接视频 → 使用 FFmpeg concat 5. 添加水印(可选)→ 图片叠加 **关键特性**: - 使用 `SemaphoreSlim` 控制并发数量(10个线程) - 使用 `Task.Run` 进行异步处理 - 通过 `Dispatcher.Invoke` 更新 UI 线程 #### ExtractWindowViewModel.cs - 视频抽帧和元数据修改的视图模型 - 实现批量处理逻辑 #### MainWindowViewModel.cs - 主窗口视图模型(当前较简单) ### 3. Model Layer(模型层) **位置**: `Models/` **职责**: - 定义数据模型 - 实现 INotifyPropertyChanged 接口 - 管理业务状态 **主要文件**: #### VideoModel.cs - 视频拼接功能的数据模型 - 包含文件夹信息、视频列表、拼接结果等 **关键属性**: - `FolderInfos`: 文件夹信息集合 - `ConcatVideos`: 拼接结果集合 - `IsJoinType1Selected` / `IsJoinType2Selected`: 拼接模式 - `CanStart`: 是否可以开始(基于数量和状态) **状态管理**: ```csharp public void SetCanStart() { if (Num > 0 && Num <= MaxNum && IsStart == false) { CanStart = true; } else { CanStart = false; } IsCanOperate = !IsStart; } ``` #### ExtractWindowModel.cs - 抽帧功能的数据模型 - 包含视频文件列表和处理状态 #### MainWindowModel.cs - 主窗口数据模型 ### 4. Service Layer(服务层) **位置**: `Services/` **职责**: - 实现核心业务逻辑 - 封装视频处理操作 - 与 FFmpeg 交互 **主要文件**: #### VideoService.cs - 视频服务,提供视频转换和拼接功能 **核心方法**: 1. **ConvertVideos**: 视频格式转换 ```csharp public static string ConvertVideos(string videoPath) { // 1. 分析视频信息 var video = FFProbe.Analyse(videoPath); // 2. 生成 MD5 缓存文件名 string _tempMd5Name = GetLargeFileMD5(videoPath); var destinationPath = Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); // 3. 检查缓存 if (File.Exists(destinationPath)) return destinationPath; // 4. 转换视频 FFMpeg.Convert(videoPath, destinationPath, VideoType.Ts); return destinationPath; } ``` 2. **JoinVideos**: 视频拼接 - 使用 `FFMpegArguments.FromConcatInput()` 进行拼接 - 支持音频重新编码 - 可选添加图片水印 3. **GetLargeFileMD5**: 大文件 MD5 计算 - 使用流式读取,避免内存溢出 - 8KB 缓冲区优化 #### VideoProcess.cs - 视频处理服务,提供抽帧、裁剪、元数据修改等功能 **核心方法**: 1. **RemoveFrameRandomeAsync**: 异步删除随机帧 - 处理 HEVC 编码(自动转换) - 分割视频并重新合并 2. **ModifyByMetadata**: 修改元数据 - 添加唯一注释改变 MD5 - 使用流复制,不重新编码 3. **SubVideo / SubAudio**: 视频/音频裁剪 #### BaseService.cs - 基础服务类(当前为空,可扩展) ### 5. Tools Layer(工具层) **位置**: `Common/Tools/` **职责**: - 提供通用工具方法 - 封装第三方库调用 - 辅助功能实现 **主要文件**: #### VideoCombine.cs - 视频组合工具 - 提供组合生成和格式转换功能 **核心方法**: ```csharp public static void GenerateCombinations( List> videoLists, int index, List currentCombination, List> result) { // 递归生成所有可能的视频组合(笛卡尔积) if (index == videoLists.Count) { result.Add([.. currentCombination]); return; } foreach (string video in videoLists[index]) { currentCombination.Add(video); GenerateCombinations(videoLists, index + 1, currentCombination, result); currentCombination.RemoveAt(currentCombination.Count - 1); } } ``` #### LogUtils.cs - 日志工具类 - 基于 log4net 封装 - 提供 Info、Debug、Error、Warn、Fatal 等方法 #### HttpUtils.cs - HTTP 请求工具 - 封装 HttpClient - 提供 GetAsync 和 PostAsync 方法 #### Config.cs - 配置管理工具 - 读取和更新 App.config 配置 ### 6. API Layer(API层) **位置**: `Common/Api/` **职责**: - 定义 API 接口 - 封装 HTTP 请求 **主要文件**: #### SystemApi.cs - 系统 API - 提供登录接口 ## 数据流 ### 视频拼接流程 ``` 用户操作 ↓ VideoWindow (View) ↓ (命令绑定) VideoViewModel ↓ 1. ListFolder() → 扫描文件夹 ↓ 2. GenerateCombinations() → 生成组合 ↓ 3. ConvertVideos() → 转换格式 (并发) ↓ 4. JoinVideos() → 拼接视频 (并发) ↓ 5. 更新 VideoModel.ConcatVideos ↓ UI 自动更新 (数据绑定) ``` ### 视频抽帧流程 ``` 用户操作 ↓ ExtractWindow (View) ↓ (命令绑定) ExtractWindowViewModel ↓ 1. ListFolder() → 扫描视频文件 ↓ 2. RemoveFrameRandomeAsync() → 抽帧处理 (并发) ↓ 3. VideoProcess.RemoveFrameRandomeAsync() ↓ 4. 输出到 out 文件夹 ↓ UI 更新状态 ``` ## 设计模式 ### 1. MVVM 模式 - **Model**: 数据模型和业务状态 - **View**: XAML 界面 - **ViewModel**: 连接 View 和 Model ### 2. 命令模式 - 自定义 `Command` 类实现 `ICommand` 接口 - 将 UI 操作封装为命令 ### 3. 观察者模式 - 使用 `INotifyPropertyChanged` 实现属性变更通知 - 使用 `ObservableCollection` 实现集合变更通知 ### 4. 单例模式 - `LogUtils` 使用静态方法,类似单例 ## 并发处理 ### SemaphoreSlim 控制并发 ```csharp SemaphoreSlim semaphore = new(10); // 限制10个并发 foreach (var item in items) { await semaphore.WaitAsync(); var task = Task.Run(() => { try { // 处理逻辑 } finally { semaphore.Release(); } }); tasks.Add(task); } await Task.WhenAll(tasks); ``` ### 异步处理 - 使用 `async/await` 进行异步操作 - 使用 `Task.Run` 在后台线程执行耗时操作 - 使用 `Dispatcher.Invoke` 更新 UI 线程 ## 缓存机制 ### 视频转换缓存 基于 MD5 的文件名缓存: ```csharp string _tempMd5Name = GetLargeFileMD5(videoPath); var destinationPath = Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}"); if (File.Exists(destinationPath)) { return destinationPath; // 直接返回缓存文件 } ``` **优势**: - 避免重复转换相同文件 - 提高处理速度 - 减少 CPU 和磁盘 I/O ## 错误处理 ### 异常捕获 ```csharp try { // 主要处理逻辑 FFMpeg.Convert(videoPath, destinationPath, VideoType.Ts); } catch (Exception ex) { // 备用方案 LogUtils.Info("视频转换失败!尝试另外一种转换"); try { FFMpegArguments .FromFileInput(videoPath) .OutputToFile(destinationPath, true, o => o .WithVideoCodec("libx264") // ... 其他参数 ) .ProcessSynchronously(true); } catch (Exception e) { LogUtils.Error($"{videoPath} 转换失败", ex); LogUtils.Error($"{videoPath} 转换再次失败", e); } } ``` ### 日志记录 - 所有错误都记录到日志文件 - 使用 log4net 进行日志管理 - 日志文件按日期命名 ## 资源管理 ### 临时文件清理 ```csharp public static void Cleanup(List pathList) { foreach (var path in pathList) { if (File.Exists(path)) { File.Delete(path); } } } ``` ### 文件流管理 使用 `using` 语句确保资源释放: ```csharp using var md5 = MD5.Create(); using var stream = File.OpenRead(filePath); // ... 处理逻辑 ``` ## 扩展性设计 ### 1. 服务层扩展 - 可以添加新的 Service 类 - 继承 BaseService(如需要) ### 2. 工具类扩展 - 在 `Common/Tools/` 中添加新工具类 - 保持工具类的静态方法设计 ### 3. API 扩展 - 在 `Common/Api/` 中添加新的 API 类 - 使用 HttpUtils 进行请求 ### 4. 功能扩展 - 添加新的 View 和 ViewModel - 在主窗口中添加导航入口 ## 性能优化建议 1. **并发控制**: 已使用 SemaphoreSlim 限制并发 2. **缓存机制**: 已实现基于 MD5 的转换缓存 3. **异步处理**: 已使用 async/await 避免阻塞 4. **流式处理**: 大文件使用流式读取 **可进一步优化**: - 添加进度报告机制 - 优化大文件 MD5 计算(可考虑分块并行) - 添加视频预览功能(减少不必要的处理) - 实现断点续传(大文件处理) ## 测试建议 1. **单元测试**: 为 Service 和 Tools 层添加单元测试 2. **集成测试**: 测试完整的视频处理流程 3. **性能测试**: 测试并发处理和大量文件处理 4. **异常测试**: 测试各种异常情况(文件不存在、格式不支持等)