Unity photography and screen recording development exploration

1. Take photos

This can be achieved using Unity’s RenderTexture and Texture2D classes, which require a camera to take pictures, and then save the camera’s rendering results to the Texture by creating a RenderTexture. Finally convert this Texture to a regular 2D texture and save it as an image file.

public class PhotoShootHandler : MonoBehaviour
{
    [Tooltip("Camera used to render the scene. If not set, the main camera will be used.")]
    public Camera foregroundCamera;
    [Tooltip("Array of sprite transforms that will be used for displaying the countdown until image shot.")]
    public Transform[] countdown;
    [Tooltip("UI-Text used to display information messages.")]
    public UnityEngine.UI.Text infoText;
    private IEnumerator currentRoutine = null,currentPhotoRoutine = null;
    /// <summary>
    /// Check if taking pictures
    /// </summary>
    public bool IsInProgress()
    {
        return (currentRoutine != null);
    }
    /// <summary>
    /// Start taking pictures
    /// </summary>
    public void CountdownAndMakePhoto()
    {
        if (currentRoutine != null)
            return;
        currentRoutine = CoCountdownAndMakePhoto();
        StartCoroutine(currentRoutine);
    }
    private IEnumerator CoCountdownAndMakePhoto()
    {
        if (countdown != null & amp; & amp; countdown.Length > 0)
        {
            for (int i = 0; i < countdown.Length; i + + )
            {
                if (countdown[i])
                    countdown[i].gameObject.SetActive(true);
                yield return new WaitForSeconds(1f);
                if (countdown[i])
                    countdown[i].gameObject.SetActive(false);
            }
        }
        MakePhoto();
        yield return null;
        currentRoutine = null;
    }
    public void MakePhoto()
    {
        MakePhoto(true);
    }
    public string MakePhoto(bool openIt)
    {
        int resWidth = Screen.width;
        int resHeight = Screen.height;
        Texture2D screenShot = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false);
        RenderTexture rt = new RenderTexture(resWidth, resHeight, 24);
        if(infoText)
        {
            infoText.text = string.Empty;
        }
        if (!foregroundCamera)
        {
            foregroundCamera = Camera.main;
        }
        // render
        if (foregroundCamera & amp; & amp; foregroundCamera.enabled)
        {
            foregroundCamera.targetTexture = rt;
            foregroundCamera.Render();
            foregroundCamera.targetTexture = null;
        }
        //Get the picture, RenderTexture.active is currently the active rendering texture
        RenderTexture prevActiveTex = RenderTexture.active;
        RenderTexture.active = rt;
        screenShot.ReadPixels(new Rect(0, 0, resWidth, resHeight), 0, 0);
        //reduction
        RenderTexture.active = prevActiveTex;
        Destroy(rt);
        //byte[] btScreenShot = screenShot.EncodeToJPG();
        byte[] btScreenShot = screenShot.EncodeToPNG();
        Destroy(screenShot);
        // save
        string sDirName = Application.persistentDataPath + "/Screenshots";
        if (!Directory.Exists(sDirName))
            Directory.CreateDirectory(sDirName);
        string fileName = string.Format("{0:F0}", Time.realtimeSinceStartup * 10f) + ".png";
        string sFileName = sDirName + "/" + fileName;
        File.WriteAllBytes(sFileName, btScreenShot);
        Debug.Log("Photo saved to: " + sFileName);
        if(infoText)
        {
            infoText.text = "Saved to: " + sFileName;
        }
        //Open i file for display
        /*if (openIt)
        {
            System.Diagnostics.Process.Start(sFileName);
        }*/
        //upload photos
        UploadRecordPhoto(fileName);
        return sFileName;
    }
    /// <summary>
    /// Upload screen recording pictures
    /// </summary>
    /// <returns></returns>
    private void UploadRecordPhoto(string fileName)
    {
        if (currentPhotoRoutine != null)
            return;
        currentPhotoRoutine = DoUploadRecordVideo(fileName);
        StartCoroutine(currentPhotoRoutine);
    }
    private IEnumerator DoUploadRecordVideo(string fileName)
    {
        yield return new WaitForSeconds(1.0f);
        string filePath = Application.persistentDataPath + "/Screenshots" + "/" + fileName;
        if (!IsOccupied(filePath, "Upload Photo"))
        {
            print("Press photo file");
            HttpUploadHandler.Instance.PushImageInQueue(fileName);
            currentPhotoRoutine = null;
        }
        else
        {
            currentPhotoRoutine = null;
            UploadRecordPhoto(fileName);
        }
    }
    #region tools
    /// <summary>
    /// Determine whether the file is occupied
    /// </summary>
    /// <param name="filePath"></param>
    /// <returns></returns>
    public bool IsOccupied(string filePath, string fileFrom)
    {
        FileStream stream = null;
        try
        {
            stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None);
            return false;
        }
        catch
        {
            print(fileFrom + "File is occupied");
            return true;
        }
        finally
        {
            if (stream != null)
            {
                stream.Close();
            }
        }
    }
    #endregion
}

