WhiteBoard Castのエンコーダーをネイティブに変えたいなぁ
WhiteBoardCastで、そろそろ標準のエンコーダーに乗り換えたいな、と思って調べている。
やりたい事
やりたいのは動画用Canvasと画面表示用Canvasの2つを作って、
- 動画Canvasは動画に描くものだけをdraw
- 画面Canvasはそこにフローティングメニュー等を上書き
- スクロールする時などのタイミングでCanvasの中身をbitmapとして取り出したり、その次の画面のBitmapを描いたりしたい(前に描いていた所に戻る場合)
くらいの事がやりたい。
アプリの性質として、更新はいつも画面のごく一部、という性質があるので、出来たら毎回全画面送りたくはない。(だが最近は全部送っても平気ならそれでも良い。以前試した時はフレーム落ちが多すぎて全然駄目だったが、今は平気かも?)。
提供されているAPI
一方EncoderはByteBufferかSurfaceを介するようになってて、ByteBufferはYUVならとりあえず動きそう。
Canvasを取得出来ればほとんど同じコードが使える。 だか、Surfaceは以下を信じる限りlockSurfaceは動かず、Mから導入されたlockHardwareCanvasを使わないと駄目そう。
Stackoverflow: Why is video made with MediaCodec garbled for Samsung Galaxy S7?
自分のメインのスマホはLなので、API Level 22はまだ切れない。
考えてる選択肢
とりあえず幾つか書き出しておいて、思いついたら足したりしていく。
選択肢1: GLESでbitmapを描く
動画用Canvasにsurfaceを使って、BitmapのdrawなどはGLESのAPIでテクスチャ経由で直接描く事。 これは画面を一部更新する都度全部送る必要はありそうだが、VRAMのフォーマットとかの問題は気にする必要は無い。
ただGLESで画面描くコードを書くのはかったるい。
選択肢2: YUVへの変換を自力でやって、ByteBufferを使う
ByteBufferでYUVはだいたい動きそう?そしてByteBufferはオフセットがあるので部分更新が出来そう?(ただニ次元っぽく無いので出来ないかも…その場合でも行ずつ送るのはまぁまぁ早いかもしれない)
ただ自分でYUV変換とかだるくて泣いちゃう。 Bitmap backedなCanvasで、YUVのものが作れればなぁ、と少し調べたが、Bitmapクラスのフラグには無さそうだった。
現時点での方針
YUVを手で変換してByteBufferが一番楽そう?
bitmap backedなCanvasはそのままにして、それと同じサイズのYUVバッファを持っておく、変更があれば該当範囲にかかる所だけYUVバッファを更新(偶数番と奇数番の2pixelあれば平気か?)、YUVバッファ自体は全体を送る、くらいでどうだろう?
YUV_420はuとvがどうパックされてるかはわからない。 yが別のバッファで良さそうなのは保証されてるが。
pixelStrideはImage.Planeから取れるので、一つずつputしていく事は出来るし、これは結構早そうではあるが…
uとvをネイティブと同じように詰めてputで一気に更新したかったが、それは無理そう。
追記。少し触って。
strideなどの情報は隠されているのでgetInputImageでImageを取る、との事。 で、ImageでPlaneが取れるからあとはStrideを考えてbyte bufferに書けば良い、、、かと思ったが、そうでも無い。
書く側はstrideを考慮して書くメソッドが無い。 連続領域に見せてくれるのか?とも思ったが、調べてみるとどうもそうでは無さそう。
StackOverflow: How to save a YUV_420_888 image? のコメントとそのリンク先matlabより
単純に下のフォーマットが秘密になってしまっている。 これではRGBから変換して書くのは一バイトずつしかできない事になるが、さすがにそれは非現実的だろう。
という事でやはりEncoderで使う場合は下のフォーマットが分かってる奴を使うしか無い。 この辺はctsのEncodeDecodeTestは通るという前提で書くしか無いだろう。
android-9.0.0のcts EncodeDecodeTest.java
めちゃくちゃdeprecatedだと言ってくるが推奨フォーマットではエンコード側は出来ないのだから仕方ない。 こういうdeprecatedのつけ方どうなの?
一通り実装が終わる
API的にはaudioまわりはコールバックで行けそうに見えたが、AudioRecordのreadのnon brockingはMから(!?)、という事でフレーム落ちするとUIスレッドがブロッキングしてしまう。
諦めて専用のスレッドを作る事にした。
いろいろ考えた結果オーディオは生のスレッドを使う事に。久しぶりだな、生のスレッド。(なお動画の方はScheduleExecuterを既に使っている)。
副作用として
- フレームレートがめっさ上がった
- めっさ軽くなった
- 音声がクリアになった
- 遅い端末で最初数msec飛ぶ(少し待ってから始める必要がある)
- サムネイルが最初になっていまいち
- 最後のマージが要らなくなった
という感じになった。全体的には良くなったけど悪くなった所もあるな。
最初飛ぶのは良く分からない。最近の端末だと起きないし。 audio のcodecまわりが間に合ってないのかなぁ。その辺はUIをブロックするはずだが、そういう感じでは無いんだが。 なお二回目以降は平気。うーん。ここだけは直したいけどなぁ。
MediaCodecまわりは毎回すごい苦戦する。今回も苦戦した。 ただICSの時に評価した時よりはCTSも増えててちゃんと使えるAPIになっていた。 Mより上でよければもMediaCodecはハードウェアの使って大抵のやりたい事は出来そう。
これだけ多様な端末で、必要最小限くらいを共通化してとりあえず使える感じになってる、というのはすごいアートだよなぁ。 もっとしっかり決めた方が良かったとは思うが、このゆるさはメーカーのやる気を出す、という部分はあると思う(アプリ書きのやる気を削いでる部分もあるが)。
追記: 最初飛ぶのはデバッグビルドの時にyuv変換に2secくらい掛かってたせいだった。 リリースビルドでは一瞬だが、一応高速化するとともに、時間かかる処理が終わるまではレコーディングは始めないように修正。
たまに変な、widthがなんとか、みたいなワーニングが出てるがそれ以外は問題無さそう。 ワーニングは20分とってて一回とかの頻度でなかなか出ないので原因の特定が難しい。
これが取れたらリリースでいいかなぁ。
追記2: これまであったバグとかもついでに見つけたりして直してリリースした。 結構大規模な変更な上にデバイス依存が多そうなので不安もあるが…
ただだいぶ良い物にはなったな。やはりハードウェア使うと早いね。