• スポンサードリンク

AndroidにUSBでカメラを繋ぎた〜い(^o^)/〜その3

Android Camera NDK USB

以前の記事、AndroidにUSBでカメラを繋ぎた〜い(^o^)/〜その1AndroidにUSBでカメラを繋ぎた〜い(^o^)/〜その2の続きになります。

いよいよ闇の中・・・じゃなくってnative codeの中へ

グルグル先生に聞いたところ、Java側でパーミッションとUSBデバイスファイルのファイルディスクリプタを取得して引き渡せば、rootを取ったりカーネルを書き換えたりしなくてもnative codeからUSBへアクセス出来るようです。
そこで先輩魔法使いにならって、libusb/libuvcへファイルディスクリプタを引き渡すようにしてみました。
元々libusbにはopenするために

という関数が有るので、それにファイルディスクリプタを引き渡せるように

を作りました。また、livuvc側にも対応する関数として

というの有るので、こちらも同じようにファイルディスクリプタを引き渡せるように

を作りlibusb_open_with_fdを呼び出すようにしました。
いざ実行(^o^)/・・・結論だけを言うと全然ダメでした。
この実装では、libusbにJavaで取得したファイルディスクリプタを渡してopenしたつもりになるというものです。libusbだけを自前で使っているだけであれば注意深くコーディングすれば問題ないのですが、今回は間にlibuvcが入っています。実はlibusbもlibuvcも内部で何度もUSBのデバイス(デバイスファイル)をopenしたりcloseしたりする部分があります。
つまり1回めのopenは自前でファイルディスクリプタを渡しているのでOKなのですが、一旦native code内でcloseしてしまうと、次にopenしようとした時にJava側から貰ったファイルディスクリプタを渡しても、既にcloseされてしまっているのでパーミッションエラーが発生してだめなのです。

じゃぁどうすんねん

open時はJava側から貰ったファイルディスクリプタを使ってopenしたふりをしています。close時にも同じようにcloseしたふりをするように変更して、実際のclose処理は全部終わった後にJava側でするようにしてみました。
この時点で、単純にopen処理を書き換えるだけでは済まない事が判明したので、libusb_open_with_fdとuvc_open_with_fdはお払い箱にして代わりにlibusb_set_device_fdを作りました。
また、元々OSに依存する部分はバックエンドとして各OS毎に別ファイルとして実装されています。Androidの場合は核がLinuxなので、linux_usbfs.cとかlinux_netlink.cとかを使っていたのを、non-rooted Android端末用にコピーしてandroid_usbfs.cとかandroid_netlink.cとしてから修正しました。
android_usbfs.c(元々はlinux_usbfs.c)内にファイルディスクリプタを取得する関数_get_usbfs_fdというのがあったので、この部分を書き換えて、予めlibusb_set_device_fdでセットして内部データとして保持しているJava側から引き渡したファイルディスクリプタを返すように変更、またバックエンド内で実際のclose処理を行うop_close関数でOSのclose(int fd)関数を呼び出さないように変更しました。プリプロセッサで__ANDROID__が定義してあれば無効にするようにしただけですけどね。

libusbのcore.cの主な追加はこれ。バックエンドを呼び出すだけです。

libusbのandroid_usbfs.c(バックエンド)の主な変更点はこんな感じ。

__get_usbfs_fdは元々の_get_usbfs_fdを名前を変えただけです。出来るだけ互換性があるようにプログラムしたつもり。

これで再度コンパイルして動かすと・・・なんとなく動いてそうです\(^o^)/表示がまだなかったのでなんとなくです(汗)
そこでJNI・画面回りまで作って表示出来るようにしたのが4月半ば、ここまで実働3日ぐらいでした。これで油断してしまったのかも。
ソースを眺めているとまだまだ他にもclose(int fd)を呼んでいる場所も有って、実際いくつかの状況ではエラーコードが返ってくるのですが、とりあえず動くようになったので後は気長に行くことにします。
でもクラッシュ・ハングアップ連発します。特に終了させようとするとクラッシュかハングアップのどちらかが必ず起こっていました。数日間粘ってみましたが、この時点ではlibusb/libuvcの内部構造にまだまだ疎かったので、原因不明のクラッシュ・ハングアップにお手上げでした。

