前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WPF桌面端开发-音视频录制(使用ffmpeg.exe实现)

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

原创
作者头像
码客说
发布2023-08-10 10:43:58
6060
发布2023-08-10 10:43:58
举报
文章被收录于专栏:码客

前言

本文只考虑在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:\Tools\ffmpeg\dll\virtual-audio\audio_sniffer.dll"
 regsvr32 "D:\Tools\ffmpeg\dll\virtual-audio\audio_sniffer-x64.dll"

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

注意

必须用管理员身份注册。

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

代码语言:javascript
复制
 regsvr32 "D:\Tools\ffmpeg\dll\screen-capture-recorder\screen-capture-recorder.dll"
 regsvr32 "D:\Tools\ffmpeg\dll\screen-capture-recorder\screen-capture-recorder-x64.dll"

解除注册

解除添加/u即可。

代码语言:javascript
复制
 regsvr32 /u "D:\Tools\ffmpeg\dll\virtual-audio\audio_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)\Libs\screen-capture-recorder\* $(TargetDir)\
 xcopy  /Y /d $(ProjectDir)\Libs\virtual-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:\Users\Administrator\AppData\Local\Temp\____temp.wav"
 ​
 ffmpeg -f dshow -i audio="Realtek(R) Audio" -t 10 -y "C:\Users\Administrator\AppData\Local\Temp\____temp.wav"

扬声器

代码语言:javascript
复制
 ffmpeg -f dshow -i audio="virtual-audio-capturer" -t 10 -y "C:\Users\Administrator\AppData\Local\Temp\____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:\mp4\luping.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:\Users\Administrator\Downloads\Test\20230524095242.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;
         }
     }
 }

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • FFMPEG的弊端
  • 解决方法
    • 安装虚拟设备
      • 命令行中注册
      • 代码中注册
      • 项目中使用
      • 虚拟设备验证
    • 使用新版本
    • 安装依赖
      • Nuget添加依赖
        • 添加引用
        • 常用的命令
          • 查看音频和视频设备列表
            • 查看Dshow库支持参数
              • 视频源
                • 音频源
                  • 获取默认的麦克风
                  • 音频录制测试
                • 录制命令
                • 音频
                  • 判断麦克风是否可用
                    • 使用FFmpeg判断(推荐)
                    • 使用NAudio判断
                • 录制工具类
                  • 录制
                    • 暂停和恢复的实现
                    • 生成缩略图
                    • 获取视频时长
                    • 打开系统声音设置
                    • 调用本地播放
                    • 环境设置
                      • 判断FFmpeg是否安装
                        • 下载FFmpeg
                          • 设置环境变量
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档