B612における動画コーデックの採用と最適化オプション

こんにちは。LINE+でB612 Android開発を担当しているHTです。今回のブログでご紹介するB612は、小説『星の王子さま』に登場する王子さまが住んでいた星の名前にちなんで名付けられた自撮り専用アプリです。セルフィーアプリでは初めて撮影前フィルターと3~6秒の分割動画撮影に対応しています。今回のブログでは、B612アプリ開発においてMediaCodecを活用してコラージュ形式のビデオを作成し、最終的にMP4ファイルに仕上げるまでの過程についてお話したいと思います。

動画サイズの決定

B612アプリでは動画撮影が可能ですが、リアルタイムで動画をエンコードする作業は大量のデータ処理が必要になり、実装がとても大変です。スクリーンに描画するデータをGPUメモリからシステムメモリに引っ張り、これをまたハードウェアのエンコーダに渡して処理するか、CPUで各ピクセルに対し個別演算を実行して動画データを作成しなければなりません。

また、大量のデータをリアルタイムで絶え間なく処理しなければならないため、データのサイズは極力小さければ小さいほど有利です。少量のデータで動画情報を作成するには、まずは画面を読み込むサイズを最小化し、必要な情報だけを取り込むようにしなければなりません。そのためには、アウトプットとなる映像のサイズを先に決め、結果映像で必要な個別の映像のサイズを計算する必要があります。これをもとに元の動画のサイズを決め、そのサイズの分だけの映像のみエンコードする構造になっています。

結果映像のサイズは、ユーザーの定めたコラージュによって決まります。カメラから取り込まれた画像情報を、コラージュの構成通りに縦横の数に合わせて並べます。このサイズが一次的な結果映像の最大サイズとなります。例えば、カメラから取り込んだ画像のサイズが320×480ピクセルで、1×2のコラージュを選んだとしたら、横1つ、縦2つの画像を配置して320×960ピクセルの形式で配置することになります。この結果映像のサイズを、更にデバイスで処理可能な最大サイズに合わせ同じ比率で縮小させます。このプロセスがGL(graphic library)内で処理されることを考えると、デバイスで処理可能な最大サイズは、GLで処理可能なテクスチャのサイズより小さくなくてはなりません。また、デバイスの性能によっても解像度が大きすぎるとリアルタイム処理ができなくなるため、最大解像度を縮小しなければなりません。

結果映像のサイズが決まったら、これをもとに元の個別の動画のサイズを決めます。このサイズは、結果映像のサイズから縦横のコラージュの数で分け、分けた結果映像のサイズを16の倍数になるよう調整します。16の倍数に調整する理由は、通常の動画で使用するマクロブロックのサイズが16×16ピクセルであるため、16の倍数の解像度の動画がほとんどのデバイスで問題なく再生できるからです。1_ja図1. 単一映像のレコーディング解像度を決める過程

MediaCodecの初期化とデコーダ種類の決定

MediaCodecの初期化は、下記のコードで行います。

mediaCodec = MediaCodec.createEncoderByType("video/avc");
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, frameInterval);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

上記コードのように、エンコーダ生成時に適切なMIME値である”video/avc”を設定し、H.264動画を作成するよう宣言しました。また、bitrateとframe rateを渡し、希望する形式の動画を作成するようにしました。

Color Formatについては、COLOR_FormatSurfaceを渡しSurfaceでエンコードできるようにしました。別のColor Formatオプションでの動画作成も可能ですが、Surfaceを使わない場合はMediaCodecの提供するColor Formatのみオプションを与えます。

しかし、デバイスによって対応しているColor Formatはとても様々です。このため、Color Formatを指定するとなると、それぞれのデバイスで対応しているColor Formatに合わせて入力データを変換しなければなりません。また、異なるColor Formatを持つ画像は、コラージュを合成する際にもColor Formatに合わせて画像データを配置しなければならないため、Androidデバイスで対応するColor Formatの分だけ更にコードの量が増えます。このような理由から、Surfaceを使ったMediaCodecエンコーダを作成しました。この方法は、より標準的で実装しやすいという利点もあります。

MediaCodecデコーダは、ハードウェアの性能によってデコードの速度が異なります。個別コラージュのサイズは小さくても複数の映像の合わせるときには、ソフトウェアで動画をデコードした方がハードウェアデコーダでデコードするより効果的な場合があります。また、ハードウェアデコーダのコーデックを使う場合は、一定数以上のMediaCodecインスタンスを生成する際に特定の端末で期待通りに動作しない場合があります。

