サンプルプロジェクトについて
GitHubにサンプルプロジェクトをアップしました。待っててくれた人居るかな?
リンクはこちらです。
プログラムの説明の前に、サンプルプロジェクトのアプリについて説明をしておきましょう。
ビルドして実行すると問答無用で0番のカメラをopenして全画面でのプレビュー表示になります。プレビューではcrop in center表示にしていますので、出力される動画とプレビュー画面では少し画角が違うかもしれません。
インカメラ・アウトカメラの両方が付いている機種の場合は大抵アウトカメラ、Nexus7(2012)の様にインカメラしか無ければインカメラがopenするはずです。0番以外のカメラを使いたいとか、切り替えれるようにしたい方はご自分でお願いします。
画面中央のカメラアイコンをタッチすると、カメラアイコンが白色から黄色に変わります。この状態はいつでも録画出来る状態ですが、実際の録画は行ってません。
続いてカメラアイコン以外の画面のどこかにタッチするとカメラアイコンが赤色に変わりタッチしている間だけ録画を行います。画面から指を離すと録画中断します。
カメラアイコンが黄色の時にカメラアイコンにタッチすると録画終了です。実際の動画ファイルへの出力は録画終了時に別スレッドで行います。長時間の録画だと実際に再生可能になるまで少し時間がかかるかもしれません。
なお、プレビュー表示中に端末を回転させると画面も回転しますが、録画中(カメラアイコンが黄色または赤色)では画面の回転を禁止しています(録画中に縦横のサイズが変わると正常に録画・再生できなくなってしまうため)。Android標準のカメラアプリと同様の挙動です。
第2弾の始まり
てなことで、間欠撮影した映像を1つの動画ファイルとして保存したーいの第2弾始まり始まり〜( ゚Д゚ノノ☆パチパチパチパチ
概要については以前の記事を参照して下さい。これ
中間ファイルから動画ファイルへの書き出し部分のコードを前回の最後に載せました。MediaCodec周りは今までに何度も記事にしているのでパスしますが、1つだけ重要なのが録画時刻の計算部分です。
前回のコードから抜粋すると次の部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
... long videoTimeOffset = -1, videoPresentationTimeUs = -MSEC30US; long audioTimeOffset = -1, audioPresentationTimeUs = -MSEC30US; ... if (videoSequence != videoFrameHeader.sequence) { videoSequence = videoFrameHeader.sequence; videoTimeOffset = videoPresentationTimeUs - videoBufInfo.presentationTimeUs + MSEC30US; } videoBufInfo.presentationTimeUs += videoTimeOffset; ... if (audioSequence != audioFrameHeader.sequence) { audioSequence = audioFrameHeader.sequence; audioTimeOffset = audioPresentationTimeUs - audioBufInfo.presentationTimeUs + MSEC30US; } audioBufInfo.presentationTimeUs += audioTimeOffset; ... |
前回説明したとおりオレオレ中間ファイルにはシーケンス番号というのが保存してあります。これは、1つの中間ファイルに複数回の中断を含めることが出来るようにする方法の1つですが、録画開始/再開時には必ずシーケンス番号を+1するようにしているのでシーケンス番号が変化した時=録画中断&再開した時となります。
録画中断から次の録画再開までの時間は任意(今回のサンプルプロジェクトでは画面から指を離してから次にタッチするまでの時間)ですが、動画ファイルに書き込む際には間の時間をスキップして保存しなければなりません。一方中間ファイルにフレーム毎に記録されている時刻はその時のシステム時刻(System#nanoTimeで取得したlong値)にしたので最後に保存したフレーム時刻と次に保存しようとしているフレーム時刻から中断時間を計算することができます。
そこで各録画再開(開始)時の先頭時刻のオフセット値をvideoTimeOffset(映像用)およびaudioTimeOffset(音声用)として保持して、各フレーム時刻に加算することで録画休止時間をスキップするようにしました。
言葉じゃわかんないかもしれないので気になる方は実際に数字を入れて計算してみてください。初回はちゃんと中間ファイルに記録されているフレーム時刻になるようにしてあります。
なお、MediaCodecのpresentationTimeUsについての重要な制約事項として「単調増加な時刻を与えなければならない」というのがあります。手動で録画中断・再開する分には中断・再開時に必ず時間のズレが生じて同時あるいは時刻が戻る事は無いはずですが念の為に適当に30マイクロ秒の下駄を履かせてます。それが謎の定数MSEC30USです。なので1回の中断毎に30マイクロ秒ずつ再生時刻が後ろにずれていくことになりますが、そもそもタッチ開始・中断処理自体に数ミリ秒以上はかかるのでまぁ気にしないでください。
後は特に説明しなくてもわかると思いますが、MediaMuxerの初期化(addTrack)はMediaCodecから取得して保存しておいたMediaFormatを使っています。
また、MediaCodecでのエンコードとMediaMuxerでのファイル出力を同時進行する際には、特に録音録画を同時にする場合には、それぞれのstartのタイミングとか同期が面倒でしたが既にエンコードは済んでいるので前から順に読んでは書くの繰り返しです。
まぁそういう風に処理できるように中間ファイルに書き込んでいるからこそですが。
ちなみに、中間ファイルに書き出しておいてそれを後で動画ファイルに出力し直すという処理の都合上出力動画ファイルサイズの最大で2倍のストレージ容量が必要になります。何時間も録画して数GBにもなるような用途には向かないかなぁ(^_^;)
中間ファイルの生成
今回の中間ファイル生成用のエンコーダーの実行は全てワーカースレッド上で動くようにしました。理由?そうしてみたかったから〜\(^o^)/ 地下も中断、再開があって状態が複雑なのでわかりやすいようにステートマシン風にしてみました。理由?そうしてみたかったから〜(笑) あっでも、もちろんコンストラクタやパラメータの設定は呼び出し側のスレッドで動きますけどね。
ステートは中間状態も含めて次の8つ。もっとも中間状態に居る時間が殆ど無いので中間状態を省略しても良かったかも(^^;) それぞれの意味は読んで字の如し、百聞は一見に如かず。
- STATE_RELEASE
- STATE_INITIALIZED
- STATE_PREPARING
- STATE_PREPARED
- STATE_PAUSING
- STATE_PAUSED
- STATE_RESUMING
- STATE_RUNNING
クラス構成はこんな感じにしました。
—-TLMediaMovieBuilder
—-TLMediaEncoder
|
—-TLMediaoVideoEncoder
|
—-TLAbstractMediaAudioEncoder—-TLMediaoAudioEncoder
いつものように基底クラスでエンコーダーとしての状態遷移とかエンコード済みデータのファイルへの出力等を行って、子クラスでは映像または音声固有の追加処理を行います。音声側は間に更に抽象クラスを挟んでますが…あまり大した意味はありません^^;1個にまとめてしまっても大丈夫です。なぜ分けたの?分けたかったから〜^^/ もうええっちゅうねん。
エンコーダースレッドの実体はRunnableとして定義しておいてコンストラクタで生成したスレッドに渡して実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
private final Runnable mEncoderTask = new Runnable() { @Override public void run() { int request = REQUEST_NON; if (DEBUG) Log.v(TAG, "#run"); mIsRunning = true; setState(STATE_INITIALIZED, null); for (; mIsRunning; ) { if (request == REQUEST_NON) { // if there is no handling request request = waitRequest(); // wait for next request } if (request == REQUEST_STOP) { handlePauseRecording(); mIsRunning = false; break; } if (mState == STATE_RUNNING) { request = handleRunning(request); } else { if (request == REQUEST_DRAIN) { request = REQUEST_NON; // just clear request removeRequest(REQUEST_DRAIN); continue; } switch (mState) { case STATE_RELEASE: setState(STATE_RELEASE, new IllegalStateException("state=" + mState + ",request=" + request)); mIsRunning = false; continue; case STATE_INITIALIZED: request = handleInitialized(request); break; case STATE_PREPARING: request = handlePreparing(request); break; case STATE_PREPARED: request = handlePrepared(request); break; case STATE_PAUSING: request = handlePausing(request); break; case STATE_PAUSED: request = handlePaused(request); break; case STATE_RESUMING: request = handleResuming(request); break; default: } // end of switch (mState) } } // end of for mIsRunning if (DEBUG) Log.v(TAG, "#run:finished"); setState(STATE_RELEASE, null); // internal_release all related objects internal_release(); } }; |
ちょっとイレギュラーなところもありますが、それぞれのステート毎に要求コマンドを処理して状態遷移していきます。ベタにswitch/case文を入れ子にしてもいいんですが巨大になってしまうので、わかりやすくステート毎に対応するメソッドへ飛ばしてます。イレギュラーな構造になっているところは主にswitch/caseの入れ子を少なくして高頻度に呼ばれる所を別処理することで実行の効率化をするためです。このクラスで一番たくさん呼び出されるはずのところはエンコード処理とコマンド待ちなので#handleRunningとSTATE_NONはコマンド待ちの状態は特別扱い。また停止要求はASAPなので特別扱いになってます。
各ステート毎の処理は次のような感じになります。例としてSTATE_PREPARINGとSTATE_PREPAREDを載せます。STATE_PREPARINGは中間状態なので、必要な処理を実行して実行が終われば他のステートに自発的に遷移します。
一方STATE_PREPAREDは自発的に遷移することは無く何らかのコマンドが発行されるまで待機することになりますので、次のコマンドが来た時の状態遷移のためのswitch/case文だけになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
private final int handlePreparing(int request) { if (DEBUG) Log.v(TAG, "STATE_PREPARING"); request = REQUEST_NON; try { checkLastSequence(); if (mConfigFormat == null) mConfigFormat = internal_prepare(); if (mConfigFormat != null) { setState(STATE_PREPARED, null); } else { setState(STATE_INITIALIZED, new IllegalArgumentException()); } callOnPrepared(); } catch (IOException e) { setState(STATE_INITIALIZED, e); } return request; } private final int handlePrepared(int request) { if (DEBUG) Log.v(TAG, "STATE_PREPARED"); switch (request) { case REQUEST_PREPARE: request = REQUEST_NON; // just clear request break; case REQUEST_RESUME: setState(STATE_RESUMING, null); break; case REQUEST_PAUSE: setState(STATE_PAUSING, null); break; default: setState(STATE_INITIALIZED, new IllegalStateException("state=" + mState + ",request=" + request)); request = REQUEST_NON; } return request; } |
そうだ、要求コマンドも載せとかなきゃ^^この6つです。これも読んで字の如し、百聞は一見にしかずですので説明は無し。
- REQUEST_NON
- REQUEST_PREPARE
- REQUEST_RESUME
- REQUEST_STOP
- REQUEST_PAUSE
- REQUEST_DRAIN
REQUEST_NON以外には対応するパブリックメソッドがあって、それぞれ#preapre, #resume, #stop, #pause, #onFrameAvailableになってます。一、ニ、三…でも四ってのと同じで最後だけパターンが違う〜^^
要求コマンドはLinkedBlockingDequeで受け渡しをします。FIFO・・・原則として先入れ先出しの処理なので自動的に処理がシリアライズされます(コマンドが発行順に実行されます)。QueueではなくDequeにしてあるのは、STOP要求等で他のコマンドの前に割り込んで処理出来るようにしといた方が良い場合もあるからですが、今回はDequeとしては使ってません。殆どの場合はLinkedBlockingQequeで大丈夫でしょう。
コマンド発行時にパラメータは必要ないようにしたので、LinkedBlockingDequeの型はIntegerです。Integerだと少しうれしいことがあります。C++のテンプレートがプリミティブな型でもOKなのに対しJavaのジェネリクスはクラスしか受け付けないので、こういう類の処理をする場合にはオブジェクトプールによるオブジェクトの再利用が前提になります。そうしないと大量のクラス生成と破棄が発生してさらにとろくさいJavaになってしまいます。例えばHandlerだと#obtainMessageというメソッドを使ってオブジェクトプールからMessageインスタンスを取り出して再利用できるようになっています。
しかし、Integerの場合はちょっとだけ楽ちんなのです。正確に言えばAndroidで-127〜+127の整数を使う場合にはオブジェクトプールを使わなくても良いのです。と言うのも、AndroidのIntegerクラスは-127〜+127の整数に対しInteger#valueOfまたはオートボクシングによって取得されるIntegerインスタンスを内部でキャッシュしてくれているのです\(^o^)/
なお、パラメータ付きのコマンド発行が必要な場合はコマンド用のクラスを作ることになります。例えば、次のようなコードを書けばHandlerと同じようにint✕2個とObjectをパラメータとして引き渡すことが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
// inner class private static final class Request { int cmd; int arg1; int arg2; Object obj; Request(final int _cmd, final int _arg1, final int _arg2, final Object _obj) { cmd = _cmd; arg1 = _arg1; arg2 = _arg2; obj = _obj; } }; ... // オブジェクトプールの最大容量, これを超える数のオブジェクトはプールに入らずGCで破棄されます。 private static final int MAX_POOL_NUM = 10; // 既にキューに入っているコマンドよりも前に優先コマンドを入れることができるようにDequeuを使います private final LinkedBlockingDeque<Request> mRequests = new LinkedBlockingDeque<Request>(); // オブジェクトプールは順番は気にしないのでFIFOである必要はありませんので好きなコンテナでOK。 // でも追加・取り出しが高速なコンテナが良いです。 // Queue(LinkedBlockingQeque)だと生成時に最大容量を指定できるのでプール容量の制限が楽できます。 private final LinkedBlockingQeque<Request> mRequestPool = new LinkedBlockingQeque<Request>(MAX_POOL_NUM); ... // オブジェクトプールから再利用可能なオブジェクトを取り出します。 // 再利用可能なオブジェクトない場合には新規に生成します。 public Request obtainRequest(final int cmd, final int arg1, final int arg2, final Object obj) { Request req = mRequestPool.poll(); if (req != null) { req.request = request; req.arg1 = arg1; req.arg2 = arg2; req.obj = obj; } else { req = new Request(request, arg1, arg2, obj); } return req; } // こういうヘルパーメソッドも作っておいた方が便利 protected Request obtainRequest(final int cmd) { return obtainRequest(cmd, 0, 0, null); } // コマンドの処理が終わった後にこのメソッドを呼び出してRequestインスタンスを再利用可能にします protected void recycleRequest(final Request request) { // 最大容量を指定して初期化しているので既に最大容量分のRequestが // プールに存在する場合にはプールには入りません。 // プールに入れれなかった時は#offerがfalseを返すので // 必要であればオブジェクトの後処理をします。 if (!mRequestPool.offer(request)) { // プールに入れれなかった時の処理 } } |
まぁ、高負荷高頻度とか特別の事情でもなければわざわざ自前で実装せずに素直にHandlerを使ったほうが楽ちんだと思いますが、こういうことも出来るんだというのは知ってても損ではないかな?
お疲れ様でした。