Slide 1

Slide 1 text

©MIXI MIXI TECH CONFERENCE 2023 「騒ゲーハイライト」について 開発本部 CTO室 クライアントグループ Yuki Shinobu

Slide 2

Slide 2 text

©MIXI Yuki Shinobu / YuukiARIA 株式会社MIXI 開発本部CTO室クライアントグループ 2014年新卒入社。 直近ではゴーストスクランブルのテクニカルリードとして 主にクライアントの技術課題に取り組む。 2 自己紹介

Slide 3

Slide 3 text

©MIXI 本日の内容 ● ゴーストスクランブルと「騒ゲーハイライト」システム ● Unityゲーム画面のキャプチャ ● Androidでmp4動画を出力するまで ● 様々な不具合対応 3

Slide 4

Slide 4 text

©MIXI ゴーストスクランブルについて

Slide 5

Slide 5 text

©MIXI ゴーストスクランブル モンストシリーズの協力アクションスマホゲーム ストブルと略します 5 ゲームエンジン グラフィック サウンド Unity 2020.3 Universal Render Pipeline (URP) CRI ADX

Slide 6

Slide 6 text

©MIXI 「騒ゲーハイライト」とは? ゲームプレイの特定の15秒を動画として保存できる機能 6

Slide 7

Slide 7 text

©MIXI 基本の設計コンセプト

Slide 8

Slide 8 text

©MIXI システムの要件 ● キャプチャ実行時,直前の10秒程度を含む前後の動画を保存 ● インゲームプレイ中,いつでもキャプチャ実行可能 ● 保存した動画はSNS等へ投稿できる形式 ○ Unity等に依存しない,一般的な mp4 (H.264) 形式 8

Slide 9

Slide 9 text

©MIXI システムの要件 ● キャプチャ実行時,直前の10秒程度を含む前後の動画を保存 ● インゲームプレイ中,いつでもキャプチャ実行可能 ● 保存した動画はSNS等へ投稿できる形式 ○ Unity等に依存しない,一般的な mp4 (H.264) 形式 9 これは ドライブレコーダーだ! 最近はゲーム機に搭載されていることも多い

Slide 10

Slide 10 text

©MIXI 基本の設計コンセプト 10

Slide 11

Slide 11 text

©MIXI 基本の設計コンセプト 11 15秒分の固定長の循環バッファを作り 常時録画・録音

Slide 12

Slide 12 text

©MIXI 基本の設計コンセプト 12 Android/iOSのコーデックAPIを利用

Slide 13

Slide 13 text

©MIXI ゲーム画面のキャプチャ

Slide 14

Slide 14 text

©MIXI ゲーム画面のキャプチャ 当初はRendererFeatureとして実装 RendererFeature: Universal Render Pipelineでカスタムの描画パスを挿入する仕組み 14 キャプチャ処理の実体 パラメータ定義 Rendererにパスを挿入するコンポーネント

Slide 15

Slide 15 text

©MIXI ゲーム画面のキャプチャ CameraCaptureBridgeを使うとデリゲートを登録するだけで簡単 にキャプチャ処理を挟むことができる 15 void Execute(RenderTargetIdentifier renderTargetIdentifier, CommandBuffer commandBuffer) { using (new ProfilingScope(commandBuffer, _profilingSampler)) { commandBuffer.Blit(renderTargetIdentifier, _captureOutputRenderTarget, _material); } } CameraCaptureBridge.AddCaptureAction(_targetCamera, Execute);

Slide 16

Slide 16 text

©MIXI mp4ファイルを作る

Slide 17

Slide 17 text

©MIXI コーデックAPI 連続画像を動画にエンコードするために,低レベルAPIを使う 17 iOS VTCompressionSession AVAssetWriter Android MediaCodec MediaMuxer

Slide 18

Slide 18 text

©MIXI コーデックAPI 連続画像を動画にエンコードするために,低レベルAPIを使う 18 iOS VTCompressionSession AVAssetWriter Android MediaCodec MediaMuxer 端末ごとの差異も少なく,それほどトラブルが発生しなかった

Slide 19

Slide 19 text

©MIXI コーデックAPI 連続画像を動画にエンコードするために,低レベルAPIを使う 19 iOS VTCompressionSession AVAssetWriter Android MediaCodec MediaMuxer 大変だった

Slide 20

Slide 20 text

©MIXI android.media.MediaCodec

Slide 21

Slide 21 text