2. Record current interface video (applicable to PC and mobile terminals)

1. Overview:

Unity can use Unity Recorder to record videos, but it can only be used in the editor. Unity scenes and animations can be recorded in running mode.
Timeline is recorded as animation or video.

For non-editor conditions, we are currently exploring the use of ffmpeg to implement it, and it works normally.

2. ffmpeg parameter description

* -f: format

* gdigrab: ffmpeg’s built-in method for grabbing the Windows desktop, supports grabbing windows with specified names

* dshow: relies on the third-party software Screen Capture Recorder (hereinafter referred to as SCR)

* -i: input source

* title: The name of the window to be recorded, only used in GDIGRAB mode

* video: video playback hardware name or “screen-capture-recorder”, the latter relies on SCR

* audio: audio playback hardware name or “virtual-audio-capturer”, the latter relies on SCR

* -preset ultrafast: Encode at the fastest speed, resulting in large video files

* -c:v: video encoding method

* -c:a: audio encoding method

* -b:v: video bitrate

* -r: video frame rate

* -s: video resolution

* -y: The output file overwrites the existing file without prompting

*

*FFMPEG official document:
ffmpeg Documentation

* Screen Capture Recorder home page:
https://github.com/rdp/screen-capture-recorder-to-video-windows-free

3. Implementation code

using Newtonsoft.Json.Serialization;
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using UnityEngine;

