• スポンサードリンク

音&動画の同時キャプチャがした〜い(その3)

Android Camera MediaCodec 非同期

MediaCodec+MediaMuxerを使って音&動画の同時キャプチャがした〜いの第3弾♪
もっとも前回の最後で書いたとおりメインの部分は既に出てきてしまっているので、付録です。ボリューム的にはこっちがメインだけど。

やっぱり前置き

何で付録が必要かと言うと、カメラからの映像取得とプレビュー画面の表示部分に原因があります。
というのも、御存知の通り通常は、内蔵カメラからのプレビュー映像取得表示は、
Camera#setPreviewTexture(SurfaceTexture surfaceTexture)
または
Camera#setPreviewDisplay(SurfaceHolder holder)
を使います。SurfaceTextureまたはSurfaceHolderを渡すことで簡単にTextureViewやSurfaceViewにプレビュー画面を表示できる優れものです。

でも一度にセットできるSurfaceTextureまたはSurfaceHolderは1つだけです。MediaCodecへ入力するフレームデータはどうやって取得すればいいのでしょう?

オーソドックスな方法・・・でも非効率的

例えば次のような方法が考えられます。

  1. #setPreviewCallback(Camera.PreviewCallback cb)
    #setPreviewCallbackWithBuffer(Camera.PreviewCallback cb)
    を使ってCamera#PreviewCallbackを割り当てて、
    #onPreviewFrame(byte[] data, Camera camera)
    コールバックメソッドでバイト配列としてフレームデータを取得する方法。
    これの欠点は、動作が遅い・得られたフレームデータからピクセルフォーマットの変換・端末の向きに合わせて映像の回転処理を自前でしないといけない事などが挙げられます。元々GPU内にコピーされているフレームデータを一旦Javaのバイト配列として取り出して変換した後再度GPUへ送ることになるので、フレームレートが低くなる・消費電力が多く電池の消耗が激しいってことになります。
  2. あるいは、TextureViewから取得したSurfaceTextureを#setPreviewTextureでカメラへ引き渡して、#onFrameAvailableイベントリスナーが呼び出された時に、TextureView#getBitmapメソッドでビットマップとしてフレームデータを取得し、MediaCodecのSurfaceへ描画することも出来ます。
    この方法だとピクセルフォーマットや映像の回転はしないで済みますが、御存知の通りAndroidのBitmapは遅いし処理の負荷がかなり高いので、やはり効率がよくありません。
    ちなみに、TextureView#getBitmapには
    の3種類のバリエーションが有りますが、仮にこの方法でMediaCodecの入力用データを取得する場合には最後のものしか使ってはいけませんし、最後のものを使う場合にも注意深くコーディングする必要があります。試してみれば直ぐに判りますが、上2つでは簡単にOOMでクラッシュします。上2つが使えるのは静止画のキャプチャの用に単発・低頻度の場合のみです。
  3. GLSurfaceViewのSurfaceをカメラへ渡して描画されたデータを、glReadPixelsで読み取って・・・

他にも類似の方法は有るかもしれませんが、共通しているのはカメラがGPUに書き込んだフレームデータを一旦JavaもしくはNativeコード側へ取り出さないといけない点で、これが効率&速度低下の一番の原因になっています。

んじゃどうすんねん

って言うと、要はGPUからデータを取り出さなきゃいいわけです。というよりTextureView/SurfaceTextureが作られた理由の1つがこのためではないでしょうか(他にも重要な目的が有りますが)。

SurfaceTextureのコンストラクタにはOpenGL|ESのテクスチャID(テクスチャハンドル)を引き渡すことが出来ますので、このテクスチャIDとOpenGL|ESを使ってプレビュー画面用とMediaCodec用に描画すれば良いのです。

でも2回描画するのって遅くならないか心配に思います?同じスレッド内で2回描画すると確かに2倍以上の時間がかかります。そりゃそうだ。でも、違う2種類の場所(Surfaceまたはwindow)にフレームデータを転送(描画)しないといけないので最低2回の描画は避けて通れません。あとはいかに効率良く行うかだけです。

