Instant Replay is a Unity library for saving recent gameplay footage on demand. It maintains a rolling buffer, so you can save important moments even after they happen. Recording is limited to a preconfigured duration, and frames older than that limit are discarded.
You can implement a feature that allows users to share their recent gameplay footage on social media.
When a bug occurs, you can export the recent gameplay leading up to the bug as a video, which can help reproduce it.
- Instant Replay for Unity
- Unity 2022.3 or later
Note
The following information is based on the APIs and platform tools being used, and actual functionality may not have been verified.
| Platform | OS version | aarch64 | x86_64 | Other requirements |
|---|---|---|---|---|
| iOS | 10.0+ | ✅ | N/A | |
| Android | 8.0+ | ✅ | ✅ | |
| macOS | 11.0+ | ✅ | ✅ | |
| Windows | Windows 10+, Windows Server 2016+ | - | ✅ | |
| Linux | kernel 3.2+, glibc 2.17+ | - | ✅ | ffmpeg in PATH |
| Web | (any) | (any) | (any) | Browser supports WebCodecs |
- For legacy mode, other platforms may work if
ffmpegis available in PATH.
Warning
Known Issue with WebGL: In WebGL, flickering may occur on the screen during recording. This is caused by ScreenshotFrameProvider, which is the default IFrameProvider implementation. If you encounter this issue, please use BuiltinCameraFrameProvider (for Built-in RP), RendererFeatureFrameProvider (for Universal RP), or another custom IFrameProvider implementation that provides input RenderTexture directly.
| Platform | APIs |
|---|---|
| iOS / macOS | Video Toolbox (H.264), Audio Toolbox (AAC) |
| Android | MediaCodec (H.264 / AAC) |
| Windows | Media Foundation (H.264 / AAC) |
| Linux | FFmpeg installed on the system (H.264 / AAC) |
| Web | WebCodecs (avc1.640028 for video, mp4a.40.2 for audio) |
You can install the dependencies using either of the following methods.
Add UnityNuGet scoped registry and add the following git URL to the Package Manager:
https://github.com/CyberAgentGameEntertainment/InstantReplay.git?path=/Packages/jp.co.cyberagent.instant-replay.dependencies#release
Install individual packages using NuGetForUnity or UnityNuGet:
Add the following git URL to the Package Manager:
https://github.com/CyberAgentGameEntertainment/InstantReplay.git?path=Packages/jp.co.cyberagent.instant-replay#release
Import the "User Interfaces" sample from the Package Manager.
Place InstantReplay Recorder.prefab in the scene. This prefab contains RecorderInterface and PersistentRecorder components, which automatically record the gameplay while enabled.
Then, you can stop the recording and save the video by calling RecorderInterface.StopAndExport(). For example, you can trigger this method by clicking the button in the scene.
The recorded video will play on the screen.
To record gameplay from code, use RealtimeInstantReplaySession as shown below.
using InstantReplay;
var ct = destroyCancellationToken;
// Start recording
using var session = RealtimeInstantReplaySession.CreateDefault();
// 〜 Gameplay 〜
await Task.Delay(10000, ct);
// Stop recording and transcode
var outputPath = await session.StopAndExportAsync();
File.Move(outputPath, Path.Combine(Application.persistentDataPath, Path.GetFileName(outputPath)));Recording uses memory in two places: buffers for compressed output, and buffers for raw frames and audio samples awaiting encoding.
MaxMemoryUsageBytesForCompressedFrames controls the recording duration. By default, recording holds up to 20 MiB of compressed data; when the total size of compressed frames and audio samples reaches this limit, older data is discarded. To enable longer recordings, increase this value or reduce the frame rate, resolution, or bitrate.
VideoInputQueueSize, AudioInputQueueSizeSeconds, and MaxNumberOfRawFrameBuffers (optional) control how many raw frames and audio samples are queued for encoding. These queues are needed because the encoder runs asynchronously, receiving the next frame while encoding the current one. Reducing these values decreases memory usage but may increase the likelihood of dropped frames.
IdrIntervalSeconds sets the maximum interval between IDR (key) frames. An export can only begin at a key frame, so this bounds how closely StopAndExportAsync can match the requested duration. null applies VideoEncoderOptions.DefaultIdrIntervalSeconds, which is one second, rather than deferring to the platform encoder, whose own default differs per platform and may be considerably longer. A longer interval spends less of the bitrate on key frames, but an interval longer than the retained duration can leave the buffer with no key frame to start from, in which case the export produces nothing.
// Default settings
var options = new RealtimeEncodingOptions
{
VideoOptions = new VideoEncoderOptions
{
Width = 1280,
Height = 720,
FpsHint = 30,
Bitrate = 2500000, // 2.5 Mbps
IdrIntervalSeconds = 1f // Max interval between IDR (key) frames in seconds. null applies the default of 1 second.
},
AudioOptions = new AudioEncoderOptions
{
SampleRate = 44100,
Channels = 2,
Bitrate = 128000 // 128 kbps
},
MaxNumberOfRawFrameBuffers = 2, // (Optional) Max number of buffers to store frames to be encoded. Each buffer size is VideoOptions.Width * VideoOptions.Height * 4 bytes.
MaxMemoryUsageBytesForCompressedFrames = 20 * 1024 * 1024, // 20 MiB
FixedFrameRate = 30.0, // null to use the actual rendering frame rate
VideoInputQueueSize = 5, // Maximum number of raw frames to keep before encoding
AudioInputQueueSizeSeconds = 1.0 // Max queued audio input duration to be buffered before encoding, in seconds
};
using var session = new RealtimeInstantReplaySession(options);You can pause and resume the recording using RealtimeInstantReplaySession.Pause() and RealtimeInstantReplaySession.Resume().
You can use a custom video source by implementing IFrameProvider.
Pass an IFrameProvider instance as frameProvider to the RealtimeInstantReplaySession constructor. You can also specify whether RealtimeInstantReplaySession automatically disposes frameProvider via the disposeFrameProvider parameter.
new RealtimeInstantReplaySession(options, frameProvider: new ScreenshotFrameProvider(), disposeFrameProvider: true);ScreenshotFrameProvider: This is the defaultIFrameProviderimplementation. It usesScreenCapture.CaptureScreenshotIntoRenderTexture(), which allows it to capture the entire screen, including overlay canvases that are not rendered by a specific camera. However, it increases GPU memory usage due to the additional RenderTexture used for capturing.BuiltinCameraFrameProvider: Captures the footage of a specific camera usingOnRenderImage()in Built-in Render Pipeline.RendererFeatureFrameProvider: Captures the footage of a specific camera using Renderer Feature in Universal Render Pipeline. You need to addInstantReplayFrameRendererFeatureto the Renderer used by the camera.
Create a class that inherits InstantReplay.IFrameProvider.
public interface IFrameProvider : IDisposable
{
public delegate void ProvideFrame(Frame frame);
event ProvideFrame OnFrameProvided;
}
new RealtimeInstantReplaySession(options, frameProvider: new CustomFrameProvider(), disposeFrameProvider: true);By default, RealtimeInstantReplaySession captures the audio via OnAudioFilterRead. This automatically searches for and uses a specific AudioListener in the scene.
Warning
AudioSources with Bypass Listener Effects enabled will not be captured.
If there are multiple AudioListeners in the scene, create a UnityAudioSampleProvider with the one you want to use. Then pass it to the RealtimeInstantReplaySession constructor via the audioSampleProvider parameter.
new RealtimeInstantReplaySession(options, audioSampleProvider: new UnityAudioSampleProvider(audioListener), disposeAudioSampleProvider: true);If you want to disable the audio, you can use NullAudioSampleProvider.Instance.
new RealtimeInstantReplaySession(options, audioSampleProvider: NullAudioSampleProvider.Instance);Note
You don't need to dispose NullAudioSampleProvider.Instance because it is a shared singleton.
You can also use your own audio source by implementing IAudioSampleProvider.
public interface IAudioSampleProvider : IDisposable
{
public delegate void ProvideAudioSamples(ReadOnlySpan<float> samples, int channels, int sampleRate,
double timestamp);
event ProvideAudioSamples OnProvideAudioSamples;
}
new RealtimeInstantReplaySession(options, audioSampleProvider: new CustomAudioSampleProvider(), disposeAudioSampleProvider: true);InstantReplay provides CriAudioSampleProvider, an IAudioSampleProvider implementation that captures audio from CRIWARE.
- Install CRIWARE Unity Plug-in
- Add scripting define symbol
INSTANTREPLAY_CRIin Player Settings - Add
InstantReplay.Criassembly reference if necessary - Use
InstantReplay.Cri.CriAudioSampleProviderasaudioSampleProviderinRealtimeInstantReplaySessionconstructor
Wwise is also supported via InstantReplay.Wwise.WwiseAudioSampleProvider.
- Install Wwise Unity Integration
- Add scripting define symbol
INSTANTREPLAY_WWISEin Player Settings - Add
InstantReplay.Wwiseassembly reference if necessary - Use
InstantReplay.Wwise.WwiseAudioSampleProviderasaudioSampleProviderinRealtimeInstantReplaySessionconstructor
You can get the recording state with the RealtimeInstantReplaySession.State property.
By default, RealtimeInstantReplaySession holds encoded frames in memory. Assigning RealtimeEncodingOptions.DiskBuffer writes them to segment files on disk instead. This lowers memory pressure, and it leaves the footage that preceded an abnormal termination on disk, where a later run of the application can recover it.
Warning
Recording continuously to storage shortens the lifespan of flash memory. This is intended primarily for development and quality-assurance builds rather than for builds shipped to end users.
using InstantReplay;
var options = RealtimeEncodingOptions.Default;
options.DiskBuffer = DiskBufferOptions.Default;
using var session = new RealtimeInstantReplaySession(options);MaxMemoryUsageBytesForCompressedFrames is not used while the disk buffer is enabled. DiskBufferOptions.MaxDiskUsageBytes bounds the retained data instead.
Each session writes into its own directory. A session that is disposed normally deletes its directory unless RetainOnDispose is set, so a directory still present on the next run denotes a session that did not end normally.
using InstantReplay;
foreach (var recovery in DiskEncodedFrameBufferRecovery.FindRecoverable())
{
if (!recovery.IsCompatible) continue;
var path = await recovery.ExportAsync(durationSeconds: 30);
Debug.Log($"Recovered {path} (started at {recovery.StartedAtUtc:u}, {recovery.SizeBytes} bytes)");
recovery.Delete();
}FindRecoverable enumerates every recoverable session below the given root directory, or below the directory the recorder uses by default when none is passed. Several may be present when the application has terminated abnormally more than once, and the caller decides which to export and which to discard. TryGetRecoverable reads a single session directory when its path is already known.
Recovery never deletes anything implicitly, because a session left behind by a crash is the only copy of the footage that preceded it. Call Delete() once the exported file has been dealt with.
IsCompatible compares the platform and package version recorded in the manifest against the running application. It is necessary but not sufficient: the payloads are serialized by the native library, whose schema may change between package versions, and a mismatch of that kind surfaces as a failure from ExportAsync instead.
| Property | Default | Description |
|---|---|---|
Directory |
null |
Directory holding the session directories. null or empty uses Application.temporaryCachePath/InstantReplay/DiskBuffer, available as DiskBufferOptions.GetDefaultDirectory(). |
MaxDiskUsageBytes |
256 MiB | Upper bound of one session directory, covering the manifest, the codec configuration, and every segment. Must be at least DiskBufferOptions.MinimumDiskUsageBytes (4 MiB). |
SegmentDuration |
5.0 | Target duration of one segment file, in seconds. |
MaxSegmentBytes |
8 MiB | Upper bound of one segment file. Must not exceed MaxDiskUsageBytes. |
MaxPendingWriteBytes |
4 MiB | Upper bound of the payload waiting in the write queue. Frames arriving while it is full are dropped rather than blocking the encoder. |
RetainOnDispose |
false |
Whether the session directory is kept when the session is disposed normally. |
SyncMode |
OperatingSystem |
Flush policy. See below. |
MaxDiskUsageBytes is a bound rather than a target. Space is reserved before each record is written, and the reservation deletes as many of the oldest segments as it needs, so the directory never exceeds the bound at any instant. When the bound cannot be met even after every evictable segment has been deleted, records are dropped rather than written; a value close to MaxSegmentBytes therefore degrades the recording rather than the retention. Segments are closed at video key frames, so discarding one never leaves a partial group of pictures behind.
SyncMode selects which failure mode to defend against. DiskBufferSyncMode.OperatingSystem, the default, hands data to the operating system after every batch and flushes it to the storage device when a segment is closed. Recorded frames survive a process crash — a native fault, an out-of-memory kill, or an abort — which is what crash recovery targets, and this costs no additional device writes. Power loss or a kernel panic loses at most the records written since the current segment was opened. DiskBufferSyncMode.EveryRecord flushes every record to the device, which survives power loss as well, at the cost of one device flush per frame. It markedly increases wear on flash memory and is intended for diagnosing storage-layer problems rather than for routine use.
UnboundedRecordingSession writes encoded data directly to an MP4 file on disk without keeping it in memory, enabling unbounded recording limited only by available disk space. Apart from the required output file path in the constructor, UnboundedRecordingSession is used similarly to RealtimeInstantReplaySession.
Warning
If the app goes to the background during recording, the recording may stop and the recorded file may become corrupted. It is recommended to complete the recording before the app goes to the background.
using InstantReplay;
var ct = destroyCancellationToken;
// Start recording
using var session = new UnboundedRecordingSession("out.mp4", RealtimeEncodingOptions.Default);
// 〜 Gameplay 〜
await Task.Delay(10000, ct);
// Stop recording and export
await session.CompleteAsync();By default, RealtimeInstantReplaySession encodes video and audio samples in real-time, but legacy InstantReplaySession saves JPEG-compressed video frames and raw audio samples to disk and transcodes them to a video file when StopAndTranscodeAsync is called. While this mode has higher disk I/O, it reduces the CPU load during recording.
using InstantReplay;
var ct = destroyCancellationToken;
// Start recording
using var session = new InstantReplaySession(numFrames: 900, fixedFrameRate: 30);
// 〜 Gameplay 〜
await Task.Delay(10000, ct);
// Stop recording and transcode
var outputPath = await session.StopAndTranscodeAsync(ct: ct);
File.Move(outputPath, Path.Combine(Application.persistentDataPath, Path.GetFileName(outputPath)));You can specify numFrames and fixedFrameRate in the InstantReplaySession constructor.
new InstantReplaySession(numFrames: 900, fixedFrameRate: 30);If you set fixedFrameRate to null, the actual frame rate will be used.
When the number of frames exceeds numFrames, the oldest frames are discarded. The disk usage during recording increases in proportion to numFrames, so set it to an appropriate size.
By default, recordings use the actual screen size. You can cap the output resolution with maxWidth and maxHeight in the InstantReplaySession constructor. Reducing the size lowers disk usage, write time, and memory usage during recording.
InstantReplaySession also supports custom video and audio sources in the same way as RealtimeInstantReplaySession.
If you are using InstantReplay as part of your bug reporting workflow, you should exclude script and plugin files in your release builds.
You can exclude all library scripts by adding EXCLUDE_INSTANTREPLAY to the Scripting Define Symbols in the Player Settings. To exclude your own code from the release builds, wrap it in #if !EXCLUDE_INSTANTREPLAY.
For the licenses of the dependencies used, please refer to THIRD-PARTY-NOTICES.md.