[ExecuteInEditMode]
public class VideoRecoderHandler : MonoBehaviour
{
    #region DLL needed to simulate console signals
    [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 settings menu
    public enum RecordType
    {
        GDIGRAB,
        DSHOW
    }
    public enum Bitrate
    {
        _1000k,
        _1500k,
        _2000k,
        _2500k,
        _3000k,
        _3500k,
        _4000k,
        _5000k,
        _8000k
    }
    public enum Framerate
    {
        _14,
        _twenty four,
        _30,
        _45,
        _60
    }
    public enum Resolution
    {
        _1280x720,
        _1920x1080,
        _1080x1920,
        _Auto
    }
    public enum OutputPath
    {
        Desktop,
        StreamingAsset,
        DataPath,
        PersistentDataPath,
        Custom
    }
    #endregion
    #region members
    [Tooltip("If Debug is enabled, the CMD window will be displayed, otherwise it will not be displayed.")]
    [SerializeField]
    private bool _debug = false;
    [Tooltip("DSHOW - Record full screen \
GUIGRAB - Record game window (release version only)")]
    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 bool IsRecording { get { return _isRecording; } }
    // Parameters: window name -b bitrate -r frame rate -s resolution file path file name
    private const string FFARGS_GDIGRAB = "-f gdigrab -i title={0} -f dshow -i audio="virtual-audio-capturer" -y -preset ultrafast -rtbufsize 3500k -pix_fmt yuv420p -b:v {1} -r {2} -s {3} {4}/{5}.mp4";
    //private const string FFARGS_GDIGRAB = "-f gdigrab -i title={0} -f dshow -i audio="virtual-audio-capturer" -f dshow -i video="screen-capture-recorder" -y -preset ultrafast -rtbufsize 3500k -pix_fmt yuv420p -b:v {1} -r {2} -s {3} {4}/{5}.mp4";
    // Parameters: -b bitrate -r frame rate -s resolution file path file name
    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 _ffpath;
    private string _ffargs;
    private int _pid;
    private bool _isRecording = false;
    private string curVideoName;
    private IEnumerator currentVideoRoutine = null;


    #endregion
#if !UNITY_EDITOR & amp; & amp; !DEVELOPMENT_BUILD
    private void Start()
    {
        _debug = false;
    }
#endif
#if UNITY_EDITOR || DEVELOPMENT_BUILD
    private void OnGUI()
    {
        /*if (GUILayout.Button("Start")) StartRecording();
        if (GUILayout.Button("Stop")) StopRecording(() => { UnityEngine.Debug.Log("End recording."); });*/
    }
#endif
#if UNITY_EDITOR
    private void OnValidate()
    {
        if (_debug) UnityEngine.Debug.Log("FFRecorder - CMD window is enabled.");
        else UnityEngine.Debug.Log("FFRecorder - CMD window is disabled.");
        if (recordType == RecordType.GDIGRAB)
        {
            UnityEngine.Debug.Log("FFRecorder - Use [GDIGRAB] mode to record the current window.");
            UnityEngine.Debug.LogError("FFRecorder - [GDIGRAB] mode is not available in the editor.");
        }
        else if (recordType == RecordType.DSHOW)
        {
            UnityEngine.Debug.Log("FFRecorder - Use [DSHOW] mode to record full screen.");
        }
    }
#endif
    public void StartRecording()
    {
        if (_isRecording)
        {
            UnityEngine.Debug.LogError("FFRecorder::StartRecording - There is currently a recording process.");
            return;
        }
        // Kill the existing ffmpeg process, do not add the .exe suffix
        Process[] goDie = Process.GetProcessesByName("ffmpeg");
        foreach (Process p in goDie) p.Kill();
        // Parse the settings, if the settings are correct, start recording
        bool validSettings = ParseSettings();
        if(validSettings)
        {
            UnityEngine.Debug.Log("FFRecorder::StartRecording - Execute command: " + _ffpath + " " + _ffargs);
            StartCoroutine(IERecording());
        }
        else
        {
            UnityEngine.Debug.LogError("FFRecorder::StartRecording - improper setting, recording canceled, please check the console output.");
        }
    }
    public void StopRecording(Action _OnStopRecording)
    {
        if (!_isRecording)
        {
            UnityEngine.Debug.LogError("FFRecorder::StopRecording - There is currently no recording process and the operation has been canceled.");
            return;
        }
        StartCoroutine(IEExitCmd(_OnStopRecording));
    }


    public void FocusStopRecording(Action _OnStopRecording)
    {
        if (_isRecording)
        {
            try
            {
                UnityEngine.Debug.LogError("Forced cancellation - the recording process ends abnormally and the output file may not be played.");
                Process.GetProcessById(_pid).Kill();


                _isRecording = false;
            }
            catch (Exception e)
            {
                UnityEngine.Debug.LogError("FFRecorder::OnDestroy - " + e.Message);
            }
        }
        if (_OnStopRecording != null)
        {
            _OnStopRecording();
        }
    }


    private bool ParseSettings()
    {
        _ffpath = Application.streamingAssetsPath + @"/ffmpeg/ffmpeg.exe";
        string name = Application.productName + "_" + DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss");
        curVideoName = name + ".mp4";
        // resolution
        string s;
        if (resolution == Resolution._1280x720)
        {
            int w = 1280;
            int h = 720;
            if (Screen.width < w)
            {
                w = Screen.width;
                UnityEngine.Debug.LogWarning(string.Format("The recording horizontal resolution is greater than the screen horizontal resolution and has been automatically reduced to {0}.", w));
            }
            if (Screen.height < h)
            {
                h = Screen.height;
                UnityEngine.Debug.LogWarning(string.Format("The recording vertical resolution is greater than the screen vertical resolution and has been automatically reduced to {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("The recording horizontal resolution is greater than the screen horizontal resolution and has been automatically reduced to {0}.", w));
            }
            if (Screen.height < h)
            {
                h = Screen.height;
                UnityEngine.Debug.LogWarning(string.Format("The recording vertical resolution is greater than the screen vertical resolution and has been automatically reduced to {0}.", h));
            }
            s = w.ToString() + "x" + h.ToString();
        }
        else /*(resolution == Resolution._Auto)*/
        {
            s = Screen.width.ToString() + "x" + Screen.height.ToString();
        }
        // frame rate
        string r = framerate.ToString().Remove(0, 1);
        //bitrate
        string b = bitrate.ToString().Remove(0, 1);
        //output position
        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 if (outputPath == OutputPath.PersistentDataPath) output = Application.persistentDataPath + "/" + Application.productName + "_Records";
        else /*(outputPath == OutputPath.Custom)*/ output = customOutputPath;


        //Command line parameters
        if (recordType == RecordType.GDIGRAB)
        {
            _ffargs = string.Format(FFARGS_GDIGRAB, Application.productName, b, r, s, output, name);
        }
        else /*(recordType == RecordType.DSHOW)*/
        {
            _ffargs = string.Format(FFARGS_DSHOW, b, r, s, output, name);
        }
        //Create output folder
        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;
    }
    // It is not necessary to use coroutines
    private IEnumerator IERecording()
    {
        yield return null;
        Process ffp = new Process();
        ffp.StartInfo.FileName = _ffpath; // Process executable file location
        ffp.StartInfo.Arguments = _ffargs; // Command line parameters passed to the executable file
        ffp.StartInfo.CreateNoWindow = !_debug; // Whether to display the console window
        ffp.StartInfo.UseShellExecute = _debug; // Whether to use the operating system Shell program to start the process
        ffp.Start(); // Start the process
        _pid = ffp.Id;
        _isRecording = true;
    }
    private IEnumerator IEExitCmd(Action _OnStopRecording)
    {
        // Attach the current process to the console of the pid process
        AttachConsole(_pid);
        //Set the console event handler to Zero, that is, the current process does not respond to console events
        // Avoid ending the current process when sending the [Ctrl C] command to the console
        SetConsoleCtrlHandler(IntPtr.Zero, true);
        //Send [Ctrl C] end command to the console
        // ffmpeg will receive this command to stop recording
        GenerateConsoleCtrlEvent(0, 0);
        // ffmpeg cannot be stopped immediately, wait for a while, otherwise the video cannot be played
        yield return new WaitForSeconds(3.0f);
        // Uninstall the console event handler, otherwise subsequent ffmpeg calls cannot stop normally.
        SetConsoleCtrlHandler(IntPtr.Zero, false);
        // Strip the attached console
        FreeConsole();
        _isRecording = false;
        if (_OnStopRecording != null)
        {
            _OnStopRecording();
        }
        UploadRecordVideo();
    }


    private IEnumerator FocusIEExitCmd(Action _OnStopRecording)
    {
        // Attach the current process to the console of the pid process
        AttachConsole(_pid);
        //Set the console event handler to Zero, that is, the current process does not respond to console events
        // Avoid ending the current process when sending the [Ctrl C] command to the console
        SetConsoleCtrlHandler(IntPtr.Zero, true);
        //Send [Ctrl C] end command to the console
        // ffmpeg will receive this command to stop recording
        GenerateConsoleCtrlEvent(0, 0);
        // ffmpeg cannot be stopped immediately, wait for a while, otherwise the video cannot be played
        yield return new WaitForSeconds(3.0f);
        // Uninstall the console event handler, otherwise subsequent ffmpeg calls cannot stop normally.
        SetConsoleCtrlHandler(IntPtr.Zero, false);
        // Strip the attached console
        FreeConsole();
        _isRecording = false;
        if (_OnStopRecording != null)
        {
            _OnStopRecording();
        }


        string filePath = Application.persistentDataPath + "/" + Application.productName + "_Records" + "/" + curVideoName;
        StartCoroutine(DeleteVideoFile(filePath));
    }


    /// <summary>
    /// Upload screen recording video
    /// </summary>
    /// <returns></returns>
    private void UploadRecordVideo()
    {
        if (currentVideoRoutine != null)
            return;
        currentVideoRoutine = DoUploadRecordVideo();
        StartCoroutine(currentVideoRoutine);
    }


    private IEnumerator DoUploadRecordVideo()
    {
        yield return new WaitForSeconds(1.0f);
        string filePath = Application.persistentDataPath + "/" + Application.productName + "_Records" + "/" + curVideoName;


        if (!IsOccupied(filePath, "Upload video"))
        {
            print("Press video file");
            currentVideoRoutine = null;
        }
        else
        {
            currentVideoRoutine = null;
            UploadRecordVideo();
        }
    }


    // When the program ends, the background recording process must be killed, but this will cause the output file to be unable to be played.
    public void OnDestroy()
    {
        if (_isRecording)
        {
            try
            {
                UnityEngine.Debug.LogError("FFRecorder::OnDestroy - The recording process ended abnormally and the output file may not be played.");
                Process.GetProcessById(_pid).Kill();


                _isRecording = false;
            }
            catch (Exception e)
            {
                UnityEngine.Debug.LogError("FFRecorder::OnDestroy - " + e.Message);
            }
        }
    }


    /// <summary>
    /// Delete video files
    /// </summary>
    /// <param name="filePath"></param>
    /// <returns></returns>
    private IEnumerator DeleteVideoFile(string filePath)
    {
        yield return new WaitForSeconds(1.0f);
        if (!IsOccupied(filePath, "Delete video"))
        {
            File.Delete(filePath);
            UnityEngine.Debug.Log("Cleaning up abnormal video completed!");
        }
        else
        {
            UnityEngine.Debug.Log("Failed to delete abnormal video");
            //StartCoroutine(DeleteVideoFile(filePath));
        }
    }


    #region tools
    /// <summary>
    /// Determine whether the file is occupied
    /// </summary>
    /// <param name="filePath"></param>
    /// <returns></returns>
    public bool IsOccupied(string filePath, string fileFrom)
    {
        FileStream stream = null;
        try
        {
            stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None);
            return false;
        }
        catch
        {
            print(fileFrom + "File is occupied");
            return true;
        }
        finally
        {
            if (stream != null)
            {
                stream.Close();
            }
        }
    }
    #endregion
}