FFmpeg录屏避坑指南:C#中处理音频捕获异常的3种方法

📅 发布时间:2026/7/6 12:54:14 👁️ 浏览次数:
FFmpeg录屏避坑指南:C#中处理音频捕获异常的3种方法
FFmpeg录屏实战从音频捕获异常到稳定录制的深度调优录屏功能听起来简单不就是把屏幕画面和声音录下来吗但真正动手开发时你会发现这潭水比想象中深得多。特别是当你在C#项目中集成FFmpeg试图捕获系统音频时各种稀奇古怪的异常会接踵而至——设备找不到、音频流中断、录制文件无声每一个问题都足以让开发者抓狂。我经历过无数次深夜调试从最初的“能用就行”到后来的“必须稳定”踩过的坑不计其数。这篇文章就是把这些实战经验浓缩成一套可复用的解决方案专为那些正在与音频捕获异常搏斗的开发者准备。无论你是刚接触FFmpeg的新手还是已经有一定经验但被特定问题困扰的同行这里的内容都能帮你少走弯路快速构建稳定可靠的录屏功能。1. 理解音频捕获的核心FFmpeg的dshow与设备枚举在Windows平台上FFmpeg主要通过dshowDirectShow滤镜来捕获音频设备。这听起来很直接但问题往往就出在“设备”这两个字上。系统里有哪些音频设备FFmpeg能看到哪些virtual-audio-capturer又是什么搞不清这些调试就像在黑暗中摸索。1.1 系统音频设备架构与FFmpeg的视角Windows的音频子系统远比我们想象得复杂。除了物理声卡对应的“扬声器”设备还有各种虚拟设备比如通信应用程序如Discord、Voicemeeter创建的虚拟输出以及系统用于内部混音的“立体声混音”或“环路”设备。FFmpeg通过dshow接口枚举设备时它看到的列表可能与你在系统声音设置里看到的并不完全一致。一个快速检查可用音频输入设备的方法是直接通过FFmpeg命令行ffmpeg -list_devices true -f dshow -i dummy执行这条命令你会看到类似下面的输出具体设备名因系统而异[dshow 000001f4a8f2f040] DirectShow video devices [dshow 000001f4a8f2f040] DirectShow audio devices [dshow 000001f4a8f2f040] 麦克风阵列 (Realtek Audio) [dshow 000001f4a8f2f040] 立体声混音 (Realtek Audio) [dshow 000001f4a8f2f040] virtual-audio-capturer这里的关键点是virtual-audio-capturer并非Windows原生设备。它通常由第三方音频路由软件如VB-Audio Virtual Cable, OBS的音频捕获插件安装。如果你的开发环境或用户电脑上没有安装这类软件这个设备就不会存在直接指定它就会导致FFmpeg报错“找不到指定设备”。注意依赖virtual-audio-capturer意味着你的应用程序绑定了一个特定的第三方组件这会给部署带来不确定性。在商业软件中这通常不是最佳选择。1.2 在C#中动态枚举音频设备与其硬编码一个可能不存在的设备名不如让程序在运行时动态发现可用的音频捕获设备。我们可以封装一个方法调用FFmpeg并解析其设备列表输出。using System.Diagnostics; using System.Text.RegularExpressions; public class AudioDeviceHelper { public static Liststring GetAvailableAudioInputDevices(string ffmpegPath) { var devices new Liststring(); var processStartInfo new ProcessStartInfo(ffmpegPath) { Arguments -list_devices true -f dshow -i dummy, UseShellExecute false, RedirectStandardError true, // 设备列表信息输出到标准错误流 CreateNoWindow true, StandardErrorEncoding System.Text.Encoding.UTF8 }; using (var process new Process { StartInfo processStartInfo }) { process.Start(); string errorOutput process.StandardError.ReadToEnd(); process.WaitForExit(); // 使用正则表达式匹配音频设备行 var pattern \]\s*([^])\s*\(audio\); var matches Regex.Matches(errorOutput, pattern); foreach (Match match in matches) { if (match.Groups.Count 1) { devices.Add(match.Groups[1].Value); } } } return devices; } }这个方法返回一个设备名称的列表。在实际调用录屏功能前你可以先检查这个列表如果它为空就提示用户没有可用的音频捕获设备如果包含virtual-audio-capturer或其他设备就可以根据策略选择使用哪一个。设备选择策略对比策略类型实现方式优点缺点适用场景硬编码指定固定使用virtual-audio-capturer实现简单如果环境一致则稳定兼容性极差部署依赖第三方软件内部工具、可控环境用户选择图形界面列出所有设备供用户选择灵活性最高用户可控增加交互复杂度对非技术用户不友好专业音视频软件智能探测程序按优先级自动选择如先找“立体声混音”再找其他用户体验好无需干预逻辑复杂不同系统设备名差异大大众消费级软件降级方案优先尝试高质量设备失败后尝试基础设备或跳过音频保证核心功能录屏可用音频质量可能不稳定对音频非必需的应用对于大多数录屏工具我推荐采用“智能探测为主用户配置为辅”的策略。即程序默认自动选择最合适的设备同时在设置中提供高级选项让用户手动覆盖。2. 攻克“virtual-audio-capturer”不存在的经典异常“Device not found”或“Cannot find audio device virtual-audio-capturer”——这可能是C# FFmpeg录屏开发中排名第一的错误。直接崩溃显然不行我们需要一套健壮的异常处理机制。2.1 异常捕获与友好提示首先在启动FFmpeg进程时我们必须监听其标准错误输出。FFmpeg的所有状态信息、警告和错误都会输出到这里。在C#中通过ProcessStartInfo.RedirectStandardError true并订阅ErrorDataReceived事件来实现。public class RobustScreenRecorder { private Process _ffmpegProcess; private StringBuilder _errorBuffer new StringBuilder(); private string _selectedAudioDevice string.Empty; public void StartRecording(string outputPath, string ffmpegPath) { // 1. 尝试获取首选音频设备 var availableDevices AudioDeviceHelper.GetAvailableAudioInputDevices(ffmpegPath); _selectedAudioDevice SelectAudioDevice(availableDevices); // 选择逻辑后文详述 // 2. 构建FFmpeg参数 string arguments BuildFFmpegArguments(outputPath, _selectedAudioDevice); var startInfo new ProcessStartInfo(ffmpegPath) { Arguments arguments, UseShellExecute false, RedirectStandardError true, CreateNoWindow true, RedirectStandardInput true, StandardErrorEncoding System.Text.Encoding.UTF8 }; _ffmpegProcess new Process { StartInfo startInfo }; _ffmpegProcess.ErrorDataReceived OnFfmpegErrorOutput; try { _ffmpegProcess.Start(); _ffmpegProcess.BeginErrorReadLine(); // 等待一小段时间检查进程是否因错误立即退出 Thread.Sleep(1000); if (_ffmpegProcess.HasExited) { throw new Exception($FFmpeg进程异常退出退出代码: {_ffmpegProcess.ExitCode}。错误信息可能为: {_errorBuffer.ToString()}); } Console.WriteLine($录制已启动使用的音频设备: {_selectedAudioDevice}); } catch (Exception ex) { // 处理启动失败例如设备权限问题、路径错误等 HandleStartupFailure(ex, _errorBuffer.ToString()); } } private void OnFfmpegErrorOutput(object sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { _errorBuffer.AppendLine(e.Data); // 实时分析错误信息 if (e.Data.Contains(Device not found) || e.Data.Contains(Cannot find audio device)) { // 触发设备回退或重试逻辑 OnAudioDeviceNotFound(); } else if (e.Data.Contains(buffer queue overflow) || e.Data.Contains(real-time buffer)) { // 音频缓冲区问题可能需要调整参数 OnAudioBufferIssue(); } } } }关键点在于OnFfmpegErrorOutput方法中的实时错误分析。一旦检测到设备找不到的错误就立即触发备用方案。2.2 实现设备自动回退与重试机制当首选设备失败时程序不应该直接崩溃而应该尝试备用设备。这里需要一个设备优先级列表。private string SelectAudioDevice(Liststring availableDevices) { // 定义设备优先级列表按需调整 var preferredDevices new Liststring { virtual-audio-capturer, // 第一优先级虚拟捕获器如果安装了相关软件 立体声混音, // 第二优先级系统立体声混音常见中文系统名 Stereo Mix, // 第三优先级系统立体声混音英文系统名 What U Hear, // 第四优先级某些声卡驱动提供的设备 麦克风, // 第五优先级物理麦克风至少能录点声音 }; // 尝试在可用设备中找到优先级最高的 foreach (var preferred in preferredDevices) { // 模糊匹配设备名可能包含额外信息如“立体声混音 (Realtek High Definition Audio)” var matchedDevice availableDevices.FirstOrDefault(d d.Contains(preferred)); if (matchedDevice ! null) { return matchedDevice; // 返回完整的设备名 } } // 如果没有匹配到任何优先设备但有可用设备则返回第一个 if (availableDevices.Any()) { return availableDevices.First(); } // 完全没有音频设备 return string.Empty; // 表示无音频录制 }在构建FFmpeg参数时需要处理无音频设备的情况private string BuildFFmpegArguments(string outputPath, string audioDevice) { string videoArgs -f gdigrab -framerate 30 -i desktop -pix_fmt yuv420p; string audioArgs string.Empty; if (!string.IsNullOrEmpty(audioDevice)) { // 注意设备名包含空格或特殊字符时需要引号包裹 audioArgs $-f dshow -i audio\{audioDevice}\; } string codecArgs -c:v h264_nvenc -preset fast; // 使用NVENC硬件编码 // 如果没有音频设备只录制视频 if (string.IsNullOrEmpty(audioArgs)) { return ${videoArgs} {codecArgs} \{outputPath}\; } else { return ${videoArgs} {audioArgs} {codecArgs} -c:a aac \{outputPath}\; } }当检测到音频设备错误时可以触发重试逻辑private void OnAudioDeviceNotFound() { Console.WriteLine(检测到音频设备问题尝试回退到备用方案...); // 1. 先尝试停止当前进程 try { _ffmpegProcess.StandardInput.WriteLine(q); _ffmpegProcess.WaitForExit(2000); } catch { /* 忽略停止过程中的异常 */ } // 2. 重新探测设备设备状态可能已变化 var availableDevices AudioDeviceHelper.GetAvailableAudioInputDevices(_ffmpegProcess.StartInfo.FileName); // 3. 排除当前失败的设备选择下一个可用的 var remainingDevices availableDevices.Where(d d ! _selectedAudioDevice).ToList(); if (remainingDevices.Any()) { _selectedAudioDevice remainingDevices.First(); Console.WriteLine($切换到备用音频设备: {_selectedAudioDevice}); // 4. 重新开始录制需要重新生成输出文件名避免覆盖 string newOutputPath GenerateNewOutputPath(); StartRecording(newOutputPath, _ffmpegProcess.StartInfo.FileName); } else { Console.WriteLine(无备用音频设备可用将仅录制视频无声); // 启动无音频的录制 string newOutputPath GenerateNewOutputPath(); StartVideoOnlyRecording(newOutputPath, _ffmpegProcess.StartInfo.FileName); } }这种自动回退机制能显著提升用户体验特别是在用户安装了新的音频软件或更改了系统音频设置后。3. dshow参数调优解决音频卡顿、不同步与质量低下即使找到了可用的音频设备录制过程中仍可能出现各种质量问题声音卡顿、音画不同步、背景噪音、音量过低等。这些问题大多可以通过调整dshow参数来解决。3.1 关键dshow参数详解FFmpeg的dshow滤镜提供了丰富的参数来控制音频捕获的质量和性能。以下是最常用且有效的几个audio_buffer_size设置音频缓冲区大小单位毫秒。缓冲区太小会导致丢帧太大会增加延迟。对于系统音频录制通常设置在100-500ms之间。-audio_buffer_size 200sample_rate和sample_size指定采样率和采样大小。系统音频通常为44.1kHz或48kHz16位。-sample_rate 48000 -sample_size 16channels指定声道数。立体声为2单声道为1。-channels 2thread_queue_size设置每个输入流的线程队列大小。对于音频建议设置较大值以避免缓冲区溢出。-thread_queue_size 1024一个经过优化的完整命令示例ffmpeg -f gdigrab -framerate 30 -thread_queue_size 512 -i desktop -f dshow -audio_buffer_size 300 -thread_queue_size 1024 -i audio立体声混音 (Realtek Audio) -c:v h264_nvenc -preset fast -c:a aac -b:a 192k output.mp4在C#中动态构建这些参数public class AudioCaptureConfig { public int AudioBufferSize { get; set; } 300; // 毫秒 public int SampleRate { get; set; } 48000; // Hz public int SampleSize { get; set; } 16; // 位 public int Channels { get; set; } 2; // 立体声 public int ThreadQueueSize { get; set; } 1024; public string AudioBitrate { get; set; } 192k; } private string BuildOptimizedAudioArgs(string audioDevice, AudioCaptureConfig config) { if (string.IsNullOrEmpty(audioDevice)) return string.Empty; var args new Liststring { $-f dshow, $-audio_buffer_size {config.AudioBufferSize}, $-thread_queue_size {config.ThreadQueueSize}, $-sample_rate {config.SampleRate}, $-sample_size {config.SampleSize}, $-channels {config.Channels}, $-i audio\{audioDevice}\ }; return string.Join( , args); }3.2 针对不同场景的参数调优方案不同的使用场景对音频质量的要求不同参数也需要相应调整。场景一游戏录制与实时解说游戏录制通常需要低延迟和高音质同时要处理游戏音效和玩家语音。public AudioCaptureConfig GetGamingRecordingConfig() { return new AudioCaptureConfig { AudioBufferSize 150, // 较低延迟 ThreadQueueSize 2048, // 较大的队列防止游戏音效高峰时溢出 SampleRate 48000, // 标准游戏音频采样率 AudioBitrate 256k // 较高比特率保证音质 }; }场景二在线会议与教学录制会议录制更注重语音清晰度可以适当降低采样率以减小文件大小。public AudioCaptureConfig GetMeetingRecordingConfig() { return new AudioCaptureConfig { AudioBufferSize 300, // 中等缓冲区平衡延迟和稳定性 SampleRate 44100, // 标准语音采样率 AudioBitrate 128k, // 语音不需要太高比特率 Channels 1 // 单声道足够文件体积减半 }; }场景三长时间系统监控录制监控录制需要稳定性优先可以接受一定的延迟。public AudioCaptureConfig GetMonitoringConfig() { return new AudioCaptureConfig { AudioBufferSize 500, // 大缓冲区确保长时间稳定 ThreadQueueSize 4096, // 非常大的队列防止长时间运行溢出 SampleRate 32000, // 较低采样率减小文件体积 AudioBitrate 64k // 低比特率进一步减小体积 }; }3.3 音频问题诊断与参数自适应调整有些音频问题只有在特定系统或特定负载下才会出现。我们可以实现一个简单的诊断机制在录制初期检测音频流状态并自动调整参数。public class AudioQualityMonitor { private DateTime _lastAudioPacketTime; private int _consecutiveDropCount 0; private int _totalPackets 0; private int _droppedPackets 0; public void AnalyzeFfmpegOutput(string line) { // 解析FFmpeg的输出查找音频流信息 // 示例行: size 1234kB time00:01:23.45 bitrate 456.7kbits/s speed1.05x if (line.Contains(time) line.Contains(bitrate)) { _totalPackets; // 检查时间戳是否连续 var currentTime DateTime.Now; if (_lastAudioPacketTime ! DateTime.MinValue) { var interval (currentTime - _lastAudioPacketTime).TotalMilliseconds; // 如果间隔异常大可能是丢包 if (interval 100) // 正常应该在10-30ms { _consecutiveDropCount; _droppedPackets; if (_consecutiveDropCount 5) { // 连续丢包需要调整参数 OnConsecutivePacketDrops(); } } else { _consecutiveDropCount 0; } } _lastAudioPacketTime currentTime; } } private void OnConsecutivePacketDrops() { // 根据丢包率建议调整参数 double dropRate (double)_droppedPackets / _totalPackets; if (dropRate 0.1) // 丢包率超过10% { Console.WriteLine(音频丢包严重建议增大audio_buffer_size或thread_queue_size); // 这里可以触发自动参数调整或通知用户 } } }将监控器集成到录制过程中private AudioQualityMonitor _audioMonitor new AudioQualityMonitor(); private void OnFfmpegErrorOutput(object sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { _errorBuffer.AppendLine(e.Data); _audioMonitor.AnalyzeFfmpegOutput(e.Data); // 分析音频质量 // 原有的错误处理逻辑... } }4. 高级策略多设备捕获、混音与异常恢复对于专业级的录屏工具仅仅捕获一个音频源可能不够。用户可能希望同时录制系统声音和麦克风或者在音频设备异常时无缝切换到备份方案。4.1 同时捕获多个音频源FFmpeg支持同时从多个dshow设备捕获音频并将它们混合到一个音轨中。这在录制教程视频时特别有用——系统演示音效加上讲解人声。public string BuildMultiAudioInputArgs(string systemAudioDevice, string microphoneDevice) { var inputs new Liststring(); if (!string.IsNullOrEmpty(systemAudioDevice)) { inputs.Add($-f dshow -audio_buffer_size 300 -i audio\{systemAudioDevice}\); } if (!string.IsNullOrEmpty(microphoneDevice)) { inputs.Add($-f dshow -audio_buffer_size 150 -i audio\{microphoneDevice}\); } if (inputs.Count 0) return string.Empty; string inputArgs string.Join( , inputs); // 如果有多个音频输入需要复杂滤波器进行混音 if (inputs.Count 1) { // 使用amerge滤镜混合多个音频流 string filterComplex $-filter_complex \[0:a][1:a]amergeinputs2[a]\ -map 0:v -map \[a]\; return ${inputArgs} {filterComplex}; } else { return inputArgs; } }完整的多音频录制命令示例ffmpeg -f gdigrab -framerate 30 -i desktop -f dshow -i audiovirtual-audio-capturer -f dshow -i audio麦克风阵列 (Realtek Audio) -filter_complex [1:a][2:a]amergeinputs2[a] -map 0:v -map [a] -c:v h264_nvenc -c:a aac output.mp44.2 实现音频异常时的无缝切换在某些情况下音频设备可能在录制过程中突然失效比如用户拔掉了USB麦克风。我们可以通过监控FFmpeg的错误输出来检测这种异常并尝试切换到备用设备。public class AudioFailoverManager { private string _primaryDevice; private string _secondaryDevice; private string _currentDevice; private int _failoverCount 0; private const int MAX_FAILOVER_ATTEMPTS 3; public AudioFailoverManager(string primary, string secondary) { _primaryDevice primary; _secondaryDevice secondary; _currentDevice primary; } public bool ShouldFailover(string errorMessage) { // 检测是否需要故障转移的错误类型 var failoverErrors new[] { Device not found, Cannot find audio device, Audio device disconnected, Read timeout, buffer queue overflow }; return failoverErrors.Any(error errorMessage.Contains(error)); } public string GetNextDevice() { _failoverCount; if (_failoverCount MAX_FAILOVER_ATTEMPTS) { return string.Empty; // 超过最大重试次数放弃音频 } // 切换设备逻辑 if (_currentDevice _primaryDevice !string.IsNullOrEmpty(_secondaryDevice)) { _currentDevice _secondaryDevice; Console.WriteLine($音频故障转移: {_primaryDevice} - {_secondaryDevice}); } else if (_currentDevice _secondaryDevice !string.IsNullOrEmpty(_primaryDevice)) { _currentDevice _primaryDevice; // 尝试切回主设备 Console.WriteLine($音频故障转移: {_secondaryDevice} - {_primaryDevice}); } else { _currentDevice string.Empty; // 无更多设备可用 Console.WriteLine(无可用音频设备将仅录制视频); } return _currentDevice; } public void Reset() { _failoverCount 0; _currentDevice _primaryDevice; } }在录制主循环中使用故障转移管理器private AudioFailoverManager _failoverManager; public void StartRecordingWithFailover(string outputPath, string ffmpegPath) { // 初始化故障转移管理器 var audioDevices AudioDeviceHelper.GetAvailableAudioInputDevices(ffmpegPath); string primary audioDevices.FirstOrDefault(d d.Contains(virtual-audio-capturer)) ?? audioDevices.FirstOrDefault(); string secondary audioDevices.FirstOrDefault(d d ! primary); _failoverManager new AudioFailoverManager(primary, secondary); // 开始录制循环 StartRecordingInternal(outputPath, ffmpegPath, _failoverManager.CurrentDevice); } private void OnFfmpegErrorOutput(object sender, DataReceivedEventArgs e) { if (!string.IsNullOrEmpty(e.Data)) { // 检查是否需要故障转移 if (_failoverManager.ShouldFailover(e.Data)) { string nextDevice _failoverManager.GetNextDevice(); if (nextDevice ! _currentAudioDevice) { // 重启录制进程使用新设备 RestartRecordingWithNewDevice(nextDevice); } } } }4.3 录制状态持久化与恢复对于长时间录制任务如会议录制、游戏直播意外中断后的恢复能力至关重要。我们可以实现录制状态的持久化在异常退出后能够恢复录制。public class RecordingSession { public string SessionId { get; set; } public DateTime StartTime { get; set; } public string OutputPath { get; set; } public string CurrentAudioDevice { get; set; } public AudioCaptureConfig AudioConfig { get; set; } public Liststring UsedDevices { get; set; } new Liststring(); public ListDateTime FailoverTimestamps { get; set; } new ListDateTime(); public void SaveState(string filePath) { var json JsonConvert.SerializeObject(this, Formatting.Indented); File.WriteAllText(filePath, json); } public static RecordingSession LoadState(string filePath) { if (File.Exists(filePath)) { var json File.ReadAllText(filePath); return JsonConvert.DeserializeObjectRecordingSession(json); } return null; } public void RecordDeviceSwitch(string newDevice) { UsedDevices.Add(newDevice); FailoverTimestamps.Add(DateTime.Now); CurrentAudioDevice newDevice; } }在录制过程中定期保存状态private void SaveRecordingStatePeriodically() { // 每30秒保存一次状态 var timer new System.Timers.Timer(30000); timer.Elapsed (sender, e) { _currentSession.SaveState(recording_state.json); }; timer.Start(); }当程序异常退出后重新启动时可以尝试恢复public bool TryResumeRecording(string ffmpegPath) { var session RecordingSession.LoadState(recording_state.json); if (session null) return false; Console.WriteLine($检测到未完成的录制会话 {session.SessionId}开始时间: {session.StartTime}); // 检查之前的音频设备是否仍然可用 var availableDevices AudioDeviceHelper.GetAvailableAudioInputDevices(ffmpegPath); string resumeDevice session.CurrentAudioDevice; if (!availableDevices.Contains(resumeDevice) availableDevices.Any()) { // 原设备不可用选择新设备 resumeDevice availableDevices.First(); Console.WriteLine($原音频设备不可用切换到: {resumeDevice}); } // 恢复录制FFmpeg本身不支持真正的恢复但我们可以从上次停止的时间点开始新录制 // 实际实现需要更复杂的逻辑这里简化示例 StartRecording(session.OutputPath _resumed, ffmpegPath, resumeDevice); return true; }这些高级策略虽然增加了实现的复杂性但能显著提升录屏工具的可靠性和用户体验。特别是在商业或专业应用场景中这种鲁棒性是必不可少的。5. 实战案例构建一个完整的C# FFmpeg录屏类库现在让我们把所有知识点整合起来构建一个完整的、可重用的C#录屏类库。这个类库将包含设备发现、参数优化、异常处理和故障转移等所有功能。5.1 核心类设计public class ScreenRecorder : IDisposable { // 配置类 public class RecorderConfig { public string FfmpegPath { get; set; } public string OutputDirectory { get; set; } ./recordings; public int FrameRate { get; set; } 30; public VideoEncoder VideoEncoder { get; set; } VideoEncoder.H264_NVENC; public AudioCaptureConfig AudioConfig { get; set; } new AudioCaptureConfig(); public bool EnableAudioFailover { get; set; } true; public int MaxFailoverAttempts { get; set; } 3; public bool SaveSessionState { get; set; } true; } public enum VideoEncoder { H264_NVENC, // NVIDIA GPU编码 H264_QSV, // Intel Quick Sync Video H264_AMF, // AMD GPU编码 LIBX264 // CPU软件编码 } // 事件定义 public event EventHandlerRecordingStartedEventArgs RecordingStarted; public event EventHandlerRecordingStoppedEventArgs RecordingStopped; public event EventHandlerAudioDeviceChangedEventArgs AudioDeviceChanged; public event EventHandlerErrorEventArgs ErrorOccurred; // 私有字段 private Process _ffmpegProcess; private RecorderConfig _config; private AudioFailoverManager _failoverManager; private RecordingSession _currentSession; private StringBuilder _errorLog new StringBuilder(); private bool _isRecording false; private string _currentOutputPath; // 公共方法 public ScreenRecorder(RecorderConfig config) { _config config ?? throw new ArgumentNullException(nameof(config)); // 验证FFmpeg路径 if (!File.Exists(config.FfmpegPath)) throw new FileNotFoundException($FFmpeg未找到: {config.FfmpegPath}); // 创建输出目录 Directory.CreateDirectory(config.OutputDirectory); } public void StartRecording(string sessionName null) { if (_isRecording) throw new InvalidOperationException(录制已在进行中); try { // 1. 初始化录制会话 _currentSession new RecordingSession { SessionId sessionName ?? Guid.NewGuid().ToString(), StartTime DateTime.Now, AudioConfig _config.AudioConfig.Clone() }; // 2. 发现音频设备 var audioDevices DiscoverAudioDevices(); if (!audioDevices.Any() _config.AudioConfig ! null) { OnErrorOccurred(未找到可用的音频捕获设备将仅录制视频); } // 3. 初始化故障转移管理器 if (_config.EnableAudioFailover audioDevices.Count 2) { _failoverManager new AudioFailoverManager( audioDevices[0], audioDevices.Count 1 ? audioDevices[1] : string.Empty ); _failoverManager.MaxAttempts _config.MaxFailoverAttempts; } // 4. 生成输出文件路径 _currentOutputPath GenerateOutputPath(_currentSession.SessionId); _currentSession.OutputPath _currentOutputPath; // 5. 构建并启动FFmpeg进程 StartFfmpegProcess(); _isRecording true; OnRecordingStarted(new RecordingStartedEventArgs { SessionId _currentSession.SessionId, OutputPath _currentOutputPath, AudioDevice _failoverManager?.CurrentDevice ?? string.Empty }); // 6. 启动状态保存定时器如果启用 if (_config.SaveSessionState) StartStateSavingTimer(); } catch (Exception ex) { OnErrorOccurred($启动录制失败: {ex.Message}); Cleanup(); throw; } } public void StopRecording() { if (!_isRecording || _ffmpegProcess null) return; try { // 发送停止命令 _ffmpegProcess.StandardInput.WriteLine(q); // 等待进程退出 if (!_ffmpegProcess.WaitForExit(5000)) { _ffmpegProcess.Kill(); } // 保存最终状态 if (_config.SaveSessionState _currentSession ! null) { _currentSession.SaveState(GetStateFilePath()); } OnRecordingStopped(new RecordingStoppedEventArgs { SessionId _currentSession?.SessionId, OutputPath _currentOutputPath, Duration _currentSession ! null ? DateTime.Now - _currentSession.StartTime : TimeSpan.Zero }); } finally { Cleanup(); _isRecording false; } } // 私有辅助方法 private Liststring DiscoverAudioDevices() { // 使用之前实现的AudioDeviceHelper return AudioDeviceHelper.GetAvailableAudioInputDevices(_config.FfmpegPath); } private void StartFfmpegProcess() { // 构建FFmpeg命令参数 string arguments BuildFfmpegArguments(); var startInfo new ProcessStartInfo(_config.FfmpegPath) { Arguments arguments, UseShellExecute false, RedirectStandardError true, RedirectStandardInput true, CreateNoWindow true, StandardErrorEncoding Encoding.UTF8 }; _ffmpegProcess new Process { StartInfo startInfo }; _ffmpegProcess.ErrorDataReceived OnFfmpegOutput; _ffmpegProcess.Exited OnFfmpegExited; _ffmpegProcess.Start(); _ffmpegProcess.BeginErrorReadLine(); // 验证进程是否成功启动 Thread.Sleep(500); if (_ffmpegProcess.HasExited _ffmpegProcess.ExitCode ! 0) { throw new Exception($FFmpeg进程异常退出退出代码: {_ffmpegProcess.ExitCode}); } } private string BuildFfmpegArguments() { var args new Liststring(); // 视频输入 args.Add($-f gdigrab -framerate {_config.FrameRate} -i desktop); // 音频输入如果有 string audioDevice _failoverManager?.CurrentDevice; if (!string.IsNullOrEmpty(audioDevice)) { args.Add(BuildAudioInputArgs(audioDevice, _config.AudioConfig)); } // 视频编码器 args.Add(GetVideoEncoderArgs(_config.VideoEncoder)); // 音频编码器如果有音频 if (!string.IsNullOrEmpty(audioDevice)) { args.Add($-c:a aac -b:a {_config.AudioConfig.AudioBitrate}); } // 输出路径 args.Add($\{_currentOutputPath}\); return string.Join( , args); } private void OnFfmpegOutput(object sender, DataReceivedEventArgs e) { if (string.IsNullOrEmpty(e.Data)) return; _errorLog.AppendLine(e.Data); // 错误检测与处理 if (e.Data.Contains(Device not found) || e.Data.Contains(Cannot find audio device)) { HandleAudioDeviceError(); } // 可以添加更多的错误检测逻辑... } private void HandleAudioDeviceError() { if (_failoverManager null || !_config.EnableAudioFailover) return; string nextDevice _failoverManager.GetNextDevice(); if (nextDevice ! _failoverManager.CurrentDevice) { // 触发设备切换 SwitchAudioDevice(nextDevice); } else if (string.IsNullOrEmpty(nextDevice)) { // 无更多设备可用 OnErrorOccurred(所有音频设备均不可用将继续录制无声视频); } } private void SwitchAudioDevice(string newDevice) { // 停止当前录制并重新开始FFmpeg不支持运行时切换输入设备 StopRecording(); // 更新会话信息 _currentSession.RecordDeviceSwitch(newDevice); // 使用新设备重新开始录制 StartFfmpegProcess(); OnAudioDeviceChanged(new AudioDeviceChangedEventArgs { OldDevice _failoverManager.CurrentDevice, NewDevice newDevice, SwitchTime DateTime.Now }); } // 清理资源 private void Cleanup() { if (_ffmpegProcess ! null) { if (!_ffmpegProcess.HasExited) { try { _ffmpegProcess.Kill(); } catch { } } _ffmpegProcess.Dispose(); _ffmpegProcess null; } _stateSaveTimer?.Dispose(); _stateSaveTimer null; } public void Dispose() { StopRecording(); Cleanup(); } }5.2 使用示例class Program { static void Main(string[] args) { var config new ScreenRecorder.RecorderConfig { FfmpegPath C:\tools\ffmpeg\bin\ffmpeg.exe, OutputDirectory C:\Recordings, FrameRate 30, VideoEncoder ScreenRecorder.VideoEncoder.H264_NVENC, AudioConfig new AudioCaptureConfig { AudioBufferSize 300, SampleRate 48000, AudioBitrate 192k }, EnableAudioFailover true, MaxFailoverAttempts 3 }; using (var recorder new ScreenRecorder(config)) { // 订阅事件 recorder.RecordingStarted (s, e) Console.WriteLine($录制开始: {e.SessionId}, 输出: {e.OutputPath}); recorder.AudioDeviceChanged (s, e) Console.WriteLine($音频设备切换: {e.OldDevice} - {e.NewDevice}); recorder.ErrorOccurred (s, e) Console.WriteLine($错误: {e.Message}); // 开始录制 recorder.StartRecording(MyRecordingSession); Console.WriteLine(录制中... 按任意键停止); Console.ReadKey(); // 停止录制 recorder.StopRecording(); } } }这个完整的类库提供了生产级别的稳定性和灵活性。它处理了设备发现、参数优化、异常恢复等复杂问题同时通过事件机制提供了良好的可观察性。在实际项目中你可以根据具体需求进一步扩展比如添加视频质量设置、录制区域选择、实时预览等功能。开发FFmpeg录屏功能最磨人的地方往往不是核心录制逻辑而是这些边界情况的处理。音频设备兼容性就是个典型例子——不同用户的系统环境千差万别你永远不知道下一台电脑上会冒出什么奇怪的设备名或驱动问题。我自己的经验是与其追求一个完美的参数组合不如把精力花在构建健壮的错误处理和回退机制上。让程序能够优雅地降级从无声录制到尝试多个备用设备总比直接崩溃要好得多。那些深夜调试FFmpeg错误输出的经历最终都转化成了上面这些异常处理逻辑。现在每次看到Device not found这样的错误我反而有点亲切感——至少我知道该怎么对付它了。