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

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