前言
本文只考虑在Windows下使用FFmpeg进行桌面、麦克风、扬声器同时录制的实现方式,Mac下会有些许差异。
之前的FFmpeg有很多问题,现在随着版本的更新基本上都可以解决了,可以使用在项目中。
代码示例:
https://gitee.com/psvmc/z-screen-recorder
FFMPEG的弊端
先说一下使用FFMpeg录制的弊端
- 需要引用ffmpeg.exe 文件本身比较大
- 无法实现应用内部分界面的录制
- 无法录制扬声器
- 录制桌面的是都鼠标光标闪烁
- 设备的名称如果超过31个字符的话会被截断,而若是将完整的设备名传到参数里则无法进行音频采集,只能将截断的设备名称传进去。
- 录制桌面使用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.dll
和audio_sniffer-x64.dll
命令行中注册
打开CMD窗口,执行以下命令:
代码语言:javascript复制 regsvr32 "D:Toolsffmpegdllvirtual-audioaudio_sniffer.dll"
regsvr32 "D:Toolsffmpegdllvirtual-audioaudio_sniffer-x64.dll"
注册screen-capture-recorder.dll
和screen-capture-recorder-x64.dll
注意
必须用管理员身份注册。
打开CMD窗口,执行以下命令:
代码语言:javascript复制 regsvr32 "D:Toolsffmpegdllscreen-capture-recorderscreen-capture-recorder.dll"
regsvr32 "D:Toolsffmpegdllscreen-capture-recorderscreen-capture-recorder-x64.dll"
解除注册
解除添加/u
即可。
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到该文件夹下
属性
=> 生成事件
> 生成前事件命令行
中添加
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;
}
}
}