©MIXI MediaCodecの入力データ ● android.view.Surface ○ MediaProjectionと相性が良いが今回の要件には合わない ● android.media.Image ○ ピクセルフォーマットを気にせずYUVの各Planeを扱えるが遅い ● java.nio.ByteBuffer ○ 最速だがピクセルフォーマットを自分で組み立てる必要がある 21

Slide 22

Slide 22 text

©MIXI ByteBufferを使おう

Slide 23

Slide 23 text

©MIXI ByteBufferで入力するには… MediaCodecが直接扱える生データを入力する必要がある 以下のことを自分でやらなければならない ● キャプチャした画像の色空間をRGBからYUVに変換 ● YUVデータの色差成分をサブサンプリングしてYUV420に ● YUV420データを適切なピクセルフォーマットにパック 23

Slide 24

Slide 24 text

©MIXI ● キャプチャした画像の色空間をRGBからYUVに変換 ● YUVデータの色差成分をサブサンプリングしてYUV420に ● YUV420データを適切なピクセルフォーマットにパック 24

Slide 25

Slide 25 text

©MIXI YUV色空間 映像分野で標準的な色空間 輝度成分 Y と色差成分 U, V から成る ITU-R BT.601 の係数を使って以下のように変換 25

Slide 26

Slide 26 text

©MIXI YUV色空間 キャプチャ時にYUV変換してRGB値としてRenderTextureに出力 U, V の範囲は –0.5~0.5 なので +0.5 している 26 YUVをRenderTextureのRGBとして出力 YUV変換 Capture Y U V

Slide 27

Slide 27 text

©MIXI ● キャプチャした画像の色空間をRGBからYUVに変換 ● YUVデータの色差成分をサブサンプリングしてYUV420に ● YUV420データを適切なピクセルフォーマットにパック 27

Slide 28

Slide 28 text

©MIXI クロマサブサンプリング 色差成分を間引くことで圧縮 どのように間引くかによって YUV422, YUV420, YUV411 等の種類がある MediaCodecの入力フォーマットでは基本的にYUV420を使う 実際の間引きはピクセルフォーマット整形時に行う 28 U,V は 2×2 ピクセルあたり 1ピクセル分しか取らない https://developer.android.com/reference/android/media/MediaCodec

Slide 29

Slide 29 text

©MIXI ● キャプチャした画像の色空間をRGBからYUVに変換 ● YUVデータの色差成分をサブサンプリングしてYUV420に ● YUV420データを適切なピクセルフォーマットにパック 29

Slide 30

Slide 30 text

©MIXI ピクセルフォーマット 同じ色空間のデータでもバイナリデータの配置方法は様々 RGBデータでも, という配置だけではなく,色ごとに分離した のような配置方法も場合によっては便利 30 Packed (Interleaved) Planar

Slide 31

Slide 31 text

©MIXI NV12 YUV420のピクセルフォーマットの一つ ● Y-Plane と UV-Plane からなる Semi-planar 形式 ● UV-Plane では UVUV... のように並ぶ 31 Packed UV Planar Y Semi-planar YUV420のピクセルフォーマットたち • NV12 YYYY...UVUV... • NV21 YYYY...VUVU... • I420 YYYY...UU...VV... • YV12 YYYY...VV...UU...

Slide 32

Slide 32 text

©MIXI MediaCodecへの入力まとめ 32

Slide 33

Slide 33 text

©MIXI 音声のキャプチャ レコーダー側は音声データを受け取るのみで,録音の具体的な方法 には踏み込まない ○ Unity Audioの場合はOnAudioFilterReadを使う ○ CRI ADXの場合はCriAtomExOutputAnalyzerを使う ストブルはこちらの方法 音声はUnity C#側でリングバッファを用意して常時録音 mp4出力時にエンコーダへ音声データを書き込む 33

Slide 34

Slide 34 text

©MIXI mp4ファイル出力とシェア

Slide 35

Slide 35 text

©MIXI mp4ファイル書き出し I-フレーム(キーフレーム)が開始フレームになるように注意 補間 (P, B) フレームから開始すると動画の頭に乱れが生じる 最終的な動画の範囲はI-フレームを基準として決め,その時間に音声を合わせる 35 48,000 frames/sec 30 frames/sec

Slide 36

Slide 36 text

©MIXI OS機能のサポート ● 他のアプリへファイル・テキストを共有 ○ iOS: UIActivityViewController ○ Android: Intent.createChooser ● 「アルバム」へファイルを保存 ○ iOS: PHPhotoLibrary ○ Android: MediaStore API 36 特にiOSのアプリ内データはユーザーが触れづらいので, 外側に取り出す仕組みが必要。 もちろん開発時のデータ確認でも必要だった。

