WPF桌面端开发-音视频录制(使用ffmpeg.exe实现)

2023-08-10 10:43:58 浏览数 (1)

前言

本文只考虑在Windows下使用FFmpeg进行桌面、麦克风、扬声器同时录制的实现方式,Mac下会有些许差异。

之前的FFmpeg有很多问题,现在随着版本的更新基本上都可以解决了,可以使用在项目中。

代码示例:

https://gitee.com/psvmc/z-screen-recorder

FFMPEG的弊端

先说一下使用FFMpeg录制的弊端

  1. 需要引用ffmpeg.exe 文件本身比较大
  2. 无法实现应用内部分界面的录制
  3. 无法录制扬声器
  4. 录制桌面的是都鼠标光标闪烁
  5. 设备的名称如果超过31个字符的话会被截断,而若是将完整的设备名传到参数里则无法进行音频采集,只能将截断的设备名称传进去。
  6. 录制桌面使用GDI方式的时候如果系统缩放不是100%,在多屏录制的时候录制不全。

这些问题我们一一解决:

前两个问题是无法解决的。

解决方法

安装虚拟设备

第3个和第4个问题可以安装软件实现

我们可以安装一个FFMpeg官方提供的一个软件screen capture recorder,弊端是这个软件大概40-50m大小。

编译好的下载地址是:

http://sourceforge.net/projects/screencapturer/files/

安装完了之后,在命令行执行:

代码语言:javascript复制
 ffmpeg -list_devices true -f dshow -i dummy

就会看到多了两个设备

  • screen-capture-recorder 这个就是桌面捕获设备
  • virtual-audio-capturer 这个是音频捕获设备(这个录制的不是麦克风的声音,是系统输出的声音)

但是这样软件也太大了,当然我们也有方法:

我们从该软件的目录中复制以下4个DLL自己注册即可,就不用安装该程序了。

注意

注册必须有C 2010环境。

注册audio_sniffer.dllaudio_sniffer-x64.dll

命令行中注册

打开CMD窗口,执行以下命令:

代码语言:javascript复制
 regsvr32 "D:Toolsffmpegdllvirtual-audioaudio_sniffer.dll"
 regsvr32 "D:Toolsffmpegdllvirtual-audioaudio_sniffer-x64.dll"

注册screen-capture-recorder.dllscreen-capture-recorder-x64.dll

注意

必须用管理员身份注册。

打开CMD窗口,执行以下命令:

代码语言:javascript复制
 regsvr32 "D:Toolsffmpegdllscreen-capture-recorderscreen-capture-recorder.dll"
 regsvr32 "D:Toolsffmpegdllscreen-capture-recorderscreen-capture-recorder-x64.dll"

解除注册

解除添加/u即可。

代码语言:javascript复制
 regsvr32 /u "D:Toolsffmpegdllvirtual-audioaudio_sniffer.dll"

注意

电脑上的音频设备禁用的话,注册的设备也是不可用的。 获取到的设备名称为"virtual-audio-capturer" (none),正常的应为"virtual-audio-capturer" (audio)

代码中注册

代码语言:javascript复制
 using System.Diagnostics;
 ​
 namespace z_screen_recorder.Utils
 {
     public class ZDllUtils
     {
         public static bool LoadDll(string dllPath)
         {
             int exitCode;
             ProcessStartInfo processStartInfo =
                 new ProcessStartInfo { FileName = "regsvr32.exe", Arguments = "/s "   dllPath };
 ​
             //启动新进程并等待执行完毕
             using (Process process = new Process())
             {
                 process.StartInfo = processStartInfo;
                 process.Start();
                 process.WaitForExit();
                 // 获取进程的出错码
                 exitCode = process.ExitCode;
             }
             return exitCode == 0;
         }
 ​
         public static bool ReleaseDll(string dllPath)
         {
             int exitCode;
             ProcessStartInfo processStartInfo =
                 new ProcessStartInfo { FileName = "regsvr32.exe", Arguments = "/u "   dllPath };
 ​
             //启动新进程并等待执行完毕
             using (Process process = new Process())
             {
                 process.StartInfo = processStartInfo;
                 process.Start();
                 process.WaitForExit();
                 // 获取进程的出错码
                 exitCode = process.ExitCode;
             }
             return exitCode == 0;
         }
     }
 }
 ​

调用

代码语言:javascript复制
 if (Environment.Is64BitOperatingSystem)
 {
     Console.WriteLine(@"操作系统为64位系统");
     ZDllUtils.LoadDll("audio_sniffer-x64.dll");
     ZDllUtils.LoadDll("screen-capture-recorder-x64.dll");
 }
 else
 {
     Console.WriteLine(@"操作系统为32位系统");
     ZDllUtils.LoadDll("audio_sniffer.dll");
     ZDllUtils.LoadDll("screen-capture-recorder.dll");
 }

项目中使用

在项目的根目录添加Libs文件夹,复制DLL到该文件夹下

属性 => 生成事件 > 生成前事件命令行中添加

代码语言:javascript复制
 xcopy  /Y /d $(ProjectDir)Libsscreen-capture-recorder* $(TargetDir)
 xcopy  /Y /d $(ProjectDir)Libsvirtual-audio* $(TargetDir)

虚拟设备验证

所有设备

代码语言:javascript复制
 ffmpeg -f dshow -list_devices true -i dummpy

视频设备

代码语言:javascript复制
 ffmpeg -f dshow -list_options true -i video="screen-capture-recorder"

音频设备

代码语言:javascript复制
 ffmpeg -f dshow -list_options true -i audio="virtual-audio-capturer"

使用新版本

最后两个问题使用FFmpeg新版本即可,我这里使用的是6.0版本。

安装依赖

Nuget添加依赖

