# FFMpegCore视频导出方案

**文档日期：** 2025年9月29日
**项目：** NavisworksTransport插件
**功能：** 动画导出为导航视频

## 技术架构概述

### 核心组件设计
```
VideoExportManager
├── VideoExportSettings（导出参数配置）
├── FrameCaptureService（动画帧捕获服务）
├── VideoEncodingService（视频编码服务）
└── ProgressReportingService（进度报告服务）
```

### 集成架构
- **帧捕获点：** PathAnimationManager.OnTimerTick方法中，在RequestDelayedRedraw()后调用Document.GenerateImage()
- **UI集成：** AnimationControlView.xaml添加"导出为视频"按钮
- **数据流：** Bitmap序列 → FFMpegCore → MP4/AVI视频文件

## FFMpegCore技术细节

### 库信息
- **NuGet包：** FFMpegCore 5.2.0
- **兼容性：** .NET Framework 4.7（项目当前版本）
- **目标框架：** .NET Standard 2.0

### 依赖项
```xml
<package id="FFMpegCore" version="5.2.0" targetFramework="net47" />
<package id="Instances" version="3.0.1" targetFramework="net47" />
<package id="System.Text.Json" version="9.0.2" targetFramework="net47" />
```

## 核心代码实现

### 1. 视频导出管理器
```csharp
using FFMpegCore;
using FFMpegCore.Pipes;
using FFMpegCore.Extensions.System.Drawing.Common;
using System.Drawing;
using System.Collections.Generic;

public class VideoExportManager
{
    public class VideoExportSettings
    {
        public string OutputPath { get; set; }
        public int Width { get; set; } = 1920;
        public int Height { get; set; } = 1080;
        public int FrameRate { get; set; } = 30;
        public VideoFormat Format { get; set; } = VideoFormat.Mp4;
        public VideoCodec Codec { get; set; } = VideoCodec.LibX264;
        public ImageGenerationStyle RenderStyle { get; set; } = ImageGenerationStyle.ScenePlusOverlay;
    }

    private List<Bitmap> _capturedFrames;
    private bool _isCapturing = false;
    private VideoExportSettings _currentSettings;

    public void StartVideoCapture(VideoExportSettings settings)
    {
        _currentSettings = settings;
        _capturedFrames = new List<Bitmap>();
        _isCapturing = true;

        // 配置FFmpeg二进制路径
        GlobalFFOptions.Configure(new FFOptions
        {
            BinaryFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg"),
            TemporaryFilesFolder = Path.GetTempPath()
        });

        LogManager.Info($"开始视频捕获：{settings.Width}x{settings.Height}@{settings.FrameRate}fps");
    }

    public void CaptureCurrentFrame()
    {
        if (!_isCapturing) return;

        try
        {
            // 使用Navisworks API捕获当前帧
            using (var bitmap = Document.GenerateImage(
                _currentSettings.RenderStyle,
                _currentSettings.Width,
                _currentSettings.Height,
                enableSectioning: false))
            {
                // 复制Bitmap避免内存问题
                var frameCopy = new Bitmap(bitmap);
                _capturedFrames.Add(frameCopy);

                LogManager.Info($"已捕获第{_capturedFrames.Count}帧");
            }
        }
        catch (Exception ex)
        {
            LogManager.Error($"帧捕获失败: {ex.Message}", ex);
        }
    }

    public async Task FinishVideoCapture()
    {
        if (!_isCapturing || _capturedFrames.Count == 0)
        {
            LogManager.Warning("没有捕获到有效帧，取消视频生成");
            return;
        }

        _isCapturing = false;

        try
        {
            LogManager.Info($"开始生成视频，共{_capturedFrames.Count}帧");

            // 创建视频帧源
            var videoFramesSource = new RawVideoPipeSource(CreateFrameEnumerable())
            {
                FrameRate = _currentSettings.FrameRate
            };

            // 生成视频文件
            await FFMpegArguments
                .FromPipeInput(videoFramesSource)
                .OutputToFile(_currentSettings.OutputPath, true, options => options
                    .WithVideoCodec(_currentSettings.Codec)
                    .WithFramerate(_currentSettings.FrameRate)
                    .WithVideoFilters(filterOptions => filterOptions
                        .Scale(_currentSettings.Width, _currentSettings.Height)))
                .ProcessAsynchronously();

            LogManager.Info($"视频生成完成：{_currentSettings.OutputPath}");
        }
        catch (Exception ex)
        {
            LogManager.Error($"视频生成失败: {ex.Message}", ex);
            throw;
        }
        finally
        {
            // 清理内存
            foreach (var frame in _capturedFrames)
            {
                frame?.Dispose();
            }
            _capturedFrames.Clear();
        }
    }

    private IEnumerable<IVideoFrame> CreateFrameEnumerable()
    {
        foreach (var bitmap in _capturedFrames)
        {
            yield return new BitmapVideoFrameWrapper(bitmap);
        }
    }
}
```

