VideoConcat/docs/架构说明.md
2026-01-01 15:39:54 +08:00

510 lines
12 KiB
Markdown
Raw Permalink 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.

# VideoConcat 架构说明文档
## 架构概述
VideoConcat 采用经典的 MVVMModel-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<List<string>> videoLists,
int index,
List<string> currentCombination,
List<List<string>> 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 LayerAPI层
**位置**: `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<string> 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. **异常测试**: 测试各种异常情况(文件不存在、格式不支持等)