代码语言:javascript复制
 Install-Package NAudio.Core -Version 2.1.0
 Install-Package NAudio.Wasapi -Version 2.1.0

其中

NAudio.Wasapi的作用:

  • 用来获取默认麦克风设备。
  • 混音的时候获取扬声器的声音大小进行混音。

或者

这个版本内部没有分离,安装这一个即可。

代码语言:javascript复制
 Install-Package NAudio -Version 1.9.0

添加引用

  • System.Drawing

常用的命令

查看音频和视频设备列表

代码语言:javascript复制
 ffmpeg -f dshow -list_devices true -i dummpy

查看Dshow库支持参数

代码语言:javascript复制
 ffmpeg -h demuxer=dshow

视频源

获取视频源支持的分辨率

代码语言:javascript复制
 ffmpeg -f dshow -list_options true -i video="screen-capture-recorder"

音频源

麦克风

代码语言:javascript复制
 ffmpeg -f dshow -list_options true -i audio="麦克风 (Realtek(R) Audio)"

扬声器

代码语言:javascript复制
 ffmpeg -f dshow -list_options true -i audio="virtual-audio-capturer"

获取默认的麦克风

方式1

代码语言:javascript复制
 public static string GetDefaultMicrophone()
 {
     var defaultCaptureDevice = WasapiCapture.GetDefaultCaptureDevice();
     if (defaultCaptureDevice != null)
     {
         return defaultCaptureDevice.FriendlyName;
     }
     return "";
 }

注意

defaultCaptureDevice.FriendlyName的值为麦克风 (Realtek(R) Audio) defaultCaptureDevice.DeviceFriendlyName的值为Realtek(R) Audio 我们要用defaultCaptureDevice.FriendlyName获取的值才和FFmpeg获取的一样。

方式2