どうしたら良いと思いますか?答えの1つがマルチスレッドです。今どきの端末はマルチコアCPUにマルチコアGPUってことを知ってますか?そう、マルチスレッドで描画すれば最良の場合、2回描画しても1フレーム当たりの描画時間は1回分+αで済みます。(シングルCPU/シングルGPUの場合など)最悪の場合でも2回分の描画時間になるだけです。

そう言う事でEGL

いきなり何のこっちゃ。
OpenGL|ES, EGLについてはあまり専門家ではないので以下はあくまでも自分の理解に過ぎません。間違ってたら教えて下さいな。

とりあえずマルチスレッドでのOpenGL|ESの描画について調べてみた
  1. OpenGL(ES)のコマンドを呼び出すと、そのスレッドに紐付けられているEGLContextに対して操作が行われる
  2. OpenGL|ESの制限として、1つのスレッドに対して1つのEGLContextしか紐付けることができない
  3. 1つのEGLContextは1つのスレッドに対してしか紐付けることが出来ない

つまり、

  1. どこかのスレッドで使われているEGLContextを他のスレッドで利用することは出来ない
  2. マルチスレッドでOpenGL|ESで描画するには、各スレッド毎にEGLContextを作らないといけない

ってことらしいです。分かったようなわからんような(-_-;)そもそもEGLContextってのがわかってませんからね。

EGLContextってなんぞや

OpenGL(ES)の画面クリア色やテクスチャその他OpenGL(ES)の描画にまつわる様々な状態・リソースを保持するオブジェクト?らしいです。また、あるEGLContextに対して行った操作は他のEGLContextに影響を及ぼさないそうです。これも当然ですね。でないと画面の何処かをOpenGL|ESで赤く塗りつぶしたら他の部分も赤くなっちゃいますからね。
でも、「あるEGLContextに対して行った操作は他のEGLContextに影響を及ぼさない」ってことは、このままではテクスチャも各EGLContext毎に読み込まないと駄目になります。それだと今したいことには役に立ちません。

そこで登場するのがshared_contextらしいです。shared_contextとやらを使うと、OpenGL(ES)の状態は別個に持ったままテクスチャ等のリソースだけは共有できるようになるそうです。つまり、カメラが(テクスチャに)書き込んでくれたフレームデータをプレビュー描画用とMediaCodecへの入力用の2つのスレッドから(実際にはメモリ等のハードウエアが許す限りのスレッドから)共有して使用することが出来るってことです。何が共有できて何が出来ないかは自分で調べましょうm(__)m

でやっとEGL

今回も前置きが長くなっちゃったから、EGLContextを作ったり破棄したりをするための関数定義がEGL、と言う事にしておこう(-_-;)やっと本題です。

« »

  • スポンサードリンク