気分転換に処理速度向上対策をしてみる

本当はデバッグも全然なのでそもそも最適化などしている場合では無いのですが、原因不明のクラッシュ・ハングアップからの気分転換に最適化も試してみました。デバッグにしても最適化にしてもソースを読まないといけないのは一緒なんでそのついでですけどね(笑)
とりあえず過去の経験上比較的簡単に2〜3倍程度の速度向上が見込める、分岐命令の最適化(分岐予測の追加)を行いました。ARMに限らず今のCPUのほとんどはパイプライン処理によって、複数の命令を少しずつずらして並行実行することで処理速度の向上を図っています。分岐命令は先読みして並行実行している命令をチャラにしてしまい速度を一気に低下させる可能性があります。例えばARMのアーキテクチャの1つCortex-A8ではパイプラインが13段あるので、最大13命令分CPUがストールする可能性があります。
と言っても大したことをするわけではありません。印をつけといてgccにお願いするだけです。Linuxのカーネルでも使われている方法なのでそんなにリスクは高くありませんが、結構効果があります。
とりあえずこんなマクロを定義します。

if文とかで分岐させる際にその条件が成り立つ確率が9割とかそれ以上と予想されるならばLIKELYをくっつけてあげます。また成り立たない可能性が9割とかそれ以上(例えばメモリの確保に失敗するとかの例外処理への分岐)であればUNLIKLYをくっつけてあげます。
例えば、

みたいな部分を

にするだけです。例えばエラー処理のようなイレギュラーな処理は多少余分に時間がかかってしまってもしょうが無いと諦めて、正常に実行出来る場合を優先したコードを生成してもらうって事ですね。後はgccに最適化オプション-O2以上を付けてコンパイルすればOK。簡単ですね。ちなみに、デバッグ版でコンパイルすると最適化オプションがつかないので効果ありません。リリース版としてコンパイルすると-O2になるので、有効になるはずです。
今回のライブラリに関しては実際にプロファイリングして速度比較したわけではありませんので、本当のところどれぐらい効果があったのかはわかりませんけどね(^_^;)
後はブロックレベルでのメモリ転送が多いのでそこら辺りを最適化すればもう少し速くなるのかなぁとは思います。

クラッシュ・ハングアップ対策

一所懸命ソースを読んだ結果わかった事を大雑把に言うと、クラッシュの一番の原因は、複数のスレッドからアクセスされるオブジェクトが排他制御されているところとされていないところが混在していて、あるスレッドがアクセス中に別のスレッドが書き換えたり破棄しちゃう事があるってことです。少なくともlibuvcの方はバージョンも若くてまだまだ危ない所満載です。それでも動かせるようになるのがオープンソースのいいところですよね。
でもこれはおおごとです。実装的には良くないと知ってて今更構造の変更にまで踏み込めなくてわざと排他制御していないところも有るかもしれません。下手に排他制御するとデッドロックしてしまうかもしれないし、排他制御しないならクラッシュする、排他制御しないで済むように構造を変えると別のところがクラッシュ・デッドロックするかも・・・最悪だと1から作り直すのと変わらない手間が掛かってしまいます。
しょうが無いので、クラッシュしたところに印をつけといてそれ以外は危なそうでも現時点では基本的に無視することにしました(-_-;)まだまだたくさん危なそうなところが残っているので他の機種で動かしたりするとクラッシュしちゃうかも。動いたよ〜とかダメだったぁってコメントをいただけると嬉しいですm(__)m人柱募集中です(笑)

