• スポンサードリンク

USBカメラから音&動画の同時キャプチャした〜い(その3)

Android Camera 非同期

GitHubで公開している、UVCカメラへのアクセス用ライブラリ&サンプルプロジェクト、UVCCameraに含まれるサンプルUSBCameraTest3についての追加記事です。
最初の公開時にUSBCameraTest3のMainActivityのstartRecordingにコメントとして、プライベートスレッドで実行した方が良いよと書いていましたが、プライベートスレッドで実行するように変更したので、ちょびっと解説を。

何でプライベートスレッドやねん?

そうそう、これが大事なんです。MediaCodec+MediaMuxerを使う今までのサンプルにも書いて来ましたが、特にMediaMuxerを使う場合において、プライベートスレッドで実行するのが結構大事なのです。

  1. MediaCodecの初期化処理は結構重い
  2. MediaCodecの終了処理も結構重い
  3. MediaMuxerの終了処理も結構重い・・・しかもいつ終わるかわからない

と言う事で、エンコーダー周りのスレッド構成はこんな感じになっています。以前の記事音&動画の同時キャプチャがした〜い(その1)からの流用です。MediaCodecMediaMuxer1
USBCameraTest3についてもエンコーダー周りは同じように実装してあります。
それだったら既にプライベートスレッドで動いてるんやないん?って思ったそこのあなた、エンコーダー周りについてはその通りです。

でもUSBCameraTest3では少なくともあと2つばかりプライベートスレッドで実行した方が良い処理があったのです。

    1つ目は簡単に想像付くと思いますが、カメラ(open/close、プレビュー表示開始/停止など)の処理
    2つ目は今までのサンプルでは知らなかった事にして実装していなかった処理(笑)

カメラの処理

1つ目のカメラの処理は説明するまでも無いとは思いますが、2つの理由があります。

    処理に時間がかかる
    逐次的に処理をしないといけない。例えばカメラをopenする前にプレビュー表示開始してはいけません。

つまり、時間のかかる処理を待機しないといけないと言うことになるのですが、UIスレッドで待機するとANRになってしまいます。今までのサンプルでは、手動のボタン操作で開始指示してThreadPoolで単発処理していたので問題無いように見えていましたが、流石に処理が複雑になってきたので、そろそろ真面目に実装すべきでしょうね。
逐次的に処理・・・処理のシリアライズが必要ってことですが、オブジェクトのシリアライズと混ぜこぜにしてしまう頑固なJava使いも多いようなので、シーケンシャルに処理って言いましょう(笑)意味的にはオブジェクトのシリアライズの方が異端な気がしますけどね。
で、そのシーケンシャルに実行する方法はいくつもあるので好きな方法でいいのですが、Android&プライベートスレッド&シーケンシャル実行となると、定番はHandlerを使ったメッセージベースの方法ですね。FIFOのメッセージキューを使うことで勝手にシーケンシャル実行になります。このブログでも過去に何度も登場していますので、詳細は省略です。

知らなかった事にして実装していなかった処理

そして2つ目、「知らなかった事にして実装していなかった処理」です。この処理自体はプライベートスレッドで実行しなくてもいいのですが、MediaMuxerを使う場合はプライベートスレッドから呼び出すほうが楽になることがあります。

一体何のことか判りましたか?じゃじゃ〜ん。それは、メディアスキャン処理です(もちろん知らなかった訳では無く単に実装するのが面倒だっただけ)。Android特有の事情でメディアスキャン処理をしてあげないと、せっかく静止画や動画と保存しても、例えばGalleryアプリやPhotoアプリで見ることが出来ないのです(少なくとも端末を再起動すれば見れるようになります)。ファイラー系のアプリでは見えるのに、Galleryアプリでは見えない・・・それはメディアスキャン処理を行ってないからなのです。

メディアスキャン処理はいくつか方法が有ります。

  • 一番簡単なのはMediaScannerConnection#scanFileを使う方法だと思います。
  • 一番詳細にコントロールできるのは、ContentResolver経由で必要な情報をMediaStoreへ登録する方法
  • 昔は出来たけど確か今は通常のアプリからは出来なくなったのが、ACTION_MEDIA_MOUNTEDをブロードキャストする方法

今回は、一番簡単なMediaScannerConnection#scanFileを使います。

でも、MediaScannerConnectionでの処理は静止画や動画のファイルが出来てからでないと行うことが出来ません。静止画の方はファイルへの出力までほぼ自前でコントロールして行うので、ファイルへ書き出しが終わったタイミングでMediaScannerConnection#scanFileを呼び出せばいいのですが、問題は動画です。
最初の方で書きましたが、「MediaMuxerの終了処理も結構重い・・・しかもいつ終わるかわからない」のです。しかも、MediaScannerConnectionの処理はContextが必要なのです。別にActivityのコンテキストである必要は無くて、アプリケーションコンテキストで十分なのですが。
通常はMediaMuxerへ終了指示を出してから大体においては1〜数秒後には終わりますが、その時点でアクティビティやアプリが生きているかどうかもわからないのです。例えば録画中にバックキーを押してアプリを終了してしまった時、あるいは、録画終了指示を出した直後にアプリ終了した時など、MediaMuxerの処理が完了している保証は誰にも出来ません。この、MediaMuxer終了時に「アプリが生きているかどうかもわからない」ってのが曲者で色々手をかけてあげなければいけません。