代码语言:javascript复制
 /// <summary>
 /// 获取音视频源
 /// </summary>
 /// <returns></returns>
 public static List<DeviceInfo> GetDevice()
 {
     string cmdStr = "-list_devices true -f dshow -i dummy";
     Process process = new Process
     {
         StartInfo =
         {
             FileName = FfmpegPath,
             Arguments = $"{cmdStr}",
             UseShellExecute = false,
             CreateNoWindow = true,
             RedirectStandardInput = false,
             RedirectStandardError = true,
             StandardErrorEncoding = Encoding.UTF8
         }
     };
     process.Start();
     var lines = new List<string>();
     while (!process.StandardError.EndOfStream)
     {
         var line = process.StandardError.ReadLine();
         if (!string.IsNullOrWhiteSpace(line))
         {
             lines.Add(line);
         }
     }
     process.WaitForExit();
     process.Close();
     process.Dispose();
     var deviceList = lines.Where(t => t.StartsWith("[dshow") && (t.Contains("(video)") || t.Contains("(audio)"))).Select(
         t =>
         {
             try
             {
                 var value1 = "] "";
                 var value2 = "" (";
                 var index1 = t.IndexOf(
                                  value1,
                                  StringComparison.CurrentCulture
                              )
                                value1.Length;
                 var index2 = t.IndexOf(
                     value2,
                     StringComparison.CurrentCulture
                 );
                 var deviceName = t.Substring(
                     index1,
                     index2 - index1
                 );
                 if (t.Contains("(video)"))
                 {
                     return new DeviceInfo(
                         DeviceType.Video,
                         deviceName
                     );
                 }
                 if (t.Contains("(audio)"))
                 {
                     return new DeviceInfo(
                         DeviceType.Audio,
                         deviceName
                     );
                 }
             }
             catch
             {
                 // ignored
             }
             return new DeviceInfo(
                 DeviceType.Unknown,
                 t
             );
         }
     ).ToList();
     return deviceList;
 }
 /// <summary>
 /// 获取默认的麦克风设备
 /// </summary>
 /// <returns></returns>
 public static string GetDefaultMicrophone()
 {
     var deviceInfos = GetDevice();
     foreach (var deviceInfo in deviceInfos)
     {
         if (deviceInfo.DeviceType == DeviceType.Audio)
         {
             if (deviceInfo.DeviceName != "virtual-audio-capturer")
             {
                 return deviceInfo.DeviceName;
             }
         }
     }
     return "";
 }

音频录制测试

麦克风

代码语言:javascript复制
 ffmpeg -f dshow -i audio="麦克风 (Realtek(R) Audio)" -t 10 -y "C:UsersAdministratorAppDataLocalTemp____temp.wav"
 ​
 ffmpeg -f dshow -i audio="Realtek(R) Audio" -t 10 -y "C:UsersAdministratorAppDataLocalTemp____temp.wav"

扬声器

代码语言:javascript复制
 ffmpeg -f dshow -i audio="virtual-audio-capturer" -t 10 -y "C:UsersAdministratorAppDataLocalTemp____temp.wav"

录制命令

代码语言:javascript复制
 ffmpeg -rtbufsize 1000M -thread_queue_size 1024 -f dshow -i audio="virtual-audio-capturer" -f dshow -i audio="麦克风 (Realtek(R) Audio)" -filter_complex amix=inputs=2:duration=longest:dropout_transition=2:weights="0.5 2":normalize=0 -f dshow -video_size 1920x1080 -i video="screen-capture-recorder" -an -c:v libx264 -r 24 -pix_fmt yuv420p -preset:v ultrafast -vf scale=-1:1080 -y "D:mp4luping.mp4"
代码语言:javascript复制
 ffmpeg -rtbufsize 1000M -thread_queue_size 1024 -f dshow -i audio="virtual-audio-capturer" -f dshow -i audio="麦克风 (Realtek(R) Audio)" -filter_complex amix=inputs=2:duration=longest:dropout_transition=2:weights="0.7614819 2":normalize=0 -f dshow -video_size 1920x1080 -i video="screen-capture-recorder" -an -c:v libx264 -r 24 -pix_fmt yuv420p -preset:v ultrafast -vf scale=-1:1080 -y "C:UsersAdministratorDownloadsTest20230524095242.mp4"

音频

代码语言:javascript复制
 Install-Package NAudio.Wasapi -Version 2.1.0

默认的麦克风和扬声器

代码语言:javascript复制
 var defaultCaptureDevice = WasapiCapture.GetDefaultCaptureDevice();
 Console.WriteLine($@"默认麦克风:{defaultCaptureDevice.FriendlyName}");
 var defaultLoopbackCaptureDevice = WasapiLoopbackCapture.GetDefaultLoopbackCaptureDevice();
 Console.WriteLine($@"默认扬声器:{defaultLoopbackCaptureDevice.FriendlyName}");

获取扬声器的声音大小

代码语言:javascript复制
 /// <summary>
 /// 获取扬声器音量大小 从0-1
 /// </summary>
 /// <returns></returns>
 public static float GetVolume()
 {
     var defaultLoopbackCaptureDevice = WasapiLoopbackCapture.GetDefaultLoopbackCaptureDevice();
     return defaultLoopbackCaptureDevice.AudioEndpointVolume.MasterVolumeLevelScalar;
 }

这个方法主要用于麦克风和扬声器混音时,设置混音比。因为默认是用相当于100%的扬声器音量。

判断麦克风是否可用

要想准确判断麦克风是否可用要满足一下三个条件

  • 有激活的麦克风设备
  • 录制麦克风生成了音频文件
  • 音频文件大小要大于0

这三个条件缺一不可

使用FFmpeg判断(推荐)

本来是推荐下面的方式的,但是下面的方式有个问题

在Win7系统上,FFmpeg有问题,获取到的音频设备的名称过长的话就会被截取,而NAudio获取到的名称是完整的,导致传入完整的设备名称进行录制的时候,反而ffmpwg找不到设备,必须传被截取后的名称,所以稳妥的方式就是使用ffmpeg获取设备名称。

示例

代码语言:javascript复制
 /// <summary>
 /// 获取麦克风名称
 /// </summary>
 /// <returns></returns>
 public static string GetMicrophoneNameByFFmpeg()
 {
     string mName = "";
     var deviceInfos = GetDevice();
     for (var i = 0; i < deviceInfos.Count; i  )
     {
         var deviceInfo = deviceInfos[i];
         if (deviceInfo.DeviceType == DeviceType.Audio)
         {
             if (deviceInfo.DeviceName != "virtual-audio-capturer")
             {
                 mName = deviceInfo.DeviceName;
                 break;
             }
         }
     }
     return mName;
 }
 ​
 public static bool IsMicrophoneGoodByFFmpeg()
 {
     string mName = GetMicrophoneNameByFFmpeg();
     if (string.IsNullOrEmpty(mName))
     {
         return false;
     }
     return IsMicrophoneGoodByFFmpeg(mName);
 }
 ​
 /// <summary>
 /// 使用FFmpeg检测麦克风是否可用
 /// </summary>
 /// <param name="name"></param>
 /// <returns></returns>
 public static bool IsMicrophoneGoodByFFmpeg(string name)
 {
     string tempPath = Path.Combine(Path.GetTempPath(), "____temp.wav");
     if (File.Exists(tempPath))
     {
         File.Delete(tempPath);
     }
     Console.WriteLine($@"临时存放路径:{tempPath}");
     string ffmpegpath = FfmpegPath;
     try
     {
         using (Process mProcess = new Process())
         {
             mProcess.StartInfo.FileName = ffmpegpath; //ffmpeg.exe的绝对路径
             string args = $"-f dshow -i audio="{name}" -t 1 -y "{tempPath}"";
             mProcess.StartInfo.Arguments = args;
             mProcess.StartInfo.UseShellExecute = false; //不使用操作系统外壳程序启动
             mProcess.StartInfo.RedirectStandardError = false; //重定向标准错误输出
             mProcess.StartInfo.CreateNoWindow = true; //不显示程序窗口
             mProcess.StartInfo.RedirectStandardInput = false; //用于模拟该进程控制台的输入
             mProcess.Start(); //启动线程
             mProcess.WaitForExit(); //阻塞等待进程结束
             mProcess.Close();
         }
         return File.Exists(tempPath) && new FileInfo(tempPath).Length > 0;
     }
     catch (Exception)
     {
         return false;
     }
 }

使用NAudio判断

这种方式相对于FFmpeg的方式,更加的快速。

代码语言:javascript复制
 public static bool IsMicrophoneGood()
 {
     bool isGood = false;
     int total = 0;
     //没有麦克风
     if (WaveIn.DeviceCount == 0)
     {
         return false;
     }
     WaveInEvent waveIn;
     try
     {
         waveIn = new WaveInEvent();
         waveIn.DataAvailable  = (s, a) =>
         {
             isGood = true;
         };
         waveIn.StartRecording();
     }
     catch (Exception)
     {
         //麦克风无法启动
         return false;
     }
     while (!isGood && total <= 3000)
     {
         Thread.Sleep(100);
         total  = 100;
     }
     waveIn.StopRecording();
     waveIn.Dispose();
     return isGood;
 }

录制工具类

录制

代码语言:javascript复制
 namespace z_screen_recorder.Utils.Record
 {
     using System;
     using System.Collections.Generic;
     using System.Diagnostics;
     using System.IO;
     using System.Linq;
     using System.Text;
     using System.Threading;
     using System.Windows.Forms;
     using NAudio.CoreAudioApi;
     using NAudio.Wave;
     using Model;
     using System.Windows.Threading;
 ​
     // ReSharper disable once InconsistentNaming
     public class ZFFmpegUtils
     {
         private static ZFFmpegUtils _instance;
         private static Process _recordProcess;
         public RecordState State = RecordState.Stop;
 ​
         //是否是异常退出
         private bool _isStopByKill;
 ​
         //开始录制的时间 防止还未开始就停止 导致生成文件有问题
         private DateTime _startTime;
         private string _savePath;
 ​
         private IRecordCallback _recordCallback;
         private static Dispatcher _dispatcher;
 ​
         /// <summary>
         /// 因为设置环境变量的方式必须重启电脑,所以使用绝对路径了。
         /// </summary>
         private static readonly string FfmpegPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg.exe");
 ​
         #region 录制相关
 ​
         private ZFFmpegUtils()
         {
         }
 ​
         public static ZFFmpegUtils GetInstance()
         {
             return _instance ?? (_instance = new ZFFmpegUtils());
         }
 ​
         /// <summary>
         /// 开始录制
         /// </summary>
         /// <param name="savePath"></param>
         /// <param name="dispatcher"></param>
         /// <param name="recordCallback"></param>
         public void Start
         (
             string savePath,
             Dispatcher dispatcher = null,
             IRecordCallback recordCallback = null
         )
         {
             if (_recordProcess != null && !_recordProcess.HasExited)
             {
                 _recordProcess?.Kill();
                 _recordProcess?.Dispose();
                 _recordProcess = null;
                 if (_recordCallback != null)
                 {
                     _dispatcher?.Invoke
                     (
                         () =>
                         {
                             _recordCallback.RecordFail("请等待录制结束!");
                         }
                     );
                 }
                 return;
             }
             _dispatcher = dispatcher;
             _recordCallback = recordCallback;
             _savePath = savePath;
             new Thread
             (
                 () =>
                 {
                     if (IsMicrophoneGood())
                     {
                         string cmdStr = GetCmd(savePath);
                         State = RecordState.Start;
                         _recordProcess = new Process
                         {
                             StartInfo =
                             {
                                 FileName = FfmpegPath,
                                 Arguments = $"{cmdStr}",
                                 UseShellExecute = false,
                                 CreateNoWindow = true,
                                 RedirectStandardInput = true,
                                 RedirectStandardOutput = false,
                                 RedirectStandardError = false
                             }
                         };
                         _recordProcess?.Start();
                         _startTime = DateTime.Now;
                         if (_recordCallback != null)
                         {
                             Thread.Sleep(2000);
                             _dispatcher?.Invoke
                             (
                                 () =>
                                 {
                                     _recordCallback.RecordStart();
                                 }
                             );
                         }
                         _recordProcess?.WaitForExit();
                         _recordProcess?.Close();
                         _recordProcess?.Dispose();
                         // 程序自动停止的,不是手动触发的。
                         if (State != RecordState.Stop)
                         {
                             State = RecordState.Stop;
                             if (_recordCallback != null)
                             {
                                 _dispatcher?.Invoke
                                 (
                                     () =>
                                     {
                                         _recordCallback.RecordFail("录制启动失败!");
                                     }
                                 );
                             }
                         }
                     }
                     else
                     {
                         State = RecordState.Stop;
                         if (_recordCallback != null)
                         {
                             _dispatcher?.Invoke
                             (
                                 () =>
                                 {
                                     _recordCallback.RecordFail("麦克风不可用");
                                 }
                             );
                         }
                     }
                 }
             ).Start();
         }
 ​
         /// <summary>
         /// 暂停
         /// </summary>
         public void Pause()
         {
             if (_recordProcess == null)
             {
                 return;
             }
             if (!_recordProcess.HasExited)
             {
                 _recordProcess?.Suspend();
                 State = RecordState.Pause;
             }
         }
 ​
         /// <summary>
         /// 恢复
         /// </summary>
         public void Resume()
         {
             if (_recordProcess == null)
             {
                 return;
             }
             if (_recordProcess.HasExited)
             {
                 return;
             }
             if (State != RecordState.Pause)
             {
                 return;
             }
             _recordProcess?.Resume();
             State = RecordState.Start;
         }
 ​
         /// <summary>
         /// 停止
         /// </summary>
         public void Stop()
         {
             if (_recordProcess == null)
             {
                 State = RecordState.Stop;
                 if (_recordCallback != null)
                 {
                     _dispatcher?.Invoke
                     (
                         () =>
                         {
                             _recordCallback.RecordFail("录制进程不存在!");
                         }
                     );
                 }
                 return;
             }
             if (_recordProcess.HasExited)
             {
                 State = RecordState.Stop;
                 if (_recordCallback != null)
                 {
                     _dispatcher?.Invoke
                     (
                         () =>
                         {
                             _recordCallback.RecordFail("录制进程已退出!");
                         }
                     );
                 }
                 return;
             }
             if (State == RecordState.Pause)
             {
                 Resume();
             }
             if (DateTime.Now.Subtract(_startTime).TotalMilliseconds < 5000)
             {
                 if (_recordCallback != null)
                 {
                     _dispatcher?.Invoke
                     (
                         () =>
                         {
                             _recordCallback.RecordShort();
                         }
                     );
                 }
                 return;
             }
             State = RecordState.Stop;
             _isStop = false;
             _isStopByKill = false;
             var recordProcessId = _recordProcess?.Id;
             new Thread
             (
                 () =>
                 {
                     Thread.Sleep(3000);
                     if (_isStop)
                     {
                         return;
                     }
                     if (_recordProcess == null || _recordProcess.HasExited)
                     {
                         return;
                     }
                     //保证关闭的Process和要关闭的为同一个
                     if (recordProcessId == _recordProcess?.Id)
                     {
                         _isStopByKill = true;
                         _recordProcess?.Kill();
                         _recordProcess?.Dispose();
                         _recordProcess = null;
                         if (_recordCallback != null)
                         {
                             _dispatcher?.Invoke
                             (
                                 () =>
                                 {
                                     _recordCallback.RecordFail("异常结束");
                                 }
                             );
                         }
                     }
                 }
             ).Start();
             _recordProcess?.StandardInput.WriteLine("q");
             _recordProcess?.WaitForExit();
             _recordProcess?.Close();
             _isStop = true;
             _recordProcess = null;
             if (!_isStopByKill && _recordCallback != null)
             {
                 //防止进程结束了,但是文件还未完全写入的情况
                 Thread.Sleep(3000);
                 _dispatcher?.Invoke
                 (
                     () =>
                     {
                         _recordCallback.RecordFinish(_savePath);
                     }
                 );
             }
             State = RecordState.Stop;
         }
 ​
         #endregion
 ​
         #region 工具方法
 ​
         /// <summary>
         /// 获取FFmpeg版本
         /// </summary>
         /// <returns></returns>
         public static string GetFFmpegVersion()
         {
             string version = "";
             try
             {
                 if (!File.Exists(FfmpegPath))
                 {
                     return version;
                 }
                 Process process = new Process();
                 process.StartInfo.FileName = FfmpegPath;
                 process.StartInfo.Arguments = "-version";
                 process.StartInfo.UseShellExecute = false;
                 process.StartInfo.RedirectStandardOutput = true;
                 process.StartInfo.CreateNoWindow = true;
                 process.Start();
                 string result = process.StandardOutput.ReadToEnd();
                 process.WaitForExit();
                 int index = result.IndexOf("version", StringComparison.Ordinal)   8;
                 version = result.Substring(index, 3);
                 return version;
             }
             catch (Exception ex)
             {
                 Console.WriteLine(ex.Message);
                 return "";
             }
         }
 ​
         public static bool IsFfmpegInstalled()
         {
             return GetFFmpegVersion() == "6.0";
         }
 ​
         /// <summary>
         /// 获取视频时长
         /// </summary>
         /// <param name="sourceFile">视频地址</param>
         /// <returns></returns>
         public static string GetVideoDuration(string sourceFile)
         {
             string ffmpegpath = FfmpegPath;
             string duration = "";
             using (var ffmpeg = new Process())
             {
                 ffmpeg.StartInfo.UseShellExecute = false;
                 ffmpeg.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                 ffmpeg.StartInfo.RedirectStandardError = true;
                 ffmpeg.StartInfo.FileName = ffmpegpath;
                 ffmpeg.StartInfo.Arguments = "-i ""   sourceFile   """;
                 ffmpeg.StartInfo.CreateNoWindow = true; // 不显示程序窗口
                 ffmpeg.Start();
                 var errorreader = ffmpeg.StandardError;
                 ffmpeg.WaitForExit();
                 var result = errorreader.ReadToEnd();
                 if (result.Contains("Duration: "))
                 {
                     duration = result.Substring(result.IndexOf("Duration: ", StringComparison.Ordinal)   ("Duration: ").Length, ("00:00:00").Length);
                 }
             }
             return duration;
         }
 ​
         /// <summary>
         /// 生成缩略图
         /// </summary>
         /// <param name="videoPath"></param>
         /// <param name="imagePath"></param>
         /// <param name="width"></param>
         /// <param name="height"></param>
         /// <returns></returns>
         public static bool GenerateThumbnails
         (
             string videoPath,
             string imagePath,
             int width = 1280,
             int height = 720
         )
         {
             if (File.Exists(imagePath))
             {
                 File.Delete(imagePath);
             }
             string ffmpegpath = FfmpegPath;
             string whStr = "";
             if (width > 0)
             {
                 whStr = " -s "   width   "x"   height;
             }
             try
             {
                 using (Process mProcess = new Process())
                 {
                     mProcess.StartInfo.FileName = ffmpegpath; //ffmpeg.exe的绝对路径
                     mProcess.StartInfo.Arguments = "-i ""   videoPath   "" -ss 1 -vframes 1 -r 1 -ac 1 -ab 2"   whStr   " -f image2 ""   imagePath   """;
                     mProcess.StartInfo.UseShellExecute = false; //不使用操作系统外壳程序启动
                     mProcess.StartInfo.RedirectStandardError = false; //重定向标准错误输出
                     mProcess.StartInfo.CreateNoWindow = true; //不显示程序窗口
                     mProcess.StartInfo.RedirectStandardInput = false; //用于模拟该进程控制台的输入
                     mProcess.Start(); //启动线程
                     mProcess.WaitForExit(); //阻塞等待进程结束
                 }
                 return true;
             }
             catch (Exception)
             {
                 return false;
             }
         }
 ​
         /// <summary>
         /// 获取音视频源
         /// </summary>
         /// <returns></returns>
         public static List<DeviceInfo> GetDevice()
         {
             string cmdStr = "-list_devices true -f dshow -i dummy";
             Process process = new Process
             {
                 StartInfo =
                 {
                     FileName = FfmpegPath,
                     Arguments = $"{cmdStr}",
                     UseShellExecute = false,
                     CreateNoWindow = true,
                     RedirectStandardInput = false,
                     RedirectStandardError = true,
                     StandardErrorEncoding = Encoding.UTF8
                 }
             };
             process.Start();
             var lines = new List<string>();
             while (!process.StandardError.EndOfStream)
             {
                 var line = process.StandardError.ReadLine();
                 if (!string.IsNullOrWhiteSpace(line))
                 {
                     lines.Add(line);
                 }
             }
             process.WaitForExit();
             process.Dispose();
             var deviceList = lines.Where(t => t.StartsWith("[dshow") && (t.Contains("(video)") || t.Contains("(audio)")))
                 .Select
                 (
                     t =>
                     {
                         try
                         {
                             var value1 = "] "";
                             var value2 = "" (";
                             var index1 = t.IndexOf(value1, StringComparison.CurrentCulture)   value1.Length;
                             var index2 = t.IndexOf(value2, StringComparison.CurrentCulture);
                             var deviceName = t.Substring(index1, index2 - index1);
                             if (t.Contains("(video)"))
                             {
                                 return new DeviceInfo(DeviceType.Video, deviceName);
                             }
                             if (t.Contains("(audio)"))
                             {
                                 return new DeviceInfo(DeviceType.Audio, deviceName);
                             }
                         }
                         catch
                         {
                             // ignored
                         }
                         return new DeviceInfo(DeviceType.Unknown, t);
                     }
                 )
                 .ToList();
             return deviceList;
         }
 ​
         /// <summary>
         /// 获取视频源支持的分辨率
         /// </summary>
         /// <param name="deviceName"></param>
         /// <returns></returns>
         public static List<VideoOption> GetVideoResolutionList(string deviceName)
         {
             string cmdStr = $"-list_options true -f dshow -i video="{deviceName}"";
             Process process = new Process
             {
                 StartInfo =
                 {
                     FileName = FfmpegPath,
                     Arguments = $"{cmdStr}",
                     UseShellExecute = false,
                     CreateNoWindow = true,
                     RedirectStandardInput = false,
                     RedirectStandardError = true,
                     StandardErrorEncoding = Encoding.UTF8
                 }
             };
             process.Start();
             var lines = new List<string>();
             while (!process.StandardError.EndOfStream)
             {
                 var line = process.StandardError.ReadLine();
                 if (!string.IsNullOrWhiteSpace(line))
                 {
                     lines.Add(line);
                 }
             }
             process.WaitForExit();
             process.Close();
             process.Dispose();
             var allList = lines.Where(t => t.StartsWith("[dshow") && t.Contains("pixel_format="))
                 .Select
                 (
                     t =>
                     {
                         try
                         {
                             var value = "pixel_format=";
                             var index = t.IndexOf(value, StringComparison.CurrentCulture); //pixel_format=索引
                             var text = t.Substring(index   value.Length); //yuyv422 min s=640x480 fps = 30 max s = 640x480 fps = 30
                             index = text.IndexOf(" ", StringComparison.CurrentCulture);
                             var pixelFormat = text.Substring(0, index).TrimEnd();
                             value = "max s";
                             index = text.IndexOf(value, StringComparison.CurrentCulture); //max s索引
                             text = text.Substring(index   value.Length); // = 640x480 fps = 30
                             index = text.IndexOf("=", StringComparison.CurrentCulture); //=索引(max s=索引)
                             text = text.Substring(index   1); //640x480 fps = 30
                             index = text.IndexOf("fps", StringComparison.CurrentCulture); //fps索引
                             var resolution = text.Substring(0, index).TrimStart().TrimEnd();
                             index = text.IndexOf("=", StringComparison.CurrentCulture); //=索引(fps =索引)
                             var fpsString = text.Substring(index   1);
                             var fps = int.Parse(fpsString);
                             return new VideoOption()
                             {
                                 Fps = fps,
                                 PixelFormat = pixelFormat,
                                 Resolution = resolution
                             };
                         }
                         catch
                         {
                             // ignored
                         }
                         return new VideoOption();
                     }
                 )
                 .Where(t => t.Width > 0 && t.Height > 0)
                 .OrderByDescending(t => t.Width)
                 .ToList();
             return allList;
         }
 ​
         /// <summary>
         /// 获取扬声器音量大小 从0-1
         /// </summary>
         /// <returns></returns>
         public static float GetVolume()
         {
             var defaultLoopbackCaptureDevice = WasapiLoopbackCapture.GetDefaultLoopbackCaptureDevice();
             return defaultLoopbackCaptureDevice.AudioEndpointVolume.MasterVolumeLevelScalar;
         }
 ​
         /// <summary>
         /// 是否所需设备都存在
         /// </summary>
         /// <returns></returns>
         public static bool HasNeedDevice()
         {
             bool hasVideo = false;
             bool hasAudio = false;
             var deviceInfos = GetDevice();
             foreach (var deviceInfo in deviceInfos)
             {
                 if (deviceInfo.DeviceName == "virtual-audio-capturer")
                 {
                     hasAudio = true;
                 }
                 if (deviceInfo.DeviceName == "screen-capture-recorder")
                 {
                     hasVideo = true;
                 }
             }
             return hasVideo && hasAudio;
         }
 ​
         /// <summary>
         /// 进程是否已结束
         /// </summary>
         private static bool _isStop;
 ​
         public static bool IsMicrophoneGood()
         {
             bool isGood = false;
             int total = 0;
             //没有麦克风
             if (WaveIn.DeviceCount == 0)
             {
                 return false;
             }
             WaveInEvent waveIn = new WaveInEvent();
             waveIn.DataAvailable  = (s, a) =>
             {
                 isGood = true;
             };
             try
             {
                 waveIn.StartRecording();
             }
             catch (Exception)
             {
                 //麦克风无法启动
                 return false;
             }
             while (!isGood || total >= 3000)
             {
                 Thread.Sleep(100);
                 total  = 100;
             }
             waveIn.StopRecording();
             waveIn.Dispose();
             return isGood;
         }
 ​
         /// <summary>
         /// 获取默认的麦克风设备
         /// </summary>
         /// <returns></returns>
         public static string GetDefaultMicrophone()
         {
             try
             {
                 var defaultCaptureDevice = WasapiCapture.GetDefaultCaptureDevice();
                 return defaultCaptureDevice != null ? defaultCaptureDevice.FriendlyName : "";
             }
             catch (Exception)
             {
                 return "";
             }
         }
 ​
         /// <summary>
         /// 获取FFmpeg指令
         /// </summary>
         /// <param name="savePath"></param>
         /// <returns></returns>
         public static string GetCmd(string savePath)
         {
             List<string> strList = new List<string>
             {
                 "-rtbufsize 1000M -thread_queue_size 1024",
                 "-f dshow -i audio="virtual-audio-capturer""
             };
             string microphoneName = GetDefaultMicrophone();
             if (microphoneName != "")
             {
                 if (IsMicrophoneGood())
                 {
                     var volume = GetVolume();
                     strList.Add($"-f dshow -i audio="{microphoneName}"");
                     strList.Add($"-filter_complex amix=inputs=2:duration=longest:dropout_transition=2:weights="{volume * 2} 2":normalize=0");
                 }
             }
             int screenWidth = Screen.PrimaryScreen.Bounds.Width;
             int screenHeight = Screen.PrimaryScreen.Bounds.Height;
             strList.Add($"-f dshow -video_size {screenWidth}x{screenHeight} -i video="screen-capture-recorder" -an -c:v libx264 -r 24 -pix_fmt yuv420p -preset:v ultrafast");
             strList.Add(screenHeight < 1080 ? $"-vf scale=-1:{screenHeight} -y" : "-vf scale=-1:1080 -y");
             strList.Add($""{savePath}"");
             string cmdStr = string.Join(" ", strList);
             return cmdStr;
         }
 ​
         #endregion
     }
 ​
     public enum RecordState
     {
         Stop,
         Start,
         Pause
     }
 ​
     public interface IRecordCallback
     {
         void RecordStart();
 ​
         //录制时间过短的回调
         void RecordShort();
         void RecordFinish(string mp4Path);
         void RecordFail(string errMsg);
     }
 }

暂停和恢复的实现

FFmpeg能实现录制和停止,但是是不支持暂停和恢复的,但是我们可以扩展Process的方法来实现暂停和恢复功能。

代码语言:javascript复制
 using System;
 using System.Collections;
 using System.Diagnostics;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 ​
 namespace z_screen_recorder.Utils
 {
     public static class ProcessExtensions
     {
         #region Methods
 ​
         public static void Suspend(this Process process)
         {
             void Action(ProcessThread pt)
             {
                 var threadHandle = NativeMethods.OpenThread(ThreadAccess.SuspendResume, false, (uint)pt.Id);
 ​
                 if (threadHandle != IntPtr.Zero)
                 {
                     try
                     {
                         NativeMethods.SuspendThread(threadHandle);
                     }
                     finally
                     {
                         NativeMethods.CloseHandle(threadHandle);
                     }
                 }
             }
 ​
 ​
             var threads = process.Threads.ToArray<ProcessThread>();
 ​
             if (threads.Length > 1)
             {
                 Parallel.ForEach(threads, new ParallelOptions { MaxDegreeOfParallelism = threads.Length }, Action);
             }
             else
             {
                 Action(threads[0]);
             }
         }
 ​
         public static void Resume(this Process process)
         {
             void Action(ProcessThread pt)
             {
                 var threadHandle = NativeMethods.OpenThread(ThreadAccess.SuspendResume, false, (uint)pt.Id);
 ​
                 if (threadHandle != IntPtr.Zero)
                 {
                     try
                     {
                         NativeMethods.ResumeThread(threadHandle);
                     }
                     finally
                     {
                         NativeMethods.CloseHandle(threadHandle);
                     }
                 }
             }
 ​
             var threads = process.Threads.ToArray<ProcessThread>();
 ​
             if (threads.Length > 1)
             {
                 Parallel.ForEach(
                     threads,
                     new ParallelOptions { MaxDegreeOfParallelism = threads.Length },
                     Action
                 );
             }
             else
             {
                 Action(threads[0]);
             }
         }
 ​
         #endregion
 ​
         #region Interop
 ​
         static class NativeMethods
         {
             [DllImport("kernel32.dll")]
             [return: MarshalAs(UnmanagedType.Bool)]
             public static extern bool CloseHandle(IntPtr hObject);
 ​
             [DllImport("kernel32.dll")]
             public static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
 ​
             [DllImport("kernel32.dll")]
             public static extern uint SuspendThread(IntPtr hThread);
 ​
             [DllImport("kernel32.dll")]
             public static extern uint ResumeThread(IntPtr hThread);
         }
 ​
         [Flags]
         enum ThreadAccess
         {
             SuspendResume = (0x0002)
         }
 ​
         #endregion
     }
 ​
     static class Helper
     {
         public static T[] ToArray<T>(this ICollection collection)
         {
             var items = new T[collection.Count];
             collection.CopyTo(items, 0);
 ​
             return items;
         }
     }
 }

生成缩略图

代码语言:javascript复制
 /// <summary>
 /// 生成缩略图
 /// </summary>
 /// <param name="videoPath"></param>
 /// <param name="imagePath"></param>
 /// <param name="width"></param>
 /// <param name="height"></param>
 /// <returns></returns>
 public static bool GenerateThumbnails
 (
     string videoPath,
     string imagePath,
     int width = 1280,
     int height = 720
 )
 {
     if (File.Exists(
             imagePath
         ))
     {
         File.Delete(
             imagePath
         );
     }
     string ffmpegpath = "ffmpeg.exe";
     string whStr = "";
     if (width > 0)
     {
         whStr = " -s "   width   "x"   height;
     }
     try
     {
         Process mProcess = new Process();
         mProcess.StartInfo.FileName = ffmpegpath; //ffmpeg.exe的绝对路径
         mProcess.StartInfo.Arguments = "-i ""   videoPath   "" -ss 1 -vframes 1 -r 1 -ac 1 -ab 2"   whStr   " -f image2 ""   imagePath   """;
         mProcess.StartInfo.UseShellExecute = false; //不使用操作系统外壳程序启动
         mProcess.StartInfo.RedirectStandardError = true; //重定向标准错误输出
         mProcess.StartInfo.CreateNoWindow = true; //不显示程序窗口
         mProcess.StartInfo.RedirectStandardInput = true; //用于模拟该进程控制台的输入
         mProcess.Start(); //启动线程
         mProcess.BeginErrorReadLine(); //开始异步读取
         mProcess.WaitForExit(); //阻塞等待进程结束
         mProcess.Close(); //关闭进程
         mProcess.Dispose(); //释放资源
         return true;
     }
     catch (Exception)
     {
         return false;
     }
 }

获取视频时长

代码语言:javascript复制
 /// <summary>
 /// 获取视频时长
 /// </summary>
 /// <param name="sourceFile">视频地址</param>
 /// <returns></returns>
 public static string GetVideoDuration
 (
     string sourceFile
 )
 {
     string ffmpegpath = Path.Combine(
         Environment.CurrentDirectory,
         "ffmpeg.exe"
     );
     if (!File.Exists(
             ffmpegpath
         ))
     {
         return "";
     }
     string duration;
     using (var ffmpeg = new Process())
     {
         ffmpeg.StartInfo.UseShellExecute = false;
         ffmpeg.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
         ffmpeg.StartInfo.RedirectStandardError = true;
         ffmpeg.StartInfo.FileName = ffmpegpath;
         ffmpeg.StartInfo.Arguments = "-i "   sourceFile;
         ffmpeg.StartInfo.CreateNoWindow = true; // 不显示程序窗口
         ffmpeg.Start();
         var errorreader = ffmpeg.StandardError;
         ffmpeg.WaitForExit();
         var result = errorreader.ReadToEnd();
         duration = result.Substring(
             result.IndexOf(
                 "Duration: ",
                 StringComparison.Ordinal
             )   ("Duration: ").Length,
             ("00:00:00").Length
         );
     }
     return duration;
 }

获取视频信息中有一行

代码语言:javascript复制
 Duration: 00:00:08.00, start: 0.000000, bitrate: 648 kb/s

从中截取时长

打开系统声音设置

代码语言:javascript复制
 Process.Start("mmsys.cpl");

调用本地播放

代码语言:javascript复制
 Process pro = new Process
 {
   StartInfo = new ProcessStartInfo(videoPath)
 };
 pro.Start();

环境设置

为了保证录制正常,必须保证FFmpeg安装并设置环境变量。

判断FFmpeg是否安装

这种方式不推荐使用,添加环境变量不能立即生效

代码语言:javascript复制
 /// <summary>
 /// 判断FFmpeg是否安装并添加环境变量
 /// </summary>
 /// <returns></returns>
 public static bool IsFfmpegInstalled()
 {
     try
     {
         var processStartInfo = new ProcessStartInfo
         {
             FileName = "cmd.exe",
             Arguments = "/C ffmpeg",
             RedirectStandardOutput = false,
             RedirectStandardError = true,
             UseShellExecute = false,
             CreateNoWindow = true
         };
         var process = Process.Start(
             processStartInfo
         );
         if (process != null)
         {
             process.WaitForExit();
             var output = process.StandardError.ReadToEnd();
             return output.Contains(
                 "ffmpeg version"
             );
         }
         return false;
     }
     catch
     {
         return false;
     }
 }

推荐方式

代码语言:javascript复制
 /// <summary>
 /// 获取FFmpeg版本
 /// </summary>
 /// <returns></returns>
 public static string GetFFmpegVersion()
 {
     string version = "";
     try
     {
         if (File.Exists(FfmpegPath))
         {
             Process process = new Process();
             process.StartInfo.FileName = FfmpegPath;
             process.StartInfo.Arguments = "-version";
             process.StartInfo.UseShellExecute = false;
             process.StartInfo.RedirectStandardOutput = true;
             process.StartInfo.CreateNoWindow = true;
             process.Start();
             string output = process.StandardOutput.ReadToEnd();
             process.WaitForExit();
             int index = output.IndexOf(
                             "version",
                             StringComparison.Ordinal
                         )  
                         8;
             version = output.Substring(
                 index,
                 3
             );
         }
         return version;
     }
     catch (Exception ex)
     {
         Console.WriteLine(ex.Message);
         return "";
     }
 }
 ​
 public static bool IsFfmpegInstalled()
 {
     return GetFFmpegVersion() == "6.0";
 }

下载FFmpeg

https://www.psvmc.cn/article/2020-02-04-wpf-start-11-file-download.html

设置环境变量

代码语言:javascript复制
 namespace Z.Utils.Record
 {
     using System;
     using Microsoft.Win32;
 ​
     public class ZEnvPathUtils
     {
         // 添加环境变量Path的函数
         public static bool AddToPath
         (
             string path
         )
         {
             // 找到系统环境变量Path的注册表项
             string regPath = "SYSTEM\CurrentControlSet\Control\Session Manager\Environment";
             RegistryKey regKey = Registry.LocalMachine.OpenSubKey(
                 regPath,
                 true
             );
             if (regKey == null)
             {
                 Console.WriteLine(
                     @"Can't open Environment key."
                 );
                 return false;
             }
 ​
             // 获取当前的Path值
             string value = (string)regKey.GetValue(
                 "Path",
                 "",
                 RegistryValueOptions.DoNotExpandEnvironmentNames
             );
             Console.WriteLine(
                 $@"value:{value}"
             );
 ​
             // 新的Path值
             value = path   ";"   value;
 ​
             // 更新Path值
             regKey.SetValue(
                 "Path",
                 value,
                 RegistryValueKind.ExpandString
             );
             regKey.Close();
             return true;
         }
     }
 }

0 人点赞