Unity錄屏

一、準備工作

使用到的插件: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插件下載鏈接:

https://download.csdn.net/download/hw140701/10786951

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章