Skip to content

Instantly share code, notes, and snippets.

@DashW
Last active October 30, 2024 20:05
Show Gist options
  • Save DashW/74d726293c0d3aeb53f4 to your computer and use it in GitHub Desktop.
Save DashW/74d726293c0d3aeb53f4 to your computer and use it in GitHub Desktop.
ScreenRecorder - High Performance Unity Video Capture Script
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
class BitmapEncoder
{
public static void WriteBitmap(Stream stream, int width, int height, byte[] imageData)
{
using (BinaryWriter bw = new BinaryWriter(stream)) {
// define the bitmap file header
bw.Write ((UInt16)0x4D42); // bfType;
bw.Write ((UInt32)(14 + 40 + (width * height * 4))); // bfSize;
bw.Write ((UInt16)0); // bfReserved1;
bw.Write ((UInt16)0); // bfReserved2;
bw.Write ((UInt32)14 + 40); // bfOffBits;
// define the bitmap information header
bw.Write ((UInt32)40); // biSize;
bw.Write ((Int32)width); // biWidth;
bw.Write ((Int32)height); // biHeight;
bw.Write ((UInt16)1); // biPlanes;
bw.Write ((UInt16)32); // biBitCount;
bw.Write ((UInt32)0); // biCompression;
bw.Write ((UInt32)(width * height * 4)); // biSizeImage;
bw.Write ((Int32)0); // biXPelsPerMeter;
bw.Write ((Int32)0); // biYPelsPerMeter;
bw.Write ((UInt32)0); // biClrUsed;
bw.Write ((UInt32)0); // biClrImportant;
// switch the image data from RGB to BGR
for (int imageIdx = 0; imageIdx < imageData.Length; imageIdx += 3) {
bw.Write(imageData[imageIdx + 2]);
bw.Write(imageData[imageIdx + 1]);
bw.Write(imageData[imageIdx + 0]);
bw.Write((byte)255);
}
}
}
}
/// <summary>
/// Captures frames from a Unity camera in real time
/// and writes them to disk using a background thread.
/// </summary>
///
/// <description>
/// Maximises speed and quality by reading-back raw
/// texture data with no conversion and writing
/// frames in uncompressed BMP format.
/// Created by Richard Copperwaite.
/// </description>
///
[RequireComponent(typeof(Camera))]
public class ScreenRecorder : MonoBehaviour
{
// Public Properties
public int maxFrames; // maximum number of frames you want to record in one video
public int frameRate = 30; // number of frames to capture per second
// The Encoder Thread
private Thread encoderThread;
// Texture Readback Objects
private RenderTexture tempRenderTexture;
private Texture2D tempTexture2D;
// Timing Data
private float captureFrameTime;
private float lastFrameTime;
private int frameNumber;
private int savingFrameNumber;
// Encoder Thread Shared Resources
private Queue<byte[]> frameQueue;
private string persistentDataPath;
private int screenWidth;
private int screenHeight;
private bool threadIsProcessing;
private bool terminateThreadWhenDone;
void Start ()
{
// Set target frame rate (optional)
Application.targetFrameRate = frameRate;
// Prepare the data directory
persistentDataPath = Application.persistentDataPath + "/ScreenRecorder";
print ("Capturing to: " + persistentDataPath + "/");
if (!System.IO.Directory.Exists(persistentDataPath))
{
System.IO.Directory.CreateDirectory(persistentDataPath);
}
// Prepare textures and initial values
screenWidth = GetComponent<Camera>().pixelWidth;
screenHeight = GetComponent<Camera>().pixelHeight;
tempRenderTexture = new RenderTexture(screenWidth, screenHeight, 0);
tempTexture2D = new Texture2D(screenWidth, screenHeight, TextureFormat.RGB24, false);
frameQueue = new Queue<byte[]> ();
frameNumber = 0;
savingFrameNumber = 0;
captureFrameTime = 1.0f / (float)frameRate;
lastFrameTime = Time.time;
// Kill the encoder thread if running from a previous execution
if (encoderThread != null && (threadIsProcessing || encoderThread.IsAlive)) {
threadIsProcessing = false;
encoderThread.Join();
}
// Start a new encoder thread
threadIsProcessing = true;
encoderThread = new Thread (EncodeAndSave);
encoderThread.Start ();
}
void OnDisable()
{
// Reset target frame rate
Application.targetFrameRate = -1;
// Inform thread to terminate when finished processing frames
terminateThreadWhenDone = true;
}
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (frameNumber <= maxFrames)
{
// Check if render target size has changed, if so, terminate
if(source.width != screenWidth || source.height != screenHeight)
{
threadIsProcessing = false;
this.enabled = false;
throw new UnityException("ScreenRecorder render target size has changed!");
}
// Calculate number of video frames to produce from this game frame
// Generate 'padding' frames if desired framerate is higher than actual framerate
float thisFrameTime = Time.time;
int framesToCapture = ((int)(thisFrameTime / captureFrameTime)) - ((int)(lastFrameTime / captureFrameTime));
// Capture the frame
if(framesToCapture > 0)
{
Graphics.Blit (source, tempRenderTexture);
RenderTexture.active = tempRenderTexture;
tempTexture2D.ReadPixels(new Rect(0, 0, Screen.width, Screen.height),0,0);
RenderTexture.active = null;
}
// Add the required number of copies to the queue
for(int i = 0; i < framesToCapture && frameNumber <= maxFrames; ++i)
{
frameQueue.Enqueue(tempTexture2D.GetRawTextureData());
frameNumber ++;
if(frameNumber % frameRate == 0)
{
print ("Frame " + frameNumber);
}
}
lastFrameTime = thisFrameTime;
}
else //keep making screenshots until it reaches the max frame amount
{
// Inform thread to terminate when finished processing frames
terminateThreadWhenDone = true;
// Disable script
this.enabled = false;
}
// Passthrough
Graphics.Blit (source, destination);
}
private void EncodeAndSave()
{
print ("SCREENRECORDER IO THREAD STARTED");
while (threadIsProcessing)
{
if(frameQueue.Count > 0)
{
// Generate file path
string path = persistentDataPath + "/frame" + savingFrameNumber + ".bmp";
// Dequeue the frame, encode it as a bitmap, and write it to the file
using(FileStream fileStream = new FileStream(path, FileMode.Create))
{
BitmapEncoder.WriteBitmap(fileStream, screenWidth, screenHeight, frameQueue.Dequeue());
fileStream.Close();
}
// Done
savingFrameNumber ++;
print ("Saved " + savingFrameNumber + " frames. " + frameQueue.Count + " frames remaining.");
}
else
{
if(terminateThreadWhenDone)
{
break;
}
Thread.Sleep(1);
}
}
terminateThreadWhenDone = false;
threadIsProcessing = false;
print ("SCREENRECORDER IO THREAD FINISHED");
}
}
@d-oleksy
Copy link