ちなみに、一番クラッシュが多発していたのは、_uvc_iso_callback@stream.cとframe.c内の関数です。_uvc_iso_callbackは、ユーザーコールバックを呼び出す関数、frame.c内の関数はユーザーコールバックへ引き渡すフレームデータの生成・複製・破棄およびピクセルフォーマット変換関数ですが、メモリへ排他制御せずにアクセスしている最中に他のスレッドにメモリを開放されてしまっていました。
_uvc_iso_callbackの方は色々試したのですが、全体を排他制御すると他の部分とコンフリクトして処理に時間がかかってしまうみたいなので、別の関数で行っていたメモリの開放処理を_uvc_iso_callbackの最後に移動してまとめて行うように変更してみました。
frame.cの方は、他スレッドで使ってるメモリを破棄されてしまうのに加えて、入力側のフレームがおかしい時が多々有るのです。元々出力側のフレームは確保しているバッファサイズ等をチェックしてあったのですが、入力側は未チェック。ただこの関数レベルではそれが異常値かどうかを知るのは不可能なので、範囲外への読み書きが生じないように範囲チェックを追加しました。

あと、おそらくエラー処理の不具合か非同期転送のタイミングのずれの可能性が高いと思うのですが、時々フレームがとんだり部分的にずれたりします。特にカメラ側の処理速度が遅い場合(低価格のカメラで暗い時とかオートフォーカスが動いている時とか)に起こりやすいようです。クラッシュに直結する部分は多少ごまかしの処理を入れましたが、それ以外の所はUSBのアナライザでも無いと手が出そうに無いので今は放ったらかしです。
とはいうものの、色々ゴニョゴニョした結果手持ちのテストできる機種ではクラッシュせずに実行できるようになったので、ソースを公開します。こちら(GitHub)
あと、いくつか未実装の機能があったので追加したのと、Android用にピクセルフォーマットをRGB565やRGBX8888へ変換する関数も追加しました(frame.c)。本当はGPU側で処理させるのがいいと思いますけど、とりあえず動かして見るには必要なので。
詳しくはソースを見てね。気力が復活すれば、細かいところをもう少し説明・修正するかもしれません。

