CORETECH ENGINEER BLOG

株式会社サイバーエージェント SGEコア技術本部 技術ブログ

Unity で直近 N 秒のプレイ動画をいつでも保存できるライブラリを公開しました

こんにちは、コア技術本部の瀬戸です。新しいライブラリ「Instant Replay for Unity」を OSS として公開しました。

Instant Replay は Unity で実行中のゲーム画面を常時録画し、任意のタイミングで動画として書き出すことができるライブラリです。事前に指定したフレーム数を上限として画面のキャプチャを行い、上限を超えたフレームは古いものから破棄します。常に直近 N フレーム分を動画として書き出せるような仕組みです。
必要なタイミングで直近のゲーム映像を遡って保存することができるため、記録したい出来事を逃さずに録画できます。

具体的には以下のような用途で利用できます。

不具合の再現手順の記録として

不具合が発生した際、その不具合が発生するまでの操作手順を動画として記録できます。画面は常時録画されているので改めて操作手順を再現する必要がなく、開発者や QA エンジニアの負担を軽減できます。

SNS共有のためのゲームプレイ動画撮影機能として

ユーザーがゲームプレイ動画を好きなタイミングでSNSに共有できる機能を実装できます。
(配布アプリでの実装は各アプリストアの規約が絡む可能性があるのでご注意ください!)

対応プラットフォーム

Unity Recorder と異なりエディタだけでなく実機での録画にも対応しているのが特長です。

インストール

詳しくは README (日本語 | English) を参照してください。

基本的な使い方

InstantReplay.InstantReplaySession クラスを使用します。

using System.IO;
using System.Threading.Tasks;
using InstantReplay;

var ct = destroyCancellationToken;

// 録画開始
using var session = new InstantReplaySession(numFrames: 900, fixedFrameRate: 30);

// 〜 ゲームプレイ 〜
await Task.Delay(10000, ct);

// 録画停止と書き出し
var outputPath = await session.StopAndTranscodeAsync(ct: ct);
File.Move(outputPath, Path.Combine(Application.persistentDataPath, Path.GetFileName(outputPath)));

入力ソースのカスタマイズ

デフォルトでは映像キャプチャは SRP と BiRP 標準の画面出力, 音声キャプチャは Unity 標準オーディオにのみ対応しています。独自のレンダリングパイプライン、外部のオーディオミドルウェアなどを使用している場合は入力ソースのカスタマイズが必要です。

映像ソースは InstantReplay.IFrameProvider、音声ソースは InstantReplay.IAudioSampleProvider をそれぞれ実装し、インスタンスInstantReplaySession のコンストラクタに渡すことで独自のソースとして使用できます。詳しくは README を参照してください。

public interface IFrameProvider : IDisposable
{
    public delegate void ProvideFrame(RenderTexture frame, double timestamp);

    event ProvideFrame OnFrameProvided;
}

public interface IAudioSampleProvider : IDisposable
{
    public delegate void ProvideAudioSamples(ReadOnlySpan<float> samples, int channels, int sampleRate,
        double timestamp);

    event ProvideAudioSamples OnProvideAudioSamples;
}

内部実装

基本的な部分はこちらの記事で紹介されている手法と同じです。

  • ScreenCapture.CaptureScreenshotIntoRenderTexture() による画面内容のキャプチャ
  • AsyncGPUReadback による RenderTexture ピクセルデータの取得
  • ImageConversion.EncodeNativeArrayToJPG() による JPEG 形式へのエンコードとファイルへの一時保存
  • 指定したタイミングでの動画のエンコード

これらに加え Instant Replay ではいくつかの点を改良しているので、そちらも紹介させていただきたいと思います。

一時保存処理のバックグラウンド化

AsyncGPUReadback はデフォルトで Unity 側で自動的に確保・解放される NativeArray<T> を利用してピクセルデータを取得しますが、これを利用すると NativeArray<T> の解放タイミングを利用者側で制御できません。上記の記事でも言及されていますが、メインスレッド上で JPEG エンコードまで完結させるか一旦別のバッファにピクセルデータをコピーする必要があり、ゲームのパフォーマンスに影響が出やすい問題があります。

AsyncGPUReadback には利用者側で確保した NativeArray<T> を使って結果を受け取れる RequestIntoNariveArray() という API があり、これを利用すればワーカースレッド上でゆっくり JPEG へのエンコードやファイルへの書き出しを行うことができるため、メインスレッドをブロックせずに済みます。Instant Replay ではこれによってゲームのパフォーマンスへの影響を最小限に抑えながらキャプチャを行っています。