このため、安定性が必要とされるデバイスではOMX.google.h264.decoderオプションを使い、Googleのソフトウェアデコーダを使いました。これにより、より安定的で、且つスピーディに複数の動画をデコードできるようになります。

マルチスレッドを活用した並列デコード

MediaCodecは単一スレッドでもエンコード/デコードができるように設計されており、dequeueOutputBufferのような関数でtimeout値が提供されます。このため、この値を非常に低く指定すると、単一スレッドでもスレッドが長時間ブロックされることなく複数のデコーダにアクセスすることができます。

しかし、このように実装しても多少の待ち時間の発生は避けられません。このため、マルチスレッドを活用することが動画処理においてはより効果的です。そこで、それぞれの動画デコードのためのMediaCodecごとにスレッドを割り当ててMediaCodecでブロックが発生しても動画処理が迅速に行われるように処理しました。下記の図2にて、それぞれのデコードスレッドが生成されたときにMediaCodec内部のスレッドとどのように相互作用するかをあらわしました。

しかし、個別スレッドを使うと性能には大きな利点はありますが、すべてのスレッドが個別に進行中にエンコーダがデコード速度に追いつかない場合、大量の画像がメモリに生成されてしまい逆効果になることがあります。これを回避するために、それぞれのデコーダが生成する画像を格納するバッファーを設け、そのバッファーのサイズを制限して大量のデコード画像が入らないようにしました。

この方法を使うと、エンコード速度がデコード速度に比べて遅れる場合はバッファーがいっぱいになるまでだけデコード処理を行い、エンコード処理が進んで再びバッファーが空いたらデコードを再開してメモリが無駄に消耗されることを防止することができます。

2図2. アプリケーションで生成したスレッドとMediaCodec内部のスレッドとの相互作用

Surfaceを活用した映像の合成

前述の「MediaCodecの初期化とデコーダ種類の決定」で述べたように、エンコードプロセスでSurfaceを使ってデータが渡されるよう、エンコード設定がされています。このエンコーダにデータを渡す前に、希望する構成の結果画像が必要なため、結果画像を描画するにはFrameBufferを割り当てて希望する構成で映像をレンダリングしなければなりませんでした。

マルチスレッドで処理を行ったデコードの結果をupdateTexImage関数を通じてGLスレッドでそれぞれのTextureに更新します。その後、FrameBufferで希望する構成で配置するようTextureのVertex座標を定めます。定められたVertexを基準にしてglDrawArrays関数で描画すると、希望通りの配置の画像になります。このとき、それぞれの映像で多少の時間差が生じることがありますが、フレームを合成する過程で希望するFPS(frame per second)に合わせてひとつのフレームに仕上がります。

3図3. 複数のテクスチャを1枚の画像に構成し、MediaCodecに渡す過程

MediaMuxerを活用したMP4ファイルの作成

エンコードしたH.264 rawデータだけで構成された結果は、オーディオなどの情報が入っておらず、再生に必要な情報を追加することができないため、一般のプレイヤーでは再生できません。作業の完了した結果映像を結果画面や他のアプリで再生するには、MP4のようなコンテナフォーマットにエンコード結果を入れる必要があります。この作業にはAndroidの提供するMediaMuxerを使ってMP4ファイルを作成します。

この過程は、下記のソースコードで処理できます。

muxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
trackIndex = muxer.addTrack(newFormat); muxer.start();
// Repeatedly call the code below to record data on the MP4 file.
muxer.writeSampleData(trackIndex, encodedData, bufferInfo);
muxer.stop();
muxer.release();

上記のコードのようにMediaMuxerを生成すると、希望する映像と音声をそれぞれのトラックに入れなければなりません。しかし、B612で映像を合成する際にはオーディオデータが不要なため、オーディオデータは追加しません。後でユーザーがオーディオを選択して保存する場合だけオーディオトラックを追加して結果ファイルを作成します。

まとめ

これまでB612においてMediaCodecをどのように使ったか、そしてスレッドを用いた速度改善、コラージュの合成プロセスなどについて述べました。上記の説明を通じてハードウェアのエンコーダを利用することがソフトウェアコーデックを使うことに比べて、よりスピーディなエンコード処理ができるということをお分かりいただけたと思います。できるだけリサイズが必要ないように処理し、複数のスレッドで同時に処理することでメモリを最小限に使いながらパフォーマンスの良いアプリケーションになるよう励みました。