例えば、MediaMuxerへの強参照をいつ破棄するかが問題になります。強参照が無くなってしばらくすると処理中であっても強制的にGCに抹殺されてしまうので、MediaMuxerが処理を終了するのに十分なだけ強参照で保持してあげないといけません。元々のUSBCameraTest3の実装だと機種によってはMediaMuxerが処理完了する前にGCに抹殺される場合がありました。
1つ前のサンプルUSBCameraTest2の場合は使っているMediaCodec/MediaMuxerが映像用の1つだけだったので、エンコーダー用のスレッドでコントロールすれば良かったのですが、USBCameraTest3ではMediaCodecが音声用と映像用の2つあって、それぞれが独立してMediaMuxerへ書き込みに行くので、誰かが別に音頭を取って破棄のタイミングを図る必要あるのです。

そしてそのMediaMuxerが処理を終了してからMediaScannerConnectionを呼び出せるようにコンテキストを参照可能な状態で保持しないといけないのですが、こちらは気を付けないととリークする可能性があります(破棄されるべきオブジェクトが破棄されずに残ってしまう状態)。
USBCameraTest3では、MediaCodec/MediaMuxer周りの処理をMediaMuxerWrapper内のスレッドで実行しているので、そこに組み込んでもいいのですが、

  • どのみちカメラ操作用のスレッドを作らないといけない
  • MediaMuxerWrapperへコンテキストを渡さないといけなくなる
  • メディアスキャン処理は、エンコード処理と直接関係ない・・・どちらかと言うと上位で呼び出す方が良い処理

とかの理由で、MainActivity内に別スレッドの処理を追加実装しました。

と言う事でソースです。わっはっは、説明が面倒になってきたのでソースで誤魔化そう。長くなってきたので、そろそろソースに行きましょう。

« »

  • スポンサードリンク

コメント

  • toshiki より:

    いつもブログを見させて頂いています。
    素晴らしいプロジェクトを公開してくださってありがとうございます。
    UVCCameraSample5を使わせていただきました。Webカメラを使ったデバイスが作りたかったので、大変助かりました。感謝です。
    デバイスのアプリの製作段階で、困ったことがありましてご相談させていただきたいです。
    デバイスの影響で、水平に対して90度横になってwebカメラがついています。
    Webカメラの情報を90度回転した状態で表示して、90度回転した状態でビデオや画像に出力するにはどうしたらいいでしょか。なかなかどこを変更したらいいのか分からず悩んでいます。もしよろしければ答えていただければ嬉しいです。
    お願いしますm(_ _)m。

    • saki より:

      こんばんは。

      UVCCameraSample5は映像の回転をほとんど考慮してないので・・・
      ・同じ大きさのバッファを確保してプログラムで回す。行と列を入れ替えるだけです。
       でもJavaだとかなり重い処理だろうなぁ。nativeでもSIMD(NEONとかSSE)を使わないと
       映像サイズによっては重いです。もしかすると一旦テクスチャに書き込んでOpenGL|ESで回転
       させてからreadPixelsで読み取るほうが早いかもしれないです。
       OpenCVとかlibyuvとかRenderScriptとか他にも回転させるすべは色々ありますけど。
      ・MediaMuxer#setOrientationHintで回転していることにする。これは超簡単。
       でも再生アプリによっては回転させてくれないかも。
      ってことになってしまいますね。

      表示だけなら
      UVCCameraTextureView#setScaleX(-1.0f)で左右反転(本当は裏返し、鏡像)
      #setScaleY(-1.0f)で上下反転(本当は裏返し)
      #setScaleX(-1.0f) & #setScaleY(-1.0f)で上下左右反転=180度回転
      それ以外の角度だと、#setPivotX/#setPivotYと#setRotationを組み合わせるのが簡単ですかね。

      UVCCameraSample3だとOpenGL|ESで描画と録画用のエンコーダーへの書き込み処理をするので、
      その時に、Matrixで回転を適用すればいいのであまり考えることはありません。
      この時気を付けないといけないのは、SDKには2種類のMatrixクラスが有りまして、
      1つは、android.graphics.Matrixで、もう1つはandroid.opengl.Matrixです。
      OpenGLのテクスチャ変換行列または射影行列を回すので、使うのは後者のandroid.opengl.Matrixです。
      Matrix#translateMとMatrix#rotateMを組み合わせて使います。
      UVCCameraSample3ではGLDrawer2Dというヘルパークラスで実際のOpenGL|ESの描画をしているので、
      今は使ってないモデルビュー変換行列(=単位行列に設定してあるだけのmMvpMatrixっていう変数)を
      回転させるのがいいかもしれません。

      ちなみに、静止画の場合には、Matrixを適用しながら変換することができるのでそこで回転させれば
      大丈夫だと思います。

      よろしくお願いします。