I had the same wish... extend the OnRenderImage event by an iterator over inactive cameras (if you want to record more than one). You can use something like this to capture images from inactive cameras:

            foreach (Camera inactiveCam in inactiveCameras)
            {
                int resWidth = inactiveCam.pixelWidth;
                int resHeight = inactiveCam.pixelHeight;
                RenderTexture rt = RenderTexture.GetTemporary(resWidth, resHeight, 24);
                inactiveCam.targetTexture = rt;
                inactiveCam.Render();
                Texture2D ss = new Texture2D(resWidth, resHeight, TextureFormat.RGB24, false);
                RenderTexture.active = rt;
                ss.ReadPixels(inactiveCam.pixelRect, 0, 0);
                ss.Apply();
                queues[inactiveCam.name].Enqueue(ss.EncodeToPNG());
                inactiveCam.targetTexture = null;
                RenderTexture.active = null;
                rt.Release();
            }

Don't forget to extend the encoder as well

@colley-repos
Copy link

I would recommend Unity's Recorder, which is totally free on the Asset Store:
https://assetstore.unity.com/packages/essentials/unity-recorder-94079

As someone else said, that official solution is only usable in Editor, it uses the media encoder supplied in the UnityEditor.Media namespace which is not included in build.

@ksyao2002
Copy link

Can this script be used in android or ios build?

@md84419
Copy link

md84419 commented Feb 26, 2021

Unity Recorder is no longer available on the asset store.

@RWOverdijk
Copy link

Unity Recorder is no longer available on the asset store.

It's a package now and you can find it in package manager.

@makakaorg
Copy link

Unity Recorder is no longer available on the asset store.

https://docs.unity3d.com/Packages/com.unity.recorder@latest

@pearcircuitmike
Copy link

Would this work to capture audio with fmod? Unity recorder doesn't work with Fmod enabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment