Last active
October 30, 2024 20:05
-
-
Save DashW/74d726293c0d3aeb53f4 to your computer and use it in GitHub Desktop.
ScreenRecorder - High Performance Unity Video Capture Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
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.
Can this script be used in android or ios build?
Unity Recorder is no longer available on the asset store.
Unity Recorder is no longer available on the asset store.
It's a package now and you can find it in package manager.
Unity Recorder is no longer available on the asset store.
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
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:
Don't forget to extend the encoder as well