低マネージドメモリ確保

録画中のメモリ確保はかなり少なくなっていて、フレームごとに発生するマネージドメモリ確保(GC.Alloc)は72バイトのみです。これはフレームの一時保存処理をスレッドプールに逃がすための ThreadPool.UnsafeQueueUserWorkItem() が確保する40バイトと、JPEG 画像を一時保存する際に作成される SafeFileHandle が確保する32バイトを合わせた数字で、それ以外のメモリ確保は全てオブジェクトプールによる再利用か NativeArray<T> による手動解放を行っています。

音声キャプチャへの対応

Unity 標準のオーディオ出力のキャプチャに対応しています。これには OnAudioFilterRead() を利用しています。

プラットフォーム API による動画エンコードの実装

Unity には実行時に動画をエンコードできる API がなく、Instant Replay では各 OS が提供する API を直接利用して動画のエンコードを行っています。

Windows

Windows では Media Foundation を利用して動画のエンコードを行うネイティブプラグインを実装しています。実装には Rust と windows-rs を利用しました。
Media Foundation では生のピクセルデータやオーディオのサンプルデータを受け取ってエンコードから多重化までを行える Sink Writer という API が用意されており、これを利用して動画ファイルを生成しています。

macOS / iOS

macOS / iOS では Video ToolboxAVFoundation を利用して動画のエンコードを行うネイティブプラグインを Swift で実装しています。

Android

Android では MediaCodec を利用して動画のエンコードを行います。Android では専用のネイティブプラグインは実装しておらず、Unity の AndroidJNI を利用して直接 MediaCodec の API を呼び出すバインディングを自動生成しています。

MediaCodec ではピクセルデータを事前に YUV 4:2:0 形式に変換する必要があり、さらに実際のメモリレイアウトがデバイスによって微妙に異なるため手動でのハンドリングが必要です。この問題はこちらの資料で詳しく説明されており、参考にさせていただきました。

「騒ゲーハイライト」について【MIXI TECH CONFERENCE 2023】 - Speaker Deck

YUV 形式とは各ピクセルを Y(輝度)成分と U(色差)・V(色差)成分に分けて保存する形式で、YUV 4:2:0 とは U と V 成分の解像度を縦横それぞれ 1/2 に縮小しデータ量を削減することを意味しますが、各成分のメモリレイアウトにはバリエーションがあります。代表的な YUV 4:2:0 のメモリレイアウトの一つに NV21 形式が知られ、これは Y 成分のみを連続で配置した後に V 成分と U 成分を交互に配置します。RGB 形式のように各成分をただ連続して配置するだけではないため、少々複雑です。

バイス上で実際のメモリレイアウトを取得する方法について、上記の資料中では MediaCodec.getInputFormat() から取得できる strideslice-height を利用する方法が紹介されていますが、Intant Replay ではより汎用的な方法として MediaCodec.getInputImage() から取得できる Image オブジェクトを利用する方法を採用しました。Image オブジェクトには YUV 各要素のバッファ上の開始位置や、水平・垂直方向のピクセル間隔の情報が含まれており、これを利用することでどのデバイスでも正しいメモリレイアウトが取得できているはずです。

パイプライン処理

Instant Replay では動画のエンコード処理をパイプライン化しており、CPU のコアを有効に活用できるようにしています。パイプライン化には System.IO.PipelinesSystem.Threading.Channels などのモダンな .NET ライブラリを使用しており、メモリやスレッドの効率的な利用に役立っています。

おわりに

Unity でランタイムでの動画エンコードは実装例が少なく技術的なハードルの高さを感じていましたが、今回 Instant Replay でそれを乗り越えることができました。ただ、現時点ではまだあまり多くのデバイスでテストできておらず、デバイス依存の問題が残っているかもしれません。もし何か問題を見つけたら Issues 等に報告していただけると大変助かります。

Instant Replay が開発の一助になれば幸いです。

参考

スマホ実機のバグ報告用に直近数十秒の動画をSlackに送信、したいよね? [Unityゲームグラフィックス実践] #gamedev - Qiita

「騒ゲーハイライト」について【MIXI TECH CONFERENCE 2023】 - Speaker Deck