一、準備工作
使用到的插件:ffmpeg 和 ScreenCapturerRecorder。
ffmpeg 錄屏的核心,ScreenCapturerRecorder是對ffmpeg的擴充插件。
ffmpeg 放在工程的StreamingAssets中,如圖:
ScreenCapturerRecorder直接安裝就行,裝完桌面會有一個這樣的圖標:
弄好這兩個東西基本就準備好了。
二、功能邏輯
調用ffmpeg的邏輯,是使用命令行的方式,具體可參考文章:https://blog.csdn.net/qq_21397217/article/details/80537263
// 參數:-b比特率 -r幀率 -s分辨率 文件路徑 文件名
//private const string FFARGS_DSHOW = "-f dshow -i video=\"screen-capture-recorder\" -f dshow -i audio=\"virtual-audio-capturer\" -y -preset ultrafast -rtbufsize 3500k -b:v {0} -r {1} -s {2} \"{3}\"/{4}.mp4";
//+麥克風
private string FFARGS_DSHOW = "-f dshow -i video=\"screen-capture-recorder\" -f dshow -i audio=\"" + GMRecordManager.audioDevice + "\" -y -preset ultrafast -rtbufsize 3500k -b:v {0} -r {1} -s {2} \"{3}\"/{4}.mp4";
screen-capture-recorder 和 screen-capture-recorder 就是virtual-audio-capturer插件對截取視頻、音頻的擴展,因爲ffmpeg 自帶抓屏只能抓取指定窗口。
GMRecordManager.audioDevice 是自定義變量,獲取的是當前接入的麥克風設備。
錄像機邏輯:
public class FFRecorder : MonoBehaviour
{
#region 模擬控制檯信號需要使用的DLL
[DllImport("kernel32.dll")]
static extern bool GenerateConsoleCtrlEvent(int dwCtrlEvent, int dwProcessGroupId);
[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(IntPtr handlerRoutine, bool add);
[DllImport("kernel32.dll")]
static extern bool AttachConsole(int dwProcessId);
[DllImport("kernel32.dll")]
static extern bool FreeConsole();
#endregion
#region 設置菜單
public enum RecordType
{
GDIGRAB,
DSHOW
}
public enum Bitrate
{
_1000k,
_1500k,
_2000k,
_2500k,
_3000k,
_3500k,
_4000k,
_5000k,
_8000k
}
public enum Framerate
{
_14,
_24,
_30,
_45,
_60
}
public enum Resolution
{
_1280x720,
_1920x1080,
_Auto
}
public enum OutputPath
{
Desktop,
StreamingAsset,
DataPath,
Custom
}
#endregion
#region 成員
[Tooltip("啓用Debug則顯示CMD窗口,否則不顯示。")]
[SerializeField]
private bool _debug = false;
[Tooltip("DSHOW - 錄製全屏 \nGUIGRAB - 錄製遊戲窗口(僅用於發佈版)")]
public RecordType recordType = RecordType.DSHOW;
public Resolution resolution = Resolution._1280x720;
public Framerate framerate = Framerate._24;
public Bitrate bitrate = Bitrate._1500k;
public OutputPath outputPath = OutputPath.Desktop;
public string customOutputPath = @"D:/Records";
public string fileName = string.Empty;
public bool IsRecording { get { return _isRecording; } }
/** ffmpeg參數說明
* -f :格式
* gdigrab :ffmpeg內置的用於抓取Windows桌面的方法,支持抓取指定名稱的窗口
* dshow :依賴於第三方軟件Screen Capture Recorder(後面簡稱SCR)
* -i :輸入源
* title :要錄製的窗口的名稱,僅用於GDIGRAB方式
* video :視頻播放硬件名稱或者"screen-capture-recorder",後者依賴SCR
* audio :音頻播放硬件名稱或者"virtual-audio-capturer",後者依賴SCR
* -preset ultrafast :以最快的速度進行編碼,生成的視頻文件大
* -c:v :視頻編碼方式
* -c:a :音頻編碼方式
* -b:v :視頻比特率
* -r :視頻幀率
* -s :視頻分辨率
* -y :輸出文件覆蓋已有文件不提示
*
* FFMPEG官方文檔:http://ffmpeg.org/ffmpeg-all.html
* Screen Capture Recorder主頁:https://github.com/rdp/screen-capture-recorder-to-video-windows-free
*/
// 參數:窗口名稱 -b比特率 -r幀率 -s分辨率 文件路徑 文件名
private const string FFARGS_GDIGRAB = "-f gdigrab -i title={0} -f dshow -i audio=\"virtual-audio-capturer\" -y -preset ultrafast -rtbufsize 3500k -b:v {1} -r {2} -s {3} \"{4}\"/{5}.mp4";
// 參數:-b比特率 -r幀率 -s分辨率 文件路徑 文件名
//private const string FFARGS_DSHOW = "-f dshow -i video=\"screen-capture-recorder\" -f dshow -i audio=\"virtual-audio-capturer\" -y -preset ultrafast -rtbufsize 3500k -b:v {0} -r {1} -s {2} \"{3}\"/{4}.mp4";
//+麥克風
private string FFARGS_DSHOW = "-f dshow -i video=\"screen-capture-recorder\" -f dshow -i audio=\"" + GMRecordManager.audioDevice + "\" -y -preset ultrafast -rtbufsize 3500k -b:v {0} -r {1} -s {2} \"{3}\"/{4}.mp4";
//private const string FFARGS_DSHOW = "-f dshow -i video=\"screen-capture-recorder\" -f dshow -i audio=\"麥克風 (Realtek(R) Audio)\" -y -preset ultrafast -rtbufsize 3500k -b:v {0} -r {1} -s {2} \"{3}\"/{4}.mp4";
private string _ffpath;
private string _ffargs;
private int _pid;
private bool _isRecording = false;
#endregion
#if UNITY_EDITOR
private void OnValidate()
{
if (_debug) UnityEngine.Debug.Log("FFRecorder - CMD窗口已啓用。");
else UnityEngine.Debug.Log("FFRecorder - CMD窗口已禁用。");
if (recordType == RecordType.GDIGRAB)
{
UnityEngine.Debug.Log("FFRecorder - 使用【GDIGRAB】模式錄製當前窗口。");
UnityEngine.Debug.LogError("FFRecorder - 【GDIGRAB】模式在編輯器中不可用。");
}
else if (recordType == RecordType.DSHOW)
{
UnityEngine.Debug.Log("FFRecorder - 使用【DSHOW】模式錄製全屏。");
}
}
#endif
public void StartRecording()
{
if (_isRecording)
{
UnityEngine.Debug.LogError("FFRecorder::StartRecording - 當前已有錄製進程。");
return;
}
// 殺死已有的ffmpeg進程,不要加.exe後綴
Process[] goDie = Process.GetProcessesByName("ffmpeg");
foreach (Process p in goDie) p.Kill();
// 解析設置,如果設置正確,則開始錄製
bool validSettings = ParseSettings();
if (validSettings)
{
UnityEngine.Debug.Log("FFRecorder::StartRecording - 執行命令:" + _ffpath + " " + _ffargs);
StartCoroutine(IERecording());
}
else
{
UnityEngine.Debug.LogError("FFRecorder::StartRecording - 設置不當,錄製取消,請檢查控制檯輸出。");
}
}
public void StopRecording(Action _OnStopRecording)
{
if (!_isRecording)
{
UnityEngine.Debug.LogError("FFRecorder::StopRecording - 當前沒有錄製進程,已取消操作。");
return;
}
StartCoroutine(IEExitCmd(_OnStopRecording));
}
private bool ParseSettings()
{
_ffpath = Application.streamingAssetsPath + "/ffmpeg/ffmpeg.exe";
if (string.IsNullOrEmpty(fileName))
{
fileName = Application.productName + "_" + DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
}
// 分辨率
string s;
if (resolution == Resolution._1280x720)
{
int w = 1280;
int h = 720;
if (Screen.width < w)
{
w = Screen.width;
UnityEngine.Debug.LogWarning(string.Format("錄製水平分辨率大於屏幕水平分辨率,已自動縮小爲{0}。", w));
}
if (Screen.height < h)
{
h = Screen.height;
UnityEngine.Debug.LogWarning(string.Format("錄製垂直分辨率大於屏幕垂直分辨率,已自動縮小爲{0}。", h));
}
s = w.ToString() + "x" + h.ToString();
}
else if (resolution == Resolution._1920x1080)
{
int w = 1920;
int h = 1080;
if (Screen.width < w)
{
w = Screen.width;
UnityEngine.Debug.LogWarning(string.Format("錄製水平分辨率大於屏幕水平分辨率,已自動縮小爲{0}。", w));
}
if (Screen.height < h)
{
h = Screen.height;
UnityEngine.Debug.LogWarning(string.Format("錄製垂直分辨率大於屏幕垂直分辨率,已自動縮小爲{0}。", h));
}
s = w.ToString() + "x" + h.ToString();
}
else /*(resolution == Resolution._Auto)*/
{
s = Screen.width.ToString() + "x" + Screen.height.ToString();
}
// 幀率
string r = framerate.ToString().Remove(0, 1);
// 比特率
string b = bitrate.ToString().Remove(0, 1);
// 輸出位置
string output;
if (outputPath == OutputPath.Desktop) output = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + "/" + Application.productName + "_Records";
else if (outputPath == OutputPath.DataPath) output = Application.dataPath + "/" + Application.productName + "_Records";
else if (outputPath == OutputPath.StreamingAsset) output = Application.streamingAssetsPath + "/" + Application.productName + "_Records";
else /*(outputPath == OutputPath.Custom)*/ output = customOutputPath;
// 命令行參數
if (recordType == RecordType.GDIGRAB)
{
_ffargs = string.Format(FFARGS_GDIGRAB, Application.productName, b, r, s, output, fileName);
}
else /*(recordType == RecordType.DSHOW)*/
{
_ffargs = string.Format(FFARGS_DSHOW, b, r, s, output, fileName);
}
// 創建輸出文件夾
if (!System.IO.Directory.Exists(output))
{
try
{
System.IO.Directory.CreateDirectory(output);
}
catch (Exception e)
{
UnityEngine.Debug.LogError("FFRecorder::ParseSettings - " + e.Message);
return false;
}
}
return true;
}
// 不一定要用協程
private IEnumerator IERecording()
{
yield return null;
Process ffp = new Process();
ffp.StartInfo.FileName = _ffpath; // 進程可執行文件位置
ffp.StartInfo.Arguments = _ffargs; // 傳給可執行文件的命令行參數
ffp.StartInfo.CreateNoWindow = !_debug; // 是否顯示控制檯窗口
ffp.StartInfo.UseShellExecute = _debug; // 是否使用操作系統Shell程序啓動進程
ffp.StartInfo.Verb = "runas";
// 開始進程
_isRecording = ffp.Start();
_pid = ffp.Id;
}
private IEnumerator IEExitCmd(Action _OnStopRecording)
{
// 將當前進程附加到pid進程的控制檯
AttachConsole(_pid);
// 將控制檯事件的處理句柄設爲Zero,即當前進程不響應控制檯事件
// 避免在向控制檯發送【Ctrl C】指令時連帶當前進程一起結束
SetConsoleCtrlHandler(IntPtr.Zero, true);
// 向控制檯發送 【Ctrl C】結束指令
// ffmpeg會收到該指令停止錄製
GenerateConsoleCtrlEvent(0, 0);
// ffmpeg不能立即停止,等待一會,否則視頻無法播放
yield return new WaitForSeconds(3.0f);
// 卸載控制檯事件的處理句柄,不然之後的ffmpeg調用無法正常停止
SetConsoleCtrlHandler(IntPtr.Zero, false);
// 剝離已附加的控制檯
FreeConsole();
_isRecording = false;
if (_OnStopRecording != null)
{
_OnStopRecording();
}
}
// 程序結束時要殺掉後臺的錄製進程,但這樣會導致輸出文件無法播放
private void OnDestroy()
{
if (_isRecording)
{
try
{
UnityEngine.Debug.LogError("FFRecorder::OnDestroy - 錄製進程非正常結束,輸出文件可能無法播放。");
Process.GetProcessById(_pid).Kill();
}
catch (Exception e)
{
UnityEngine.Debug.LogError("FFRecorder::OnDestroy - " + e.Message);
}
}
}
}
然後就是對錄像機的調用,首先開啓錄像機
public void OpenVCR(long recordID)
{
if (CreateVCR(recordingsVideoPath, recordID.ToString()))
{
if (ffRecorder != null)
{
ffRecorder.StartRecording();
}
}
}
其中 recordingsVideoPath 即爲要保存的視頻地址
CreateVCR創建一個錄像機
/// <summary>
/// 創建一個錄像機
/// </summary>
/// <param name="outputFolderPath">保存路徑</param>
/// <param name="autoFilenamePrefix">文件前綴</param>
/// <param name="type">錄像類型</param>
/// <param name="downScale">分辨率精度係數</param>
/// <param name="frameRate">幀率</param>
/// <returns></returns>
private bool CreateVCR(string outputFolderPath, string fileName, bool audioEnable = true)
{
Debug.Log("檢查錄屏條件");
if (string.IsNullOrEmpty(outputFolderPath))
{
Debug.LogWarning("請先設置保存路徑!");
return false;
}
customOutputPath = outputFolderPath + @"/";
if (!Directory.Exists(customOutputPath))
{
Directory.CreateDirectory(customOutputPath);
}
if (ffRecorder == null)
{
ffRecorder = this.gameObject.AddComponent<FFRecorder>();
ffRecorder.recordType = FFRecorder.RecordType.DSHOW;
ffRecorder.resolution = FFRecorder.Resolution._Auto;
ffRecorder.customOutputPath = customOutputPath;
}
ffRecorder.outputPath = FFRecorder.OutputPath.Custom;
ffRecorder.fileName = fileName;
ffRecorder.framerate = FFRecorder.Framerate._30;
ffRecorder.bitrate = FFRecorder.Bitrate._2500k;
Debug.Log("CreateVCR Success");
return true;
}
錄完接下來就需要關閉錄像機,注意:不能直接停掉Unity的運行
public void CloseVCR(bool bSave = false)
{
if (ffRecorder == null || !ffRecorder.IsRecording)
{
return;
}
this.bSave = bSave;
ffRecorder.StopRecording(() => {
UnityEngine.Debug.Log("CloseVCR");
if (!this.bSave)
{
File.Delete(customOutputPath + ffRecorder.fileName + ".mp4");
}
else
{
UnityEngine.Debug.Log("VCRPath: " + customOutputPath + ffRecorder.fileName + ".mp4");
}
});
}
來看下效果,這個就是使用上面方法錄出來的視頻。
三、資源分享
我寫的測試工程,大家可以拿去參考一下:
https://download.csdn.net/download/YasinXin/12095470
另附ScreenCapturerRecorder插件下載鏈接: