Webカメラの動画をキャプチャリング(その2)

車輪の再発見みたいな?」から、 「Webカメラの動画をキャプチャリング(その2)」を学ぼう。

間欠撮影した映像を1つの動画ファイルとして保存した~いその1

タイトル見ても何のこっちゃよくわかんない?(^_^;)

まえがき

諸般の事情でしばらく寝かせてた記事を公開することにしました(^_^)v

Androidで動画を生成保存するには、大きく分けて2つの方法があります。

1つはAndroidの初期からあるMediaRecorderクラスを使う方法で、もう1つはAndroid4.1(API>=16)で追加されたMedia Codec APIを使う方法です。MediaRecorder使う方法はネット上にも沢山資料が公開されているのでそちらをどうぞ。Media Codec APIを使う方法はMediaRecorderに比べると資料が少ないですが、このブログでも以前紹介させていただきました。例えばここらへんの記事になります。

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

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

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

音&動画の同時キャプチャがした0い(その1)の冒頭にも書いてある通り、単純に音声と映像を録画するだけであればMedia Codec APIを使うメリットはあまりありません。Media Codec APIを使うならではのメリットの1つ・・・ようするにMediaRecorderでは出来ない事の1つを今回から数回に分けて紹介します。

MediaRecorderでは、API>=11でMediaRecorder#setCaptureRateメソッドが追加され、映像取り込み時の回数(フレームレート)を遅くすることが出来るようになりました。要はタイムラプス的な録画が出来るようになったってことなのですが、残念なことに1秒間に数フレームぐらいまでしかフレームレートを遅くすることが出来ません。

もっと遅くしたい、数分0数時間毎に映像を取り込んで1つの動画ファイルにまとめたい。そんな事を考えたことのある人も居るとは思いますが、簡単には実現できません。そこでMedia Codec APIの出番です\(^o^)/(他の方法もありますが)

目標としては、

間欠的に撮影した動画を最終的に1つのファイルとして出力出来るようにする。

普通にMediaRecorderやMedia Codec APIを使う方法では、一度録画を停止すると追加して保存するなんてことは簡単には出来ません。でもそれを出来るようにしようじゃないかと(`・∀・´)!!

1回の撮影あたりのフレーム数を1フレームだけ限定せず間欠的に任意のフレーム数をとりこめるような仕様とする。

これはまぁ例えば画面にタッチしている間だけ録画して最終的に1つのファイルに出力したいなぁってことです。純粋なタイムラプス動画を作成するためだけに作ったわけではないです。

途中でアプリが終了されても大丈夫にする。

3つ目は大事です。サービスとして実装する方法もあるのですが、例えば何時間かに1度しか動かないのに、アプリが常駐して動き続けてるあるいはリソースを使ってるなんてのは、リソースの限られたAndroid機にとっては良くないですよね。

と言う事で、動画の生成中に任意のタイミングでpause/resume出来るようなMediaRecorderもどきをMedia Codec APIを使って作ってみましたぁ\(^o^)/ 間欠動作そのもの・・・指定時間間隔毎に実行するなんて機能は含んでないです。タイマーでもサービスでも使って好きなタイミングを作って下さい。

概要

んじゃまぁ概要から行きまっせぇ0(^o^)/

Media Codec APIを使って録音や録画をする場合には、普通はMediaCodecクラスを使ってエンコードした後そのデータをMediaMuxerクラスでファイルに書き込むと言う処理を行います。FFmpeg信者ならMediaMuxerの代わりにFFmpeg使うことも出来ます。

短時間の撮影でアプリは動きっぱなし、MediaCodecもMediaMuxerも動かしたままですむような用途であれば、例えばMediaCodecへ書き込む周期を調整&presentationTimeUsを適当な値にすればタイムラプス動画を生成することが出来ますが、何分・何十分・何時間も間が空くような間欠撮影だとそんなもったいないこと出来ません。と言うかいつ電話がかかってきてバックグランドへ回されたりローメモリーになってkillされてしまうかわからないスマホ/タブレットのアプリで長時間無駄にインスタンスを保ち続けるのはダメですよね。でもMediaMuxerクラスには既に存在している動画ファイルに追記していくような機能はありませんので、一旦終了してから何時間後かに新しいフレームを追加していくなんてことは出来ないのです。

一般的な動画ファイルフォーマット、例えばMP4とかでは内部にビットレートや画像サイズ、コーデックの種類等のメタデータ、映像トラックデータ、音声トラックデータ等を含み、ボックスと呼ばれる(規格によってはコンテナやチャンク、アトム、フレームなどとも呼ばれる)バイナリブロックが連なった構造をしています。そのために単純に映像データや音声データをフレーム毎に追記していくだけでは正しい動画フォーマットにならないのです。

むむぅ0(´・ω・`) 自分で動画ファイルの内部データを直接書き換えるようなクラスを作るとか、一旦再生(デコード自体は不要)しながらMediaMuxerへ出力して元動画のデータが無くなったとこから新しいフレームを追加していくとか、FFmpeg使うとかすればばもちろん出来るのですが・・・

じゃあどうすんねん

じゃぁどうすんねんというと、いくつか実装方法は考えつくと思いますが、今回はアプリが終了しても大丈夫なようにするためにオレオレフォーマットの中間ファイルを生成して、順次追記して最後に1つの動画ファイルにまとめるって方法にしました。ようは、MediaCodecでのエンコード処理と、MediaMuxerでの動画ファイルへの出力処理を別々に分けてしまえってことですね。一般の動画ファイルでは誰が再生するかわからないので様々な情報を特定の形式で保存する必要がありますが、中間ファイルは自分しか読み書きしないので好きなように書き込むことが出来ます。

ちなみに今回の実装とは違いますが、純粋にタイムラプス動画を作るだけ・・・例えば定期的に1フレームだけ保存(ようはコマ撮り)するのであれば各フレームを静止画として保存しておいて、後からMediaCodecへ書き込んでエンコード&MediaMuxerでファイル出力するって方が簡単です。でもこれだと画面タッチ中だけ録画って事をしようとするとタッチ中に大量の静止画を生成する羽目になって色々問題が有るんだもん(^_^;)

じゃぁコードを

って言う前に1つ大事な事を書いておきましょう(^o^)/

Android DevelopersのMediaCodecの説明を見ると、

#stop()

Finish the decode/encode session, note that the codec instance remains active and ready to be start()ed again.

って書いてあります。これを読むと#stopを呼んでエンコード/デコード処理を停止させた後に#startを呼び出して再開出来るような気になりませんか? 自分はなりました。でも真っ赤な大嘘です。実際には再開できずIllegalStateException例外を生成します。