Slide 37

Slide 37 text

©MIXI MediaStore API ● Android 10 以降で Scoped Storage が有効 ○ 生パスではなくコンテンツURIを介してファイルアクセス ○ WRITE_EXTERNAL_STORAGE 権限が不要 ● Android 9 以前では直接 Movies フォルダへコピー ○ WRITE_EXTERNAL_STORAGE 権限が必要 37

Slide 38

Slide 38 text

©MIXI システムのまとめ

Slide 39

Slide 39 text

©MIXI 動画エンコードパート 39

Slide 40

Slide 40 text

©MIXI 音声エンコードパート 40

Slide 41

Slide 41 text

©MIXI mp4ファイル出力パート 41

Slide 42

Slide 42 text

©MIXI ドキドキの多端末検証

Slide 43

Slide 43 text

©MIXI 様々な問題が発生しました

Slide 44

Slide 44 text

©MIXI デフォルトが NV12 ではない端末がある ● Zenfone 8 ○ I420: Y, U, V の順の Planar 形式 ● Y は正常 ● U と V の取り方がかなり異なる ○ 格子状に拡大したような状態になる 44

Slide 45

Slide 45 text

©MIXI アライメント問題 ● 一部端末では横幅が16の倍数でなければならない ○ Pixel 3a ● パディングが必要 ○ パディングが欠けているため斜めに歪む 45

Slide 46

Slide 46 text

©MIXI 続・アライメント問題 ● さらに一部の端末では16の倍数でもだめなことがある ○ Pixel 3a XL (≧ Android 12) ● MediaCodec#getInputFormat の戻り値に含まれる stride, slice-height の値を使用する必要がある ● stride, slice-height は含まれていないこともある ○ その場合は16の倍数にアラインするのみとした 46

Slide 47

Slide 47 text

©MIXI slice-height の考慮 ● Plane間にも適切なパディングが必要なことがある ● 高さが適切でないと下部に緑色の領域が現れる事が多い ○ U, V が足りず途中で0になるため 47

Slide 48

Slide 48 text

©MIXI 入力バッファのサイズ getInputBufferで返されるByteBufferは stride, slice-height を考 慮した必要十分なサイズが確保されている 48

Slide 49

Slide 49 text

©MIXI 入力バッファのサイズ Y-Plane のサイズは 768 × 864 = 663,552 UV-Plane のサイズは 768 × 848 / 2 = 325,632 ∴計 989,184 49 問題 width = 752, height = 848 で MediaCodec を初期化したところ, 入力フォーマットに stride = 768, slice-height = 864 という値が含まれ ていた。このとき,入力バッファのサイズは何バイトか。

Slide 50

Slide 50 text

©MIXI 入力バッファのサイズ Y-Plane のサイズは 768 × 864 = 663,552 UV-Plane のサイズは 768 × 848 / 2 = 325,632 ∴計 989,184 50 問題 width = 752, height = 848 で MediaCodec を初期化したところ, 入力フォーマットに stride = 768, slice-height = 864 という値が含まれ ていた。このとき,入力バッファのサイズは何バイトか。 しかし実際には 989,169 15バイト少ない…

Slide 51

Slide 51 text

©MIXI 入力バッファのサイズ getInputBufferで返されるByteBufferは stride, slice-height を考 慮した必要十分なサイズが確保されている 足りない15バイトは 768 (stride) – 752 (width) = 15 UV-Plane の最終行のパディングが不要 51

Slide 52

Slide 52 text

©MIXI stride, slice-height まとめ 52

Slide 53

Slide 53 text

©MIXI パフォーマンス問題 NV12データを Unity C# から Android Java へ渡す部分 53 byte[] frameData; AndroidJavaClass pluginClass; pluginClass.CallStatic("writeFrame", frameData); Unity 2019 vs 2020 で性能が2~3桁違う! 400,000 バイトの配列を渡してみると… Unity 2019 350 ms Unity 2020 0.5 ms (x700 faster!)

Slide 54

Slide 54 text

©MIXI パフォーマンス問題 Unity 2020.1.0 リリースノート https://unity.com/ja/releases/editor/whats-new/2020.1.0 54

Slide 55

Slide 55 text

©MIXI ということで

Slide 56

Slide 56 text

©MIXI 完成!!

Slide 57

Slide 57 text

©MIXI