12 KiB
12 KiB
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
关键代码:
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
- 视频拼接功能的视图模型
- 实现文件夹选择、视频组合生成、拼接执行等逻辑
核心流程:
- 用户选择文件夹 →
ListFolder()扫描视频文件 - 生成视频组合 →
GenerateCombinations()或顺序组合 - 转换视频格式 → 并发转换为 TS 格式
- 拼接视频 → 使用 FFmpeg concat
- 添加水印(可选)→ 图片叠加
关键特性:
- 使用
SemaphoreSlim控制并发数量(10个线程) - 使用
Task.Run进行异步处理 - 通过
Dispatcher.Invoke更新 UI 线程
ExtractWindowViewModel.cs
- 视频抽帧和元数据修改的视图模型
- 实现批量处理逻辑
MainWindowViewModel.cs
- 主窗口视图模型(当前较简单)
3. Model Layer(模型层)
位置: Models/
职责:
- 定义数据模型
- 实现 INotifyPropertyChanged 接口
- 管理业务状态
主要文件:
VideoModel.cs
- 视频拼接功能的数据模型
- 包含文件夹信息、视频列表、拼接结果等
关键属性:
FolderInfos: 文件夹信息集合ConcatVideos: 拼接结果集合IsJoinType1Selected/IsJoinType2Selected: 拼接模式CanStart: 是否可以开始(基于数量和状态)
状态管理:
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
- 视频服务,提供视频转换和拼接功能
核心方法:
-
ConvertVideos: 视频格式转换
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; } -
JoinVideos: 视频拼接
- 使用
FFMpegArguments.FromConcatInput()进行拼接 - 支持音频重新编码
- 可选添加图片水印
- 使用
-
GetLargeFileMD5: 大文件 MD5 计算
- 使用流式读取,避免内存溢出
- 8KB 缓冲区优化
VideoProcess.cs
- 视频处理服务,提供抽帧、裁剪、元数据修改等功能
核心方法:
-
RemoveFrameRandomeAsync: 异步删除随机帧
- 处理 HEVC 编码(自动转换)
- 分割视频并重新合并
-
ModifyByMetadata: 修改元数据
- 添加唯一注释改变 MD5
- 使用流复制,不重新编码
-
SubVideo / SubAudio: 视频/音频裁剪
BaseService.cs
- 基础服务类(当前为空,可扩展)
5. Tools Layer(工具层)
位置: Common/Tools/
职责:
- 提供通用工具方法
- 封装第三方库调用
- 辅助功能实现
主要文件:
VideoCombine.cs
- 视频组合工具
- 提供组合生成和格式转换功能
核心方法:
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 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 控制并发
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 的文件名缓存:
string _tempMd5Name = GetLargeFileMD5(videoPath);
var destinationPath = Path.Combine(Path.GetTempPath(), $"{_tempMd5Name}{FileExtension.Ts}");
if (File.Exists(destinationPath))
{
return destinationPath; // 直接返回缓存文件
}
优势:
- 避免重复转换相同文件
- 提高处理速度
- 减少 CPU 和磁盘 I/O
错误处理
异常捕获
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 进行日志管理
- 日志文件按日期命名
资源管理
临时文件清理
public static void Cleanup(List<string> pathList)
{
foreach (var path in pathList)
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
文件流管理
使用 using 语句确保资源释放:
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
- 在主窗口中添加导航入口
性能优化建议
- 并发控制: 已使用 SemaphoreSlim 限制并发
- 缓存机制: 已实现基于 MD5 的转换缓存
- 异步处理: 已使用 async/await 避免阻塞
- 流式处理: 大文件使用流式读取
可进一步优化:
- 添加进度报告机制
- 优化大文件 MD5 计算(可考虑分块并行)
- 添加视频预览功能(减少不必要的处理)
- 实现断点续传(大文件处理)
测试建议
- 单元测试: 为 Service 和 Tools 层添加单元测试
- 集成测试: 测试完整的视频处理流程
- 性能测试: 测试并发处理和大量文件处理
- 异常测试: 测试各种异常情况(文件不存在、格式不支持等)