こんにちは、コア技術本部の瀬戸です。Instant Replay for Unity を v1.0.0 正式版としてリリースしたので、変更内容等についてご紹介します。
Instant Replay for Unity について
Instant Replay for Unity は Unity で実行中のゲーム画面を常時録画し、任意のタイミングで動画として書き出すことができるライブラリです。Instant Replay for Unity の概要については以前の記事をご覧ください。
導入方法等はREADMEを参照してください。
リアルタイムモードのデフォルト化
録画中にリアルタイムで H.264 / AAC のエンコードを行う リアルタイムモード を v0.4.0 で導入しましたが、v1.0.0 ではこれをデフォルトの動作モードとしました。従来のモードは [Obsolete] としてマークされ、積極的にサポートされなくなります。
移行について
すでに Instant Replay for Unity を利用している場合、InstantReplaySession の代わりに RealtimeInstantReplaySession を使用することでリアルタイムモードに移行できます。詳しい使い方は前回の記事をご覧ください。
Package Manager 経由でインストールできるサンプルを利用している場合は、再度サンプルをインポートすることでリアルタイムモードを利用したバージョンに更新できます。
Linux サポートの強化
リアルタイムモードではこれまで Linux がサポートされていませんでしたが、v1.0.0 では FFmpeg を利用した Linux サポートを追加しました。ffmpeg コマンドに PATH が通っている環境であれば動作します。これによって対応プラットフォームは iOS, Android, macOS, Windows, Linux の 5 つとなりました。
技術的な詳細
ネイティブ実装の統合
前回の記事で紹介した通り、v0.4.0 ではリアルタイムモードの追加に伴って新しい Rust 実装のネイティブプラグインを導入しました。これによって従来のモードで使われているネイティブ実装と、リアルタイムモード用の Rust 実装が併存する状態になっていましたが、v1.0.0 では従来のモードでも Rust 実装のプラグインを使用するように変更しました。これにより全てのネイティブプラグインは Rust 実装に統合され、メンテナンス性が向上しました。
FFmpeg によるエンコード
リアルタイムモードではキャプチャしたフレーム・音声サンプルを H.264 / AAC でリアルタイムにエンコードし、古いデータを破棄しながらバッファに保持し、最終的に MP4 ファイルに多重化します。メモリ上に保持されるのは生の H.264 / AAC ストリームです。FFmpeg では生のストリームを扱う方法はあまり一般的ではありませんがサポートはされており、次のようなコマンドでエンコードを行うことができます。
# 32ビット BGRA ピクセルデータを H.264 ストリームに変換 ffmpeg -y -loglevel error -f rawvideo -pixel_format bgra -video-size 1280x720 -framerate 30 -i - -f h264 -pix_fmt yuv420p -r 30 -c:v h264 - # 生の音声データを AAC ストリーム (ADTS) に変換 ffmpeg -y -loglevel error -f s16le -ar 44100 -ac 2 -i - -f adts -
上記のコマンドは標準入出力を利用してデータのやり取りを行います。得られる出力は単なるバイトストリームであり、フレームやパケットの区切りは自分で見つける必要があります。FFmpeg における h264 フォーマットは H.264 の仕様で定義されたバイトストリーム (Annex B) 形式で、0x000001 というスタートコードを区切りとする可変長なパケット(NAL ユニット)の連続として表現されます。音声で使用している adts フォーマットは可変長な ADTS パケットの連続として表現され、各パケットのヘッダにパケットの長さが記載されています。
さらに、リアルタイムモードでは各フレームが I フレームか P フレームかを判別する必要があるため、NAL ユニットの内容をパースする必要があります。NAL ユニットのパースには cros-codecs というライブラリに含まれる実装を利用しました。
FFmpeg による多重化
多重化では次のようなコマンドを利用しています。
ffmpeg -f h264 -r 30 -i - -f aac -i pipe:XX -pix_fmt yuv420p -c:v copy -c:a copy -f mp4 out.mp4
重要なのは入力に パイプ を利用している点です。多重化においては映像と音声を同時に入力する必要があるため、標準入力に加えてパイプを併用することで、ディスクに頼らずに複数の入力を実現しています。
パイプは tokio::net::unix::pipe::pipe() を使用して作成しています。ただし、このパイプはデフォルトで close-on-exec (O_CLOEXEC) フラグが付与されているため、Rust から FFmpeg を起動する際の fork-exec の過程で close されてしまい、子プロセスから参照することができません。
そこで、libc::dup() を使用してパイプの read 側のファイルディスクリプタを複製します。dup() は複製されたファイルディスクリプタの O_CLOEXEC フラグをクリアするため、そのまま子プロセスで利用することができます。
// パイプの作成 let (tx, rx) = tokio::net::unix::pipe::pipe()?; // dup で複製しつつ O_CLOEXEC をクリア let rx_dup = unsafe { libc::dup(rx.as_raw_fd()) }; if rx_dup < 0 { return Err(anyhow::anyhow!("Failed to dup pipe read end")); } // 親プロセス側は複製された rx を close する必要があるので、OwnedFd に変換しておく // これは子プロセスが立ち上がるまで drop されないようにしておく必要がある let rx_dup = unsafe { std::os::fd::OwnedFd::from_raw_fd(rx_dup) }; // FFmpeg を起動 let child = Command::new("ffmpeg").args([/* ... */, "-i", format!("pipe:{}", rx_dup.as_raw_fd()), /* ... */]).spawn()?; drop(rx_dup); // パイプに書き込み tx.write_all(&data).await?; tx.flush().await?;
ハードウェアエンコーダーの選択
リアルタイムモードでは次々と入力されるフレームを可能な限りドロップせずにエンコードしていく必要があるため、ハードウェア支援を活用して高速にエンコードすることが望ましいです。FFmpeg では -c:v オプションにハードウェアエンコーダーの名前を指定できます。利用可能なエンコーダーのリストは ffmpeg -encoders で確認できるため、ここから既知のハードウェアエンコーダーを探して優先的に利用すればいいわけです。
ただし、ffmpeg -encoders は FFmpeg のビルド時に有効化されたエンコーダーのリストを示しているに過ぎないため、実際にはそのマシンでサポートされていないエンコーダーもリストに含んでしまいます。そこで、ffmpeg -encoders で見つけたエンコーダーを利用して実際に簡単なエンコードを試みることで、そのエンコーダーが実際に利用可能かどうかを判別しています。FFmpeg はこのようなテスト用にダミーのソースやシンクを提供しているため、非常に簡単なコマンドでエンコードの可否を試すことができます。
ffmpeg -y -loglevel error -f lavfi -i testsrc=s=256x256:r=2:d=1 -c:v h264_nvenc -f null -
おわりに
Instant Replay for Unity は引き続き OSS として開発中です。お気づきの点があれば Issue や PR をお寄せいただけますと幸いです。