AOSPのコードを見ると、#stopを呼び出すとINITIALIZEDステート/UNINITIALIZEDステート(生成した直後と同じ状態)になっていました。一方で#startを呼び出すには、CONFIGUREDステート(#configureが正常終了した状態)になっている必要があるのです。つまり、現在のMediaCodecの実装で者#stopと#reset(API>=21)は実質的には同じになっているってことです。だめじゃん(●`ε´●) と言う事で、#stopを呼び出した後は#configureからやり直さないとだめっです。

しかも#stopを呼び出してから少し時間を置いてからでないと#configureに失敗する機種もある・・・それもLogCatに出力されるだけで、#configureもそれ以降のメソッド呼び出しも例外すら生成せずに素通り(;_;) どおせぇってちゅうねん。

APIとしては再利用出来ると謳っているにも関わらず、#stopと#resetは実質的には同じ実装になってるってとこからしても実際には再利用は真剣にサポートする気の無い機能なのかもしれないですね。今回の実装では再利用はキッパリ諦めてreleaseしてしまうことにします。

その代わりといってはなんですが、今回の実装ではあまり短周期でのresume/pauseは不得意です。10数秒以上の間隔が開いているような場合がメインターゲットです。1秒に数フレーム以上保存したいのであれば普通にMediaRecorderとかMediaCodecを使って下さい。

ちなみに表向きは4ステートほどですが、実際のMediaCodec内では中間ステートも含めて11ステート定義されてあります。しかもメソッド呼び出しに伴う遷移以外に異常系の遷移もある(=実際には11ステートでは済まない)ので状態遷移表や状態遷移図にするのはかなり大変(T T) だからAndroid Developersを見てもMediaRecorderやMediaPlayerには状態遷移図があるのにMediaCodecには無いんだろうなぁきっと(-_-;)

今度こそコードを

と思ったけどやっぱり先にオレオレ中間ファイルの構造にしよう(^_^;)

ファイルの先頭から、ヘッダー、フォーマット、実際のフレームデータが並びます。

  • ヘッダー
  • コーデックの初期化データ。
  • MediaCodecの初期化に使用したMediaFormatをUTF文字列にシリアライズしたもの。

    同じ条件で初期化したMediaCodecでないと1つの動画ファイルにまとめることが難しいので、初回初期化時に保存しておいて、2回目以降のMediaCodec初期化にも同じMediaFormatを使えるようにします。

  • コーデックからの出力フォーマットデータ。
  • MediaCodec#getOutputFormatで取得したMediaFormatをUTF文字列にシリアライズしたもの。

    H.264(AVC)等では動画ファイルに出力する際にMediaCodec#getOutputFormatで取得したMediaFormatに含まれるcsd-1, csd-2が必要になります(PPSとかが入ってます)。MediaCodec初期化用のMediaFormatと同じでこれも同じでないと困るので初回取得時に保存しています。

  • フレームデータ
  • フレームデータ

ヘッダーは次の64バイトにしています。

  • シーケンス番号, 32ビット符号付き整数
  • resumeする度にインクリメントしています。

  • フレーム番号, 32ビット符号付き整数
  • フレーム時刻, 64ビット符号付き整数
  • System#nanoTimeで取得した各フレーム保存時の時刻です。

  • フレームサイズ, 32ビット符号付き整数。ヘッダーのサイズは不含
  • フラグ, 32ビット符号付き整数
  • 予約領域, 40バイト(32ビット整数で5つ分)

フレームデータはサイズ不定なので各フレーム毎に全てヘッダーをつけてその後に実際のビットストリームを書き込みます。

  • ヘッダー
  • 映像または音声のビットストリーム

作り始めはresumeの度に1つづつファイルを生成してましたが、最終的には1つのファイルに追記するだけにしました。ただし映像用と音声用は別ファイルです。

1つのファイルに追記していくんならシーケンス番号なんていらないんじゃないのって思うかもしれませんが、フレーム時刻を補正する際に必要です。シーケンス番号の変わり目でresume/pauseしたということなので、そこを起点にフレーム時刻をオフセットさせます。そうせずにもし単純にフレーム時刻をそのまま動画の各フレームの時刻にしてしまうと、仮にフレーム間が1時間空いていれば1時間もの間同じ映像が変わらず表示されてしまいます。

まぁシーケンス番号とフレーム番号をどちらか1個にまとめるのは出来ますけど最初の頃の名残&将来への備えと言う事で残しています。予約領域の40バイトも同じ理由ですが所詮はオレホレフォーマットなので辻褄さえ合わせていれば削除しても増やしてもOKですよ。

今度こそ今度こそコードを

と言ってもMediaCodec周りはいつもと変わらないんだよなぁ・・・後回しにしよう(^_^;)

代わりに中間ファイルから動画を生成する方をまず載せましょう。

どっか0ん\(^o^)/