ライセンスはApache License v2.0です。ただし、jni/libusb, jni/libuvc, jni/libjpeg下にあるファイルにはそれぞれ別々のライセンスが有りますのでご注意ください。
ということで今回はおしまいです。いや〜疲れたぁ。(´・ω・`)。お疲れ様でした。
ソースはこちら。GitHub

« »

  • スポンサードリンク

コメント

  • alin より:

    ご返信がありがとうございます。

    > 想像ですがおそらくは、USBの通信にエラーが発生しているんじゃないかと思います。
    今日は、もう一度確認して、Nexus7(2013, android 4.4.2)で、映像を表示できました。ケーブルを触ると、時々映像を表示できなくなったことがあります。

    > 後、お使いのカメラは何でしょうか?
    カメラ: logicool C920
    http://www.logicool.co.jp/ja-jp/product/hd-pro-webcam-c920
    ケーブル: SANWA AD-USB18
    http://www.sanwa.co.jp/product/syohin.asp?code=AD-USB18
    端末: Nexus7(2013) android 4.4.2

    別の話ですけど、実は、私は、サーバー中心(Rubyを使う)として、働いています。自分の知識範囲を広くしたいために、androidを勉強するつもり上に、動画技術を習得したいけど、まだ初心者の初心者です。この詳しい説明がありがとうございますが、理解のために、時間がかかるはずです。

    • saki より:

      映像が表示されたそうで良かったです。
      自分のところでは、
      カメラ: LogcoolC920HD, C270 / ELECOM UCAM-DLY300TA
      OTGケーブル: ノーブランド, L型(http://item.rakuten.co.jp/3aonlinestore/usb-mic20ll/)
      ELECOM TB-MAEMCB010BK
      端末: Nexus7(2012, Android4.4.2), Nexus5(Android 4.4.2〜4.4.4), Garaxy S3(SC-06D, Android 4.1.2)
      で動いています。
      でも、USBケーブルを延長したり間に変換コネクタとかを挟むと動かなくなります。PCでもケーブルを変えたり延長したりすると映像をとれなくなることがあるので、カメラ⇔端末間の電気特性が変わって信号が歪んだりしてしまうのではないかと・・・USBアナライザ挟むなりしてちゃんと調べればもう少しリカバリのしようも有るのかもしれません。

      自分の場合は、元々はPC(DOS/Windows)と組み込み(μItron他)がメインで、Basic・アセンブラ・C/C++・Delphiが殆どでした。今はJava・C/C++メインですね。動的言語はそんなに好きでは無いので、PHPがこのブログを作れる程度、極稀にperlって感じです。動画・映像系が特に得意と言うわけでも無いですが、参考になれば幸いです。

  • alin より:

    すばらしいですね!!!

    galaxy S4(android 4.3)に、試して、映像をうまく表示できました。すごいな!!!
    しかし、Nexus7(android 4.4.2)に、映像を表示できないです。 デバッグして、「failed start_streaming:Unknown error (-99)」のエラーがでたことがあります。
    エラーの原因を教えてくれませんか?

    • saki より:

      初めまして。コメントありがとうございます。

      Nexus7で映像が表示できないということですが、エラーコード-99と言うのは、厄介です。
      公開しているサンプルそのままであれば、libusbというライブラリ内で発生しています。
      どのような時にこのエラーが発生するかというと・・・「なぜかわからないけどエラーになっちゃった時」なのです。
      libusbの方は元々のライブラリから殆ど変更していないので・・・(-_-;)
      想像ですがおそらくは、USBの通信にエラーが発生しているんじゃないかと思います。
      経験的に怪しいのはOTGケーブルなんですが、OTGケーブル/USBのケーブル共に長いほどエラーが発生しやすいです。

      libusbの詳細エラーを見ればもしかすると何かわかるかもしれませんので、可能であれば、
      UVCPreview.cpp内のUVCPreview::prepare_preview関数の先頭に、
      libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_WARNING);
      という行を追加してNDKでビルドして、サンプルのlibs/armeabi-v7a/libUVCCamera.soと入れ替えて、logを確認していただけませんか?
      Javaからデバッグメッセージを切り替えれるようにしとけばよかったですねm(__)m

      なお、デバイその選択からプレビュー開始までの流れはおよそ次のようになっています。
      デバイスの選択(Java)
      パーミッションの取得(Java, 必要な場合)
      デバイスのオープン(Java & native)
      表示用Surfaceの割り当て(Java & native)
      プレビュー開始(Java & native)
      プレビュー表示用のスレッド生成(native)
      映像ストリームのネゴシエーション(native)
      ネゴシエーション結果の確認(native)
      ストリーミング開始(native)
      uvc_start_iso_streaming
      uvc_start_streaming
      uvc_stream_open_ctrl
      uvc_claim_if

      uvc_stream_ctrl

      uvc_stream_start
      libusb_submit_transfer

      failed start_streaming:Unknown error (-99)」であれば、uvc_start_streamingの内部でエラーになっています。

      もし、カメラからの映像取得をYUV422(YUV2)からMJPEGへ変更されているのであれば、
      JPEGのデコードに失敗している可能性もあります。
      この場合は、フレームデータを正常に受信出来てない(データが化けている)ということなので、
      1番目:OTGケーブル/USBのケーブル/端末/カメラの相性
      2番目:libjpegのコンパイルがうまく出来てない
      って感じかなぁ

      Nexus7(2012)であれば、手元のと同じなので動くはずなんですけど・・・
      Nexus7(2013)であれば、galaxy S4とだいたい同じCPU/GPUなので・・・
      後、お使いのカメラは何でしょうか?
      あまり参考にならなくてすみませんm(__)m

      ちなみに凄いのは元々のライブラリを公開されている方々です。
      自分はAndroid用にポーティングしただけなので(^_^;)