### 2. PathAnimationManager集成
```csharp
// 在PathAnimationManager类中添加视频导出支持
public class PathAnimationManager
{
    private VideoExportManager _videoExportManager;

    private void OnTimerTick(object sender, EventArgs e)
    {
        try
        {
            // 处理动画帧
            ProcessAnimationFrame();

            // 关键改进：显式请求重绘，解决卡顿
            NavisApplication.ActiveDocument.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.All);

            // 新增：视频导出时捕获帧
            if (_videoExportManager?.IsCapturing == true)
            {
                _videoExportManager.CaptureCurrentFrame();
            }
        }
        catch (Exception ex)
        {
            LogManager.Error($"[DispatcherTimer模式] 动画更新异常: {ex.Message}");
            ShutdownAnimation();
        }
    }

    public void StartVideoExport(VideoExportManager.VideoExportSettings settings)
    {
        _videoExportManager = new VideoExportManager();
        _videoExportManager.StartVideoCapture(settings);
    }

    public async Task FinishVideoExport()
    {
        if (_videoExportManager != null)
        {
            await _videoExportManager.FinishVideoCapture();
            _videoExportManager = null;
        }
    }
}
```

### 3. UI集成代码
```csharp
// AnimationControlView.xaml.cs中添加
private async void OnExportVideoButtonClick(object sender, RoutedEventArgs e)
{
    try
    {
        LogManager.Info("用户点击导出视频按钮");

        // 检查前置条件
        if (ViewModel?.CurrentPathRoute == null || ViewModel.CurrentPathRoute.Points.Count == 0)
        {
            MessageBox.Show("请先选择有效的路径", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }

        if (ViewModel?.SelectedAnimatedObject == null)
        {
            MessageBox.Show("请先选择移动物体", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
            return;
        }

        // 显示视频导出设置对话框
        var exportDialog = new VideoExportSettingsDialog();

        var parentWindow = Window.GetWindow(this);
        if (parentWindow != null)
        {
            exportDialog.Owner = parentWindow;
        }

        if (exportDialog.ShowDialog() == true && exportDialog.IsConfirmed)
        {
            var settings = new VideoExportManager.VideoExportSettings
            {
                OutputPath = exportDialog.OutputPath,
                Width = exportDialog.VideoWidth,
                Height = exportDialog.VideoHeight,
                FrameRate = exportDialog.FrameRate,
                Format = exportDialog.VideoFormat,
                Codec = exportDialog.VideoCodec
            };

            // 开始视频导出
            await ViewModel.StartVideoExportAsync(settings);
        }
    }
    catch (Exception ex)
    {
        LogManager.Error($"导出视频失败: {ex.Message}", ex);
        MessageBox.Show($"导出视频失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
    }
}
```

## 部署要求

### 文件分发清单
```
NavisworksTransport插件目录/
├── TransportPlugin.dll（主插件）
├── FFMpegCore.dll（NuGet自动处理）
├── Instances.dll（依赖项）
├── System.Text.Json.dll（依赖项）
└── ffmpeg/
    ├── ffmpeg.exe（Windows x64版本，~50MB）
    └── ffprobe.exe（探测工具，~20MB）
```

### FFmpeg二进制文件获取
1. **官方下载：** https://ffmpeg.org/download.html#build-windows
2. **推荐版本：** FFmpeg 4.4.0+ Windows x64 static builds
3. **文件来源：** Gyan.dev或BtbN builds（预编译静态版本）

### 配置代码
```csharp
// 在应用程序启动时配置FFmpeg路径
public static void ConfigureFFmpeg()
{
    var ffmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg");

    if (!Directory.Exists(ffmpegPath))
    {
        throw new DirectoryNotFoundException($"FFmpeg二进制目录不存在: {ffmpegPath}");
    }

    GlobalFFOptions.Configure(new FFOptions
    {
        BinaryFolder = ffmpegPath,
        TemporaryFilesFolder = Path.GetTempPath()
    });

    LogManager.Info($"FFmpeg已配置: {ffmpegPath}");
}
```

## 支持的视频格式

### 输出格式
- **MP4**（推荐）：H.264编码，兼容性最好
- **AVI**：MPEG-4编码，文件较大但兼容性好
- **WebM**：VP9编码，适合web播放
- **MOV**：QuickTime格式，适合Mac用户