@Override
public void run() {
	if (DEBUG) Log.v(TAG, "MuxerTask#run");
	boolean isMuxerStarted = false;
	try {
		final MediaMuxer muxer = new MediaMuxer(mMuxerFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
		if (muxer != null)
		try {
			int videoTrack = -1;
			int audioTrack = -1;
			DataInputStream videoIn = TLMediaEncoder.openInputStream(mMovieDir, TLMediaEncoder.TYPE_VIDEO, 0); // changeInput(null, 0, ++videoSequence);
			if (videoIn != null) {
				final MediaFormat format = TLMediaEncoder.readFormat(videoIn);
				if (format != null) {
					videoTrack = muxer.addTrack(format);
					if (DEBUG) Log.v(TAG, "found video data:format=" + format + "track=" + videoTrack);
				}
			}
			DataInputStream audioIn = TLMediaEncoder.openInputStream(mMovieDir, TLMediaEncoder.TYPE_AUDIO, 0); // changeInput(null, 1, ++audioSequence);
			if (audioIn != null) {
				final MediaFormat format = TLMediaEncoder.readFormat(audioIn);
				if (format != null) {
					audioTrack = muxer.addTrack(format);
					if (DEBUG) Log.v(TAG, "found audio data:format=" + format + "track=" + audioTrack);
				}
			}
			if ((videoTrack >= 0) || (audioTrack >= 0)) {
				if (DEBUG) Log.v(TAG, "start muxing");
				ByteBuffer videoBuf = null;
				MediaCodec.BufferInfo videoBufInfo = null;
				TLMediaEncoder.TLMediaFrameHeader videoFrameHeader = null;
				if (videoTrack >= 0) {
					videoBufInfo = new MediaCodec.BufferInfo();
					videoFrameHeader = new TLMediaEncoder.TLMediaFrameHeader();
				}
				ByteBuffer audioBuf = null;
				MediaCodec.BufferInfo audioBufInfo = new MediaCodec.BufferInfo();
				TLMediaEncoder.TLMediaFrameHeader audioFrameHeader = null;
				if (audioTrack >= 0) {
					audioBufInfo = new MediaCodec.BufferInfo();
					audioFrameHeader = new TLMediaEncoder.TLMediaFrameHeader();
				}
				byte[] readBuf = new byte[64 * 1024];
				isMuxerStarted = true;
				int videoSequence = 0;
				int audioSequence = 0;
				long videoTimeOffset = -1, videoPresentationTimeUs = -MSEC30US;
				long audioTimeOffset = -1, audioPresentationTimeUs = -MSEC30US;
				muxer.start();
				for (; mIsRunning && ((videoTrack >= 0) || (audioTrack >= 0)); ) {
					if (videoTrack >= 0) {
						if (videoIn != null) {
							try {
								videoBuf = TLMediaEncoder.readStream(videoIn, videoFrameHeader, videoBuf, readBuf);
										videoFrameHeader.asBufferInfo(videoBufInfo);
								if (videoSequence !=  videoFrameHeader.sequence) {
									videoSequence = videoFrameHeader.sequence;
									videoTimeOffset = videoPresentationTimeUs - videoBufInfo.presentationTimeUs + MSEC30US;
								}
								videoBufInfo.presentationTimeUs += videoTimeOffset;
								muxer.writeSampleData(videoTrack, videoBuf, videoBufInfo);
								videoPresentationTimeUs = videoBufInfo.presentationTimeUs;
							} catch (IllegalArgumentException e) {
								if (DEBUG) Log.d(TAG, String.format("MuxerTask:size=%d,presentationTimeUs=%d,",
											videoBufInfo.size, videoBufInfo.presentationTimeUs) + videoFrameHeader, e);
							} catch (IOException e) {
								videoTrack = -1;	// end
							}
						} else {
							videoTrack = -1;	// end
						}
					}
					if (audioTrack >= 0) {
						if (audioIn != null) {
							try {
								audioBuf = TLMediaEncoder.readStream(audioIn, audioFrameHeader, audioBuf, readBuf);
										audioFrameHeader.asBufferInfo(audioBufInfo);
								if (audioSequence !=  audioFrameHeader.sequence) {
									audioSequence = audioFrameHeader.sequence;
									audioTimeOffset = audioPresentationTimeUs - audioBufInfo.presentationTimeUs + MSEC30US;
								}
								audioBufInfo.presentationTimeUs += audioTimeOffset;
								muxer.writeSampleData(audioTrack, audioBuf, audioBufInfo);
								audioPresentationTimeUs = audioBufInfo.presentationTimeUs;
							} catch (IOException e) {
								audioTrack = -1;	// end
							}
						} else {
							audioTrack = -1;	// end
						}
					}
				}
				muxer.stop();
			}
			if (videoIn != null) {
				videoIn.close();
			}
			if (audioIn != null) {
				audioIn.close();
			}
		} finally {
			muxer.release();
		}
	} catch (Exception e) {
		Log.w(TAG, "failed to build movie file:", e);
		mIsRunning = false;
		synchronized (mSync) {
			if (mCallback != null) {
				mCallback.onError(e);
			}
		}
	}
	// remove intermediate files and its directory
	TLMediaEncoder.delete(mMovieDir);
	mBuilder.finishBuild(this);
	if (DEBUG) Log.v(TAG, "MuxerTask#finished");
	synchronized (mSync) {
		if (mCallback != null) {
			mCallback.onFinished(mIsRunning && isMuxerStarted ? mMuxerFilePath : null);
		}
	}
}

実行内容は簡単で次の4つです。

  • 普通にMediaMuxerを初期化します。
  • 音声と映像のそれぞれの中間ファルを開きます。
  • MediaCodecから#getOutPutFormatで出力フォーマットを取得する代わりにファイルから読み込んだ出力フォーマットを使って#addTrackします。
  • 後は音声・映像それぞれの中間ファイルからフレーム毎にデータを読み込んでMediaMuxerへ書き込んでいくのを繰り返す

作り始めた時はもしかすると音声と映像の時刻を自前で同期しながら書き込まないとダメかもって思ってましたが、気にせずに順に書き込んでいって大丈夫でした。ただし前に書いたとおり音声・映像それぞれでresume/pasueした時の時刻補正だけはしておかないとダメです。 最初はresume/pause毎にファイルを生成したので面倒でしたが、ファイルを1つにしてからは意外とすんなり出来てしまって拍子抜けでした。もしかすると普通にMediaCodecでエンコードしながらMediaMuxerへ書き込んでいくよりも簡単かもしれないです。

と言う事で今回はおしまい。サンプルプロジェクトは近いうちにGitHubへ上げま0す。

お疲れ様でした。

間欠撮影した映像を1つの動画ファイルとして保存した~いその2

あっ、どんなWebカメラからでもh264動画を取得できるわけではないからね。

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

スマホとかタブレットにUSBのWebCameraを繋ぎたいと思った事ありませんか?内蔵のカメラだと画面見ながら好きな方向に向けたり出来ないし。

でも公式にはAndroid3.1以降でUSBのキーボードとかマウスに対応しているだけでUSB WebCameraには対応していません。

Androidを核はLinuxなのでもちろんroot取ったりカーネルにデバイスドライバ組み込んだりすれば出来るんですけど、non-rootedでカメラ繋ぎたい。って事で、昔試してダメだったのを再挑戦してみました。

それなりに苦労したものの、動くようになったので公開します。

確認したのはNexus2012(Android4.4.2) & SC-06D(Android4.1.2)と、LogicoolのWebCamera C-910(1000万画素) & ElecomのUCAM0DLY300TA(300万画素)の組み合わせ。USBホスト機能の入っていないAndroid2.xでは動きません。Android3.1以降とUVC WebCameraの組み合わせなら動く可能性が高いとは思いますが、でも公式には出来ないことになっているし、色々とトリッキーなのでグルグル先生次第でいつ動かなくなるかもわからないので自己責任でお願いします。ちなみにAndroid3.x台は手持ちに無くて確認出来ないので一応4.x以降対応という事にします。

解決すべき課題

Android3.1以降の機種でnon-rooted AndroidでUSB Web Cameraへアクセスする際の問題点は、

  1. UVC(USB Video Class)対応のドライバが入っていない(普通のLinuxなら最近はドライバが入っているのでUSB WebCameraなんて楽ちんで使えますけどね)
  2. non-rootedだとNative CodeからUSBデバイスファイルにアクセスするパーミッションを設定・取得出来ない
  3. ユーザー空間からのアクセスなのでisochoronus転送に追随出来るだけの処理速度を確保できるか

の3つが大きな課題になります。ちなみに、Android3.1よりも前ではUSBホスト機能が入ってないので基本的にはrootを取らないとダメですので対象外です。

開発方針

  1. のドライバの件は、ユーザー空間からUSBデバイスとアクセするするためのライブラリlibusb(LGPLv3)と、libusbを使ってUVCデバイスとのアクセスをするためのライブラリlibuvc(BSD License)を使って頑張ります。それなりに手を加えないとダメでしたが、先輩たちに感謝。更にlibuvcのMJPEG対応用にlibjpeg(LGPLv2)を組み込むと・・・AndroidはApache License v2なのでオープンソースライセンスのオンパレードですね。閑話休題。
  2. のパーミッションの件は、rootがあれば解決する・・・と言うかroot取れるならドライバを組み込んでしまえば良い話なのですが、あくまでもnon-rootedで頑張ります。 グルグル先生に聞いたところでは、Java側でパーミッションを取得してからファイルディスクリプタをNative側へ渡せばなんとかなりそう、ということです。
  3. はNDKやらGPU(shader/RenderScript)やら何やらを駆使して…

ということで、いざ・・・やっぱりだめだったぁ(´・ω・`)。以前確かめた時と一緒でした。簡単に出来るならいっぱい出回っているよなぁ(-_-;)

でも、クラッシュダンプを解析したり色々調べていると、なんか行けそうって感じでしばらくだいぶ頑張ると・・・出来たぁ0\(^o^)/

実装

まずは、Java側から。USBデバイスの接続をモニターしたりパーミッションを要求したりopen/closeしたりをカプセル化したクラスを作りました。

こんな感じになります。

package com.serenegiant.usb;
public class USBMonitor {

	private static final String TAG = USBMonitor.class.getSimpleName();
	
	private static final String ACTION_USB_PERMISSION = "com.serenegiant.USB_PERMISSION";
	 
	private final HashMap mCtrlBlocks = new HashMap();

	private final Context mContext;
	private final UsbManager mUsbManager;	// API >= 12
	private PendingIntent mPermissionIntent;
	private final OnDeviceConnectListener mOnDeviceConnectListener;
	
	public interface OnDeviceConnectListener {
		/**
		 * called when device attached
		 * @param device
		 */
		public void onAttach(UsbDevice device);
		/**
		 * called when device dettach(after onDisconnect)
		 * @param device
		 */
		public void onDettach(UsbDevice device);
		/**
		 * called after device opend
		 * @param device
		 * @param connection 
		 */
		public void onConnect(UsbDevice device, UsbControlBlock ctrlBlock, boolean createNew);
		/**
		 * called when USB device removed or its power off (this callback is called before device closing)
		 * @param device
		 * @param ctrlBlock
		 */
		public void onDisconnect(UsbDevice device, UsbControlBlock ctrlBlock);
	}
	
	public USBMonitor(Context context, OnDeviceConnectListener listener) {
		mContext = context;
		mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
		mOnDeviceConnectListener = listener;
	}

	public void destroy() {
		unregister();
		synchronized (mCtrlBlocks) {
			Set keys = mCtrlBlocks.keySet();
			if (keys != null) {
				UsbControlBlock ctrlBlock;
				for (UsbDevice key: keys) {
					ctrlBlock = mCtrlBlocks.remove(key);
					ctrlBlock.close();
				}
			}
		}
	}

	/**
	 * register BroadcastReceiver to monitor USB events
	 * @param context
	 */
	public void register() {
		unregister();
		mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
		final IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
		filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
		filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
		mContext.registerReceiver(mUsbReceiver, filter);
	}
	
	/**
	 * unregister BroadcastReceiver
	 * @param context
	 */
	public void unregister() {
		if (mPermissionIntent != null) {
			mContext.unregisterReceiver(mUsbReceiver);
			mPermissionIntent = null;
		}
	}
	
	/**
	 * return all USB device list
	 * @return
	 */
	public List getDeviceList() {
		return getDeviceList(null);
	}

	/**
	 * return specified USB device list
	 * @param filter null returns all USB devices
	 * @return
	 */
	public ListgetDeviceList(DeviceFilter filter) {
		final HashMap deviceList = mUsbManager.getDeviceList();
		List result = null;
		if (deviceList != null) {
			result = new ArrayList();
			final Iterator iterator = deviceList.values().iterator();
			UsbDevice device;
			while(iterator.hasNext()){
			    device = iterator.next();
			    if ((filter == null) || (filter.matches(device))) {
					result.add(device);
				}
			}
		}
		return result;
	}
	/**
	 * get USB device list
	 * @return
	 */
	public Iterator getDevices() {
		Iterator iterator = null;
		final HashMap list = mUsbManager.getDeviceList();
		if (list != null)
			iterator = list.values().iterator();
		return iterator;
	}
	
	/**
	 * output device list to LogCat
	 */
	public void dumpDevices() {
		final HashMap list = mUsbManager.getDeviceList();
		if (list != null) {
			final Set keys = list.keySet();
			if (keys != null && keys.size() > 0) {
				for (String key: keys) {
					Log.i(TAG, "key=" + key + ":" + list.get(key));
				}
			} else {
				Log.i(TAG, "no device");
			}
		} else {
			Log.i(TAG, "no device");
		}
	}
	
	public boolean hasPermission(UsbDevice device) {
		return mUsbManager.hasPermission(device);
	}

	/**
	 * request permission to access to USB device 
	 * @param device
	 */
	public void requestPermission(UsbDevice device) {
		if (device != null) {
			if ((mPermissionIntent != null)
				&& !mUsbManager.hasPermission(device)) {

				mUsbManager.requestPermission(device, mPermissionIntent);
			} else if (mUsbManager.hasPermission(device)) {
				processConnect(device);
			}
		}
	}

	/**
	 * BroadcastReceiver for USB permission
	 */
	private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
		
		public void onReceive(Context context, Intent intent) {
			final String action = intent.getAction();
			if (ACTION_USB_PERMISSION.equals(action)) {
				final UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
				if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
					if (device != null) {
						processConnect(device);
					}
				} else {
					// failed to get permission
				}
			} else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
				// when USB device attached to Android device
				if (mOnDeviceConnectListener != null) {
					final UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
					mOnDeviceConnectListener.onAttach(device);
				}
			} else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
				// when device removed or power off
				final UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
				if (device != null) {
					UsbControlBlock ctrlBlock = null;
					synchronized (mCtrlBlocks) {
						ctrlBlock = mCtrlBlocks.remove(device);
					}
					if (ctrlBlock != null) {
						ctrlBlock.close();
					}
					if (mOnDeviceConnectListener != null) {
						mOnDeviceConnectListener.onDettach(device);
					}
				}
			}
		}
	};
	
	private final void processConnect(UsbDevice device) {
		boolean createNew = false;
		UsbControlBlock ctrlBlock; 
		synchronized (mCtrlBlocks) {
			ctrlBlock = mCtrlBlocks.get(device); 
			if (ctrlBlock == null) {
				ctrlBlock = new UsbControlBlock(device);
				mCtrlBlocks.put(device, ctrlBlock);
			}
		}
		if (mOnDeviceConnectListener != null) {
			mOnDeviceConnectListener.onConnect(device, ctrlBlock, createNew);
		}
	}
	
	public final class UsbControlBlock {
		private final UsbDevice mDevice;
		private UsbDeviceConnection mConnection;
		private final SparseArray mInterfaces = new SparseArray();

		/**
		 * this class needs permission to access USB device before constructing
		 * @param device
		 */
		public UsbControlBlock(UsbDevice device) {
			mDevice = device;
			mConnection = mUsbManager.openDevice(device);
		}

		public UsbDeviceConnection getUsbDeviceConnection() {
			return mConnection;
		}

		public int getFileDescriptor() {
			return mConnection != null ? mConnection.getFileDescriptor() : 0;
		}

		public byte[] getRawDescriptors() {
			return mConnection != null ? mConnection.getRawDescriptors() : null;
		}

		public int getVenderId() {
			return mDevice.getVendorId();
		}

		public int getProductId() {
			return mDevice.getProductId();
		}

		public UsbInterface open(int interfaceIndex) {
			UsbInterface intf = null;
			synchronized (mInterfaces) {
				intf = mInterfaces.get(interfaceIndex);
			}
			if (intf == null) {
				intf = mDevice.getInterface(interfaceIndex);
				if (intf != null) {
					synchronized (mInterfaces) {
						mInterfaces.append(interfaceIndex, intf);
					}
				}
			}
			return intf;
		}

		public void close(int interfaceIndex) {
			UsbInterface intf = null;
			synchronized (mInterfaces) {
				intf = mInterfaces.get(interfaceIndex);
				if (intf != null) {
					mInterfaces.delete(interfaceIndex);
					mConnection.releaseInterface(intf);
				}
			}
		}

		public void close() {
			if (mConnection != null) {
				if (mOnDeviceConnectListener != null) {
					mOnDeviceConnectListener.onDisconnect(mDevice, this);
				}
				synchronized (mInterfaces) {
					final int n = mInterfaces.size();
					int key;
					UsbInterface intf;
					for (int i = 0; i < n; i++) {
						key = mInterfaces.keyAt(i);
						intf = mInterfaces.get(key);
						mConnection.releaseInterface(intf);
					}
				}
				mConnection.close();
				mConnection = null;
			}
		}

		@Override
		protected void finalize() throws Throwable {
			close();
			super.finalize();
		}
	}

}

いきなり長げ~。でも、USBデバイスへのアクセスはほぼこのクラスで完結です。

使い方は、こんな感じに生成して#registerを呼び出すと、USBデバイスの接続に応じてイベントが飛んでくるのでそこでゴニョゴニョして、使い終わったら#unregisterを呼び出すだけ。

public class MainActivity extends Activity {

    private USBMonitor mUSBMonitor;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mUSBMonitor = new USBMonitor(this, mOnDeviceConnectListener);
	}

	@Override
	public void onResume() {
		super.onResume();
		mUSBMonitor.register();
	}

	@Override
	public void onPause() {
		mUSBMonitor.unregister();
		super.onPause();
	}

	private final OnDeviceConnectListener mOnDeviceConnectListener = new OnDeviceConnectListener() {
		@Override
		public void onAttach(UsbDevice device) {
			Toast.makeText(MainActivity.this, "USB_DEVICE_ATTACHED", Toast.LENGTH_SHORT).show();
		}

		@Override
		public void onConnect(UsbDevice device, final UsbControlBlock ctrlBlock, boolean createNew) {
			Toast.makeText(MainActivity.this, "USB_DEVICE_CONNECT", Toast.LENGTH_SHORT).show();
		}

		@Override
		public void onDisconnect(UsbDevice device, UsbControlBlock ctrlBlock) {
			Toast.makeText(MainActivity.this, "USB_DEVICE_DISSCONNECT", Toast.LENGTH_SHORT).show();
		}

		@Override
		public void onDettach(UsbDevice device) {
			Toast.makeText(MainActivity.this, "USB_DEVICE_DETACHED", Toast.LENGTH_SHORT).show();
		}

	};
}

長いの今回はここでおしまい。えっ物足りない?次回をお楽しみにね。

お疲れ様でした。

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

前回の記事AndroidにUSBでカメラを繋ぎた0い(^o^)/0その1の続きです。

USBデバイス一覧の取得

前回のUSBMonitorクラスを使って接続されているUSBデバイスを取得してみます。

既にUSBMonitorのインスタンスを生成してmUSBMonitorに保持しているとすると、

	List devices = mUSBMonitor.getDeviceList();

簡単ですね。簡単すぎて説明する気にもならへんけど(-_-;)

USBデバイス一覧の取得

でもこれだと接続されているUSBデバイスはUSBメモリだろうとキーボードだろうとなんでも返って来てしまいます。

どげんかしてフィルターしないといけんって事で、AndroidのSDKのソースの中から探してきて修正してDeviceFilterクラスを作りました。まぁ自前で実装しても対して代わり映えしなですけどね。大元はcom.android.server.usb.UsbSettingsManagerの中にあります。

これにxmlリソースを渡してDeviceFilterを生成してそれを使ってフィルター処理を行います。

	final List filter = DeviceFilter.getDeviceFilters(context, R.xml.device_filter);
	final List devices = mUSBMonitor.getDeviceList(filter.get(0)));
<?xml version="1.0" encoding="utf-8"?>
<usb>
	<usb-device class="239" subclass="2" />	<!-- all device of UVC -->
</usb>

ちなみに、xmlファイル内の「class=”239″ subclass=”2″」ってのがUVC Webカメラを指定しています。他にも特定のメーカーとかだけを選択するようにも指定出来ます。

選択ダイアログ

でも、もしかすると端末に複数のカメラを繋ぐ人もいるかも・・・選択できるようにしないとダメですね。選択ダイアログを出すようにしましょう。絶対に最大1台しか繋がへんねんって言うなら上のList devicesがnullで無い時に先頭のUsbDeviceを使えばいいです。

なお、ライブラリ自体は複数のカメラをサポートしていますが、Android端末自体の処理速度が間に合うかどうかは別問題です。

選択ダイアログはこんな感じ。

package com.serenegiant.usbcameratest;

public class CameraDialog extends DialogFragment {
	private static final String TAG = CameraDialog.class.getSimpleName();
	
	/**
	 * Helper method
	 * @param parent FragmentActivity
	 * @return
	 */
	public static CameraDialog showDialog(Activity parent/* add parameters here if you need */) {
		CameraDialog dialog = newInstance(/* add parameters here if you need */);
		try {
			dialog.show(parent.getFragmentManager(), TAG);
		} catch (IllegalStateException e) {
			dialog = null;
		}
    	return dialog;
	}

	public static CameraDialog newInstance(/* add parameters here if you need */) {
		final CameraDialog dialog = new CameraDialog();
		final Bundle args = new Bundle();
		// add parameters here if you need
		dialog.setArguments(args);
		return dialog;
	}
	
	protected USBMonitor mUSBMonitor;
	private Spinner mSpinner;
	private DeviceListAdapter mDeviceListAdapter;

	public CameraDialog(/* no arguments */) {
		// Fragment need default constructor
	}

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
       if (mUSBMonitor == null)
        try {
    		mUSBMonitor = ((MainActivity)activity).getUSBController();
        } catch (ClassCastException e) {
    	} catch (NullPointerException e) {
        }
		if (mUSBMonitor == null) {
        	throw new ClassCastException(activity.toString() + " must implement #getUSBController");
		}
	}

	@Override
    public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		if (savedInstanceState == null)
			savedInstanceState = getArguments();
	}

	@Override
	public void onSaveInstanceState(Bundle saveInstanceState) {
		final Bundle args = getArguments();
		if (args != null)
			saveInstanceState.putAll(args);
		super.onSaveInstanceState(saveInstanceState);
	}

	@Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
		final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
		builder.setView(initView());
    	builder.setTitle(R.string.select);
	    builder.setPositiveButton(android.R.string.ok, mOnDialogClickListener);
	    builder.setNegativeButton(android.R.string.cancel , mOnDialogClickListener);
	    builder.setNeutralButton(R.string.refresh, null);
	    final Dialog dialog = builder.create();
	    dialog.setCancelable(true);
	    dialog.setCanceledOnTouchOutside(true);
        return dialog;
	}

	/**
	 * create view that this fragment shows
	 * @return
	 */
	private final View initView() {
		final View rootView = getActivity().getLayoutInflater().inflate(R.layout.dialog_camera, null);
		mSpinner = (Spinner)rootView.findViewById(R.id.spinner1);
		final View empty = rootView.findViewById(android.R.id.empty);
		mSpinner.setEmptyView(empty);
		return rootView;
	}


	@Override
	public void onResume() {
		super.onResume();
		updateDevices();
	    final Button button = (Button)getDialog().findViewById(android.R.id.button3);
	    if (button != null) {
	    	button.setOnClickListener(mOnClickListener);
	    }
	}

	private final OnClickListener mOnClickListener = new OnClickListener() {
		@Override
		public void onClick(View v) {
			switch (v.getId()) {
			case android.R.id.button3:
				updateDevices();
				break;
			}
		}
	};
	
	private final DialogInterface.OnClickListener mOnDialogClickListener = new DialogInterface.OnClickListener() {
		@Override
		public void onClick(DialogInterface dialog, int which) {
			switch (which) {
			case DialogInterface.BUTTON_POSITIVE:
				final Object item = mSpinner.getSelectedItem();
				if (item instanceof UsbDevice) {
					mUSBMonitor.requestPermission((UsbDevice)item);
				}
				break;
			}
		}
	};

	public void updateDevices() {
//		mUSBMonitor.dumpDevices();
		final List filter = DeviceFilter.getDeviceFilters(getActivity(), R.xml.device_filter);
		mDeviceListAdapter = new DeviceListAdapter(getActivity(), mUSBMonitor.getDeviceList(filter.get(0)));
		mSpinner.setAdapter(mDeviceListAdapter);
	}
	
	private static final class DeviceListAdapter extends BaseAdapter {

		private final LayoutInflater mInflater;
		private final List mList;

		public DeviceListAdapter(Context context, Listlist) {
			mInflater = LayoutInflater.from(context);
			mList = list != null ? list : new ArrayList();
		}

		@Override
		public int getCount() {
			return mList.size();
		}

		@Override
		public UsbDevice getItem(int position) {
			if ((position >= 0) && (position < mList.size()))
				return mList.get(position);
			else
				return null;
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			if (convertView == null) {
				convertView = mInflater.inflate(R.layout.listitem_device, parent, false);
			}
			if (convertView instanceof CheckedTextView) {
				final UsbDevice device = getItem(position);
				((CheckedTextView)convertView).setText(
					String.format("UVC Camera:(%x:%x)", device.getVendorId(), device.getProductId()));
			}
			return convertView;
		}
	}
}

先ほどのフィルター処理を使って接続されているUSBカメラの一覧を取得して専用のAdapterへセットしてSpinnerで選択するようにしています(133-135行)。USBで接続できる台数なんてしれているので毎回生成しても大したこと無いという言い訳をしながら、横着して毎回Adapterを生成しています。まっとうに作るなら#setDevicesみたいなメソッドを作って、変更されたdeviceだけを更新してからAdapter#notifyDataSetChangedを呼び出すべきでしょうね。

もしかすると、ダイアログ表示時にカメラを繋いでいないかもしれないので、「更新」ボタンも付けてみました。ここはちょっとしょうもないテクニックが有ります。普通にAlertDialog.Builder#setPositiveButton/#setNegativeButton/#setNeutralButtonにコールバックを渡してクリックした時の処理をしようとすると、問答無用でダイアログが閉じてしまいます。まぁ表示しているView内にボタンをつければいいだけなんですけど、コールバックの返り値にtrueを返すと閉じるとかにしてくれればよかったのに・・・と愚痴を言っても始まらないので、どげんか・・・もういいですね(-_-;)

どうするかというと、AlertDialog.Builder#setPositiveButton/#setNegativeButton/#setNeutralButtonを呼び出した時に表示されるボタンには実はidが付いています。それぞれandroid.R.id.button1/android.R.id.button2/android.R.id.button3になっています。なので、必要なボタンのidからfindViewByIdでボタンのインスタンスを取得してそれに#setOnClickListenerします(97-100行)。こうすると標準の処理を上書きしてクリックしてもダイアログを閉じないようにすることが出来ます。もしこのlistener内でダイアログを閉じたい状況になるのであればDialog#dismissやDialog#cancelを呼べばOKです。でも標準の処理を上書きするということは標準のデザインパターンから外れることにも繋がるのでよく考えてから実装しましょう。閑話休題。

本当ならベンダー名・製品名・シリアル番号等を取得して表示すればいいんですが、今回は省略。ベンダーIDと製品IDを表示するだけです。

パーミッションの要求(とUSBデバイスのopen)

無事ダイアログでカメラを選択できれば、ダイアログを閉じる時にパーミッションの要求をします。121行目になります。

	mUSBMonitor.requestPermission((UsbDevice)item);

USBMonitor#requestPermissionでは、既にパーミッションが有ればそのままopenし無ければパーミッションを要求します。パーミッションを持っているかどうかを確認するには、UsbManager#hasPermission(UsbDevice device)メソッドを使います。また、パーミッションを要求するにはUsbManager#requestPermission(UsbDeveice device, Intent intent)を使います。

requestPermissionUsbManager#requestPermissionが呼ばれると、AndroidのOSはパーミッションが有るかどうかを確認して、無ければユーザーに許可を求めるダイアログを表示します。右の画像みたいなやつです。ダイアログで[OK]とすると、無事パーミッションゲットです。USBデバイスを取り外すまではパーミッションが有効になります。

パーミッションが取得できると、USBMinitor内でUSBデバイスのopen処理を行ってからOnDeviceConnectListener#onConnectコールバックが呼ばれるので、ここでUSBカメラへの接続処理を行います。


パーミッションの要求画面

ようやく登場!USBカメラへの接続

USBカメラへの接続処理はUVCCameraクラスで行います。こんな感じです。

package com.serenegiant.usb;
public class UVCCamera {

	private static final String TAG = UVCCamera.class.getSimpleName();

	private static boolean isLoaded;
	static {
		if (!isLoaded) {
			System.loadLibrary("uvc");
			System.loadLibrary("UVCCamera");
			isLoaded = true;
		}
	}

	private UsbControlBlock mCtrlBlock;
    protected long mNativePtr;	// this field is accessed from native code and do not change name and remove

    /**
     * the sonctructor of this class should be call within the thread that has a looper
     * (UI thread or a thread that called Looper.prepare)
     */
    public UVCCamera() {
    	mNativePtr = nativeCreate();
	}

    /**
     * connect to a UVC camera
     * USB permission is necessary before this method is called
     * @param ctrlBlock
     */
    public void open(UsbControlBlock ctrlBlock) {
    	mCtrlBlock = ctrlBlock;
    	nativeConnect(mNativePtr,
       		mCtrlBlock.getVenderId(), mCtrlBlock.getProductId(),
       		mCtrlBlock.getFileDescriptor());
    }

    /**
     * close and release UVC camera
     */
    public void close() {
    	if (mNativePtr != 0) {
    		nativeRelease(mNativePtr);
    	}
   		mCtrlBlock = null;
    }

    /**
     * set preview surface with SurfaceHolder
* you can use SurfaceHolder came from SurfaceView/GLSurfaceView * @param holder */ public void setPreviewDisplay(SurfaceHolder holder) { nativeSetPreviewDisplay(mNativePtr, holder.getSurface()); } /** * set preview surface with SurfaceTexture. * this method require API >= 14 * @param texture */ public void setPreviewTexture(SurfaceTexture texture) { // API >= 11 final Surface surface = new Surface(texture); // XXX API >= 14 nativeSetPreviewDisplay(mNativePtr, surface); } /** * set preview surface with Surface * @param Surface */ public void setPreviewDisplay(Surface surface) { nativeSetPreviewDisplay(mNativePtr, surface); } /** * start preview */ public void startPreview() { if (mCtrlBlock != null) { nativeStartPreview(mNativePtr); } } /** * stop preview */ public void stopPreview() { if (mCtrlBlock != null) { nativeStopPreview(mNativePtr); } } /** * destroy UVCCamera object */ public void destroy() { close(); if (mNativePtr != 0) { nativeDestroy(mNativePtr); mNativePtr = 0; } } // #nativeCreate and #nativeDestroy are not static methods. private final native long nativeCreate(); private final native void nativeDestroy(long id_camera); private static final native int nativeConnect(long id_camera, int venderId, int productId, int fileDescriptor); private static final native int nativeRelease(long id_camera); private static final native int nativeStartPreview(long id_camera); private static final native int nativeStopPreview(long id_camera); private static final native int nativeSetPreviewDisplay(long id_camera, Surface surface); }

ようやく登場なんて事をほざいた割には全然大したことをしてませんね。Java側の引数をnativeメソッドへ引き渡すだけです。全ては闇の中・・・じゃなかった、native codeの中にあります。mNativePtrはNative側オブジェクトの実体を指すポインタです。

このクラスの一番のキーは35行目で呼び出しているUSBMonitor#getFileDescriptorに有ります。実体はUsbDeviceConnection#getFileDescriptorです。ここでパーミッションを取得できたUSBデバイスファイルのファイルディスクリプタを取得してnative code側へ引き渡すことで、root無しでnative codeからUSBデバイスへアクセスできるようになります。

ちなみにこのクラスの、public void setPreviewTexture(SurfaceTexture texture)メソッドはAPI14(Android4.0)以降対応となります。

これを取り除けばAPI12以降で動くんじゃないかなって思います(SurfaceTextureはAPI11以降対応だけどUSBホスト機能がAPI12以降なので)。

いよいUSBカメラのopen処理\(^o^)/

native code側は別にまとめてするとして、Java側のopen処理を載せますね。

前回載せたOnDeviceConnectListenerを修正してUVCCameraオブジェクトのopen処理を行うようにしました。

#onAttachと#onDettachは特にすることも無いので単にToastでメッセージを表示するだけにしました。

	private final OnDeviceConnectListener mOnDeviceConnectListener = new OnDeviceConnectListener() {
		@Override
		public void onAttach(UsbDevice device) {
			Toast.makeText(MainActivity.this, "USB_DEVICE_ATTACHED", Toast.LENGTH_SHORT).show();
		}

		@Override
		public void onConnect(UsbDevice device, final UsbControlBlock ctrlBlock, boolean createNew) {
			if (mUVCCamera != null)
				mUVCCamera.destroy();
			mUVCCamera = new UVCCamera();
			EXECUTER.execute(new Runnable() {
				@Override
				public void run() {
					mUVCCamera.open(ctrlBlock);
					mUVCCamera.setPreviewTexture(mUVCCameraView.getSurfaceTexture());
					mUVCCamera.startPreview();
				}
			});
		}

		@Override
		public void onDisconnect(UsbDevice device, UsbControlBlock ctrlBlock) {
			// XXX you should check whether the comming device equal to camera device that currently using
			if (mUVCCamera != null) {
				mUVCCamera.close();
			}
		}

		@Override
		public void onDettach(UsbDevice device) {
			Toast.makeText(MainActivity.this, "USB_DEVICE_DETACHED", Toast.LENGTH_SHORT).show();
		}

	};

内蔵カメラのopen処理と同じで、USBカメラでもopen処理は時間がかかる可能性が有る・・・と言うか実際に時間がかかるので別スレッドに投げて処理をしています。例によっていつものスレッドプールです。まぁスレッドプールにする必要性はかけらほども無くって普通にThread+Runnableでいいんですけどね。

あっそうそう、今回のサンプルではUVCCamera#setPreviewTextureを呼び出しているのでSurfaceTextureをどないかして生成しないといけません。自前で生成するか、TextureViewを使いましょう。今回はTextureViewを使っています。また、UVCCamera#setPreviewDisplayを使うならSurfaceまたはSurfaceHolderが必要になります。この場合は、SurfaceViewまたはGLSurfaceViewから取得すればOKです。

残念ながらnative codeにはたどり着いていませんが、もう1万2千文字を超えてしました。五千文字一区切りのつもりなのにコードを入れると一気に文字数が・・・(-_-;)。と言うことで今回はここまで。

お疲れ様でした。

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

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

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

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

そこで先輩魔法使いにならって、libusb/libuvcへファイルディスクリプタを引き渡すようにしてみました。

元々libusbにはopenするために

int LIBUSB_CALL libusb_open(libusb_device *dev, libusb_device_handle **handle);

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

int LIBUSB_CALL libusb_open_with_fd(libusb_device *dev, libusb_device_handle **handle, int fd);

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

uvc_error_t uvc_open(uvc_device_t *dev, uvc_device_handle_t **devh);

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

uvc_error_t uvc_open_with_fd(uvc_device_t *dev, uvc_device_handle_t **devh, int fd);

を作り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の主な追加はこれ。バックエンドを呼び出すだけです。

int API_EXPORTED libusb_set_device_fd(libusb_device *dev, int fd) {
	return usbi_backend->set_device_fd(dev, fd);
}

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

static int op_set_device_fd(struct libusb_device *device, int fd) {
	struct linux_device_priv *dpriv = _device_priv(device);
	dpriv->fd = fd;
	return 0;
}

static int _get_usbfs_fd(struct libusb_device *device, mode_t mode, int silent) {
#ifdef __ANDROID__
	struct linux_device_priv *dpriv = _device_priv(device);

	if (LIKELY(dpriv->fd > 0))
		return dpriv->fd;
	else {
		// fall back to original _get_usbfs_fd function
		// but this call will fail on Android devices without root
		usbi_dbg("fd have not set yet. device=%x,fd=%d", (int )device, dpriv->fd);
		return __get_usbfs_fd(device, mode, silent);
	}
#else
	return __get_usbfs_fd(device, mode, silent);
#endif
}

static void op_close(struct libusb_device_handle *dev_handle) {
	int fd = _device_handle_priv(dev_handle)->fd;
	usbi_remove_pollfd(HANDLE_CTX(dev_handle), fd);
#ifndef __ANDROID__
	// We can not (re)open USB device in the native code on no-rooted Android devices
	// so keep open and defer real open/close operation on Java side
	close(fd);
#endif
}

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

これで再度コンパイルして動かすと・・・なんとなく動いてそうです\(^o^)/表示がまだなかったのでなんとなくです(汗)

そこでJNI・画面回りまで作って表示出来るようにしたのが4月半ば、ここまで実働3日ぐらいでした。これで油断してしまったのかも。 ソースを眺めているとまだまだ他にもclose(int fd)を呼んでいる場所も有って、実際いくつかの状況ではエラーコードが返ってくるのですが、とりあえず動くようになったので後は気長に行くことにします。

でもクラッシュ・ハングアップ連発します。特に終了させようとするとクラッシュかハングアップのどちらかが必ず起こっていました。数日間粘ってみましたが、この時点ではlibusb/libuvcの内部構造にまだまだ疎かったので、原因不明のクラッシュ・ハングアップにお手上げでした。

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

本当はデバッグも全然なのでそもそも最適化などしている場合では無いのですが、原因不明のクラッシュ・ハングアップからの気分転換に最適化も試してみました。デバッグにしても最適化にしてもソースを読まないといけないのは一緒なんでそのついでですけどね(笑)

とりあえず過去の経験上比較的簡単に203倍程度の速度向上が見込める、分岐命令の最適化(分岐予測の追加)を行いました。ARMに限らず今のCPUのほとんどはパイプライン処理によって、複数の命令を少しずつずらして並行実行することで処理速度の向上を図っています。分岐命令は先読みして並行実行している命令をチャラにしてしまい速度を一気に低下させる可能性があります。例えばARMのアーキテクチャの1つCortex-A8ではパイプラインが13段あるので、最大13命令分CPUがストールする可能性があります。

と言っても大したことをするわけではありません。印をつけといてgccにお願いするだけです。Linuxのカーネルでも使われている方法なのでそんなにリスクは高くありませんが、結構効果があります。

とりあえずこんなマクロを定義します。

#define  LIKELY(x)	__builtin_expect(!!(x), 1)	// x is likely true
#define  UNLIKELY(x)	__builtin_expect(!!(x), 0)	// x is likely false

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

例えば、

void *ptr = malloc(100);
if (!ptr) {
// エラー処理
}

みたいな部分を

void *ptr = malloc(100);
if (UNLIKELY(!ptr)) {
// エラー処理
}

にするだけです。例えばエラー処理のようなイレギュラーな処理は多少余分に時間がかかってしまってもしょうが無いと諦めて、正常に実行出来る場合を優先したコードを生成してもらうって事ですね。後はgccに最適化オプション-O2以上を付けてコンパイルすればOK。簡単ですね。ちなみに、デバッグ版でコンパイルすると最適化オプションがつかないので効果ありません。リリース版としてコンパイルすると-O2になるので、有効になるはずです。

今回のライブラリに関しては実際にプロファイリングして速度比較したわけではありませんので、本当のところどれぐらい効果があったのかはわかりませんけどね(^_^;)

後はブロックレベルでのメモリ転送が多いのでそこら辺りを最適化すればもう少し速くなるのかなぁとは思います。

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

一所懸命ソースを読んだ結果わかった事を大雑把に言うと、クラッシュの一番の原因は、複数のスレッドからアクセスされるオブジェクトが排他制御されているところとされていないところが混在していて、あるスレッドがアクセス中に別のスレッドが書き換えたり破棄しちゃう事があるってことです。少なくともlibuvcの方はバージョンも若くてまだまだ危ない所満載です。それでも動かせるようになるのがオープンソースのいいところですよね。

でもこれはおおごとです。実装的には良くないと知ってて今更構造の変更にまで踏み込めなくてわざと排他制御していないところも有るかもしれません。下手に排他制御するとデッドロックしてしまうかもしれないし、排他制御しないならクラッシュする、排他制御しないで済むように構造を変えると別のところがクラッシュ・デッドロックするかも・・・最悪だと1から作り直すのと変わらない手間が掛かってしまいます。

しょうが無いので、クラッシュしたところに印をつけといてそれ以外は危なそうでも現時点では基本的に無視することにしました(-_-;)まだまだたくさん危なそうなところが残っているので他の機種で動かしたりするとクラッシュしちゃうかも。動いたよ0とかダメだったぁってコメントをいただけると嬉しいです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



お花畑2