コメント

  • egr より:

    説明&サンプルコードの更新ありがとうございます。
    素晴らしい対応の速さに驚きました。
    SurfaceViewの仕組みと画面が乱れる原因、その対策について納得できました。
    更新して頂いたソースを参考に対策してみます。

    ActionBarのタイトルとOptionMenuが化ける(文字が全て■で塗りつぶされる)件については何かご存じないでしょうか。
    GLDrawer2D.initTex()でGLES20.glGenTexturesを実行すると画面回転時に化けるようになるようです。
    ActionBarで使用しているテクスチャ(?)にも影響してしまっているのでしょうか?

    • saki より:

      こんばんわ。
      SurfaceView系でViewのサイズ調整をするとおかしくなるのは前から知ってて放置していただけなので。

      自分のアプリではあまりActionBarを使ってない・・・(^_^;)ので確かなことは言えませんが、
      なんとなく背景のdrawableがおかしくなることがあったような・・・
      ActionBarにかぎらずdrawable・・・特にリソースで作ったdrawableはタイミングによって
      ちゃんと表示できずに、再セットしないとダメなのがあったような・・・
      ActionBarの文字等は普通のTextViewかButtonだったと思うので直接的にはOpenGLは関係ない
      気がします。FragmentかActivityのonResume辺りでActionBar#setBackgroundDrawableとかで
      背景を再セットするともしかすると治るかも?
      確かめてないので治らなかったらごめんなさいm(_ _)m

  • egr より:

    とても有益な情報を公開して頂きありがとうございます。
    AudioVideoRecordingSampleを参考にさせていただいてます。

    サンプルを使用すると撮影される動画や音声には問題ないのですが、
    画面を回転またはアプリケーションを履歴から復帰させた際に、
    アクションバーに表示されたタイトルを含めて画面が乱れてしまいます。
    (カメラのプレビューは表示&更新されています)

    OpenGLによる描画部分の復帰方法に問題があると思われるのですが、改善できずにいます。
    Resumeさせた際に画面の描画の乱れについて、何か原因や対策などをご存じないでしょうか?

    Nexus7 (2013) Android 4.3にて確認しています。

    • saki より:

      こんにちは。

      ちょっと横着しているのがバレてしまいましたm(_ _)m
      このサンプルプロジェクトの表示ではGLSurfaceViewを継承したクラス内で行っています。
      GLSurfaceViewやその大元であるSurfaceViewは、一応Viewという名前にはなっているのですが、
      View自身では実際の表示をせずにViewの大きさにくり抜いてその後ろに配置したSurfaceを透過表示
      するような仕組みになっています。一方で、サンプルではカメラ映像のサイズに合わせてViewのサイズ
      調整を行っています。サイズ調整前(Viewのデフォルトのサイズ=レイアウトに最初に配置された時の
      サイズ)と、最終的に調整されたViewの領域とに隙間ができた時にもSurfaceView自身は何も描画
      しない上に、カメラ映像はViewport内にしか描画できませんので、ゴミが表示される・・・乱れて
      見える・・・ことになります。
      SurfaceViewでの透過領域はPaddingを考慮しないので、まずはPaddingをクリアします。
      後は次のようなことをします。
      1)アスペクト比の調整を止める(その1)
        このサンプルでは映像のサイズを640×480に固定してるのでアスペクト比の調整を行わない
        場合には映像が歪んで表示されることがあります。
        実際には、端末毎の画面サイズに近いサイズが用意されているはずなので、アスペクト比が
        近いカメラプレビューサイズを選択して、画面一杯にViewを表示すれば大丈夫。
      3)アスペクト比の調整を止める(その2)
        2)の方法だと、好きな大きさのViewに表示できません。
        3−1)まずViewのアスペクト比の調整をやめます(リサイズしない)
        3−2)このままだと映像が歪んでしまうので、まずViewportの設定でView全体を描画領域
           として選択してから全面を塗りつぶします。
        3−3)高さまたは幅の少なくともどちらか一方がViewの値と一致するように描画領域の幅と
           高さを計算して、その描画領域がViewの中央に配置されるようにオフセットを計算して
           Viewportの設定を行います。
        これで、アスペクト比を保った領域のみカメラ映像が表示され、それ以外の部分は3−1で塗り
        つぶした色になります。
      3)SurfaceView(やSurfaceViewから継承したView)を止める
      TextureViewを使うとViewの大きさに応じで描画領域の設定を自動で行ってくれます。
        なので、表示したいカメラ映像のサイズからアスペクト比を計算して、それでViewをリサイズ
        すればOK。
      4)カメラ映像のアスペクト比が前もってわかっているのであれば、サイズ・位置を計算して
        からLayoutParamsとしてSurfaceViewを生成する。
        ただし、レイアウトファイルには配置出来ませんので、プレースホルダとなるViewGroupを
        かわりに配置して実際のSurfaceViewは動的に生成することになります。

      と言う事で、AudioVideoRecordingSampleを3の方法で更新しておきました。わかりやすい
      ように余白部分は黄色になっています。
      CameraGLView#onSurfaceCreated内で、使っても無いのにわざわざ
         GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
      などとしてでクリア色を黄色に設定してのはこんな所の伏線だったんですねぇ(笑)
      塗りつぶしの色は好きに変えて下さい。