### 编码参数建议
```csharp
// 高质量设置
.WithVideoCodec(VideoCodec.LibX264)
.WithConstantRateFactor(23)  // CRF值，18-28范围，数值越小质量越高
.WithFramerate(30)

// 压缩优化设置
.WithVideoCodec(VideoCodec.LibX264)
.WithVideoBitrate(2000)  // 2Mbps码率
.WithAudioCodec(AudioCodec.Aac)  // 如果需要音频
```

## 性能优化策略

### 内存管理
```csharp
// 避免内存泄漏的Bitmap处理
public void CaptureCurrentFrame()
{
    using (var originalBitmap = Document.GenerateImage(...))
    {
        // 创建副本避免引用原始对象
        var frameCopy = new Bitmap(originalBitmap.Width, originalBitmap.Height, originalBitmap.PixelFormat);
        using (var graphics = Graphics.FromImage(frameCopy))
        {
            graphics.DrawImage(originalBitmap, 0, 0);
        }
        _capturedFrames.Add(frameCopy);
    }
}
```

### 异步处理
```csharp
// UI不会冻结的异步导出
public async Task ExportVideoAsync(VideoExportSettings settings)
{
    await Task.Run(async () =>
    {
        // 在后台线程执行视频编码
        await _videoExportManager.FinishVideoCapture();
    });

    // 回到UI线程更新状态
    Dispatcher.Invoke(() =>
    {
        UpdateStatus("视频导出完成");
    });
}
```

### 进度报告
```csharp
public class VideoExportManager
{
    public event EventHandler<ProgressEventArgs> ProgressChanged;

    private void ReportProgress(int current, int total, string status)
    {
        var progress = (double)current / total * 100;
        ProgressChanged?.Invoke(this, new ProgressEventArgs(progress, status));
    }
}
```

## 错误处理和诊断

### 常见问题排查
```csharp
public static class VideoExportDiagnostics
{
    public static bool VerifyFFmpegInstallation()
    {
        try
        {
            var ffmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg", "ffmpeg.exe");
            if (!File.Exists(ffmpegPath))
            {
                LogManager.Error($"FFmpeg可执行文件不存在: {ffmpegPath}");
                return false;
            }

            // 测试FFmpeg是否可用
            var processInfo = new ProcessStartInfo(ffmpegPath, "-version")
            {
                CreateNoWindow = true,
                UseShellExecute = false,
                RedirectStandardOutput = true
            };

            using (var process = Process.Start(processInfo))
            {
                process.WaitForExit(5000);
                return process.ExitCode == 0;
            }
        }
        catch (Exception ex)
        {
            LogManager.Error($"FFmpeg验证失败: {ex.Message}", ex);
            return false;
        }
    }
}
```

## 风险评估和缓解

### 技术风险
| 风险等级 | 描述 | 缓解措施 |
|---------|------|----------|
| 低 | FFMpegCore兼容性 | 使用稳定版本5.2.0，社区活跃 |
| 中 | FFmpeg二进制分发 | 提供详细安装指南，支持便携版 |
| 中 | 内存消耗 | 实现流式处理，及时释放Bitmap |
| 低 | 文件大小 | 提供压缩参数配置 |

### 部署风险
- **用户环境差异**：提供自包含的FFmpeg二进制文件
- **权限问题**：确保插件目录有写入权限
- **杀毒软件**：FFmpeg可能被误报，提供白名单说明

## 替代方案对比

| 方案 | 优势 | 劣势 | 推荐度 |
|-----|------|------|--------|
| FFMpegCore | 功能强大，格式丰富，性能好 | 需要分发FFmpeg二进制 | ⭐⭐⭐⭐⭐ |
| AForge.NET | API简单 | 已停止维护，功能有限 | ⭐⭐ |
| 图像序列导出 | 零依赖，部署简单 | 用户需要额外工具合成 | ⭐⭐⭐ |

## 实施时间估算

- **阶段1：基础框架**（1-2天）
- **阶段2：动画集成**（1天）
- **阶段3：视频编码**（1天）
- **阶段4：UI优化**（0.5天）
- **总计：3.5-4.5天**

## 总结

FFMpegCore方案虽然需要分发额外的二进制文件，但提供了最完整和专业的视频导出功能。通过合理的架构设计和错误处理，可以为用户提供高质量的导航视频导出体验。

**推荐实施顺序：**
1. 先实现基础的帧捕获功能
2. 集成FFMpegCore进行视频编码测试
3. 完善UI和用户体验
4. 优化性能和错误处理

这个方案能够满足专业用户对视频质量和格式的要求，是长期来看最具价值的技术选择。