android カメラアプリを作ろう　その３ プレビュー方法まとめ

えー、諸々の事情であんま時間を掛けられなかった御題ですが

さすがにこの状態で終わるのも気持ち悪いのでちょっと直していきます。
前回はプレビュー画面を端末のモニタに表示するところまでを作成。

正直この記事で解説しなおすつもりなので前回の記事は見なくていいかも。

# プレビュー画面を表示するまで

## 要素の分解

ここで、再度カメラのプレビュー生成に必要な要素を大きく３つに分けておきます。

| 要素名 | 詳細 |
|:-:|:--|
| 画面描画 |  カメラから取得した画像データを描画する対象となるビューを準備する |
| カメラ制御 | カメラの動作を制御するための情報を取得・整備する  |
| セッション  | カメラからの画像情報を画面に結び付ける  |

この３要素を更に細かい要素に分解して並べたのがこちら

![プレビュー_全体.jpg](https://qiita-image-store.s3.amazonaws.com/0/259679/0f80102b-e646-4271-0c64-f38d0572e8b4.jpeg)

これをそれぞれ細かく見ていきましょ。

### 画面描画

![プレビュー_画面制御.jpg](https://qiita-image-store.s3.amazonaws.com/0/259679/826fb04c-8b5f-d027-35e6-97ce52f25d82.jpeg)

ここではビューの設定に焦点を当てていきましょう。

1. 描画先のビューを取得あるいは生成する。
2. イベントリスナを設定する。
3. ビューからSurfaceTextureのインスタンスを取得する。


#### 描画先のビューを取得あるいは生成する。

まずはアクティビティに表示するビューの設定。

<details><summary>アクティビティとは？</summary>androidの画面を構成するフレームに当たる要素。iOSのビューと同義。</details>
<details><summary>ビューとは？</summary>androidの画面を構成するコントロールに当たる要素。</details>

コードとして生成するもよし、Layout Editorで作成したビューをFindViewById()メソッドなどを利用してもよし。
ここで注意するべきなのは、使いたいのはあくまで"Surface"を保持できるビューである必要がある点。

[android カメラアプリを作ろう　その２ androidのカメラから映像を取得する](https://qiita.com/Shiratori/items/4070f4f663a1325b66a5)でリンクを張ったandroid.hardware.camera2クラスの説明によれば、
こんなビューが利用できる様子。

* [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView) : [SurfaceHolder](https://developer.android.com/reference/android/view/SurfaceHolder)を利用する
* [TextureView](https://qiita.com/Shiratori/items/4070f4f663a1325b66a5) : [SurfaceTexture](https://developer.android.com/reference/android/graphics/SurfaceTexture)を利用する

**今回はTetureViewを利用して実装していきます。**

#### イベントリスナを設定する.

TextureViewを利用する場合、setTextureListenerメソッドを利用することでイベントリスナを設定できます。
設定できるイベントは以下のとおり

| イベント名 |呼ばれるタイミング |
|:-:|:--|
| onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) | TextureViewのSurfaceTextureが利用可能になったとき  |
| onSurfaceTextureDestroyed(SurfaceTexture surface)  |  SurfaceTextureが破棄されたとき |
| onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)  |  SurfaceTextureのサイズが変更されたとき |
| onSurfaceTextureUpdated(SurfaceTexture surface)  | SurfaceTexture.updateTexImage()メソッドを利用してSurfaceTextureが更新されたとき  |

詳細は[こちら](https://developer.android.com/reference/android/view/TextureView.SurfaceTextureListener)を参照(英語)

TextureViewからSurfaceTextureを取得する方法は

* 上記のイベントリスナを用いる
* TextureView.getSurfaceTexture()メソッドを用いる

の2通りがあります。
今回はonSurfaceTextureAvailable()イベントからSurfaceTextureをいただくことにしましょう。

~~ところでandroid Developers - Documantation TextureViewにCameraクラス使ったプレビューの出し方がコードごと乗ってるんですがそれは･･･~~

### カメラ制御

ここではカメラを制御するための設定を行います。

![プレビュー_カメラ制御.jpg](https://qiita-image-store.s3.amazonaws.com/0/259679/7dd5705c-11f2-979b-db90-5b2f6319ab90.jpeg)

1. CameraManagerインスタンス取得
2. カメラIDリストの取得
3. (a)カメラからのコールバックインスタンスを生成<br>(b) CameraCharacteristicsインスタンスの取得
4. カメラデバイスのOpen

#### CameraManagerインスタンス取得

カメラの制御は当然ながらandroid OSによるハードウェア制御です。
なので、まずはandroid OSからgetSystemService()を利用して融通してもらいましょう。

ちなみに上記メソッドを利用することでほかにもアラームだのウィジェットだのといったいろいろなサービスにアクセスすることが出来ます。

参考：[android Developers - Documantation Context](https://developer.android.com/reference/android/content/Context)


#### カメラIDリストの取得

androidでは1台の端末に複数のカメラが乗っていることがままあります。
外側カメラと内側カメラが主ですかね。ほかのカメラはどんなのがあるんでしょう？

で、このカメラはandroid内ではID管理されているので、<CameraManager>.getCameraIDList()を利用してカメラIDをごそっといただきましょ。
型はString[]です。

#### (a)カメラからのコールバックインスタンスを生成

カメラデバイスインスタンスに登録するコールバックを用意しましょ。
[CameraDevice.StateCallback](https://developer.android.com/reference/android/hardware/camera2/CameraDevice.StateCallback)型のインスタンスを作ります。

これも特定タイミングで呼ばれるので内容をざっくり。

| イベント名 |呼ばれるタイミング |
|:-:|:--|
| onClosed (CameraDevice camera) | カメラデバイスがCameraDevice.close()メソッドを利用して閉じられたとき  |
| onDisconnected (CameraDevice camera)  |  カメラデバイスが利用できなくなったとき。<br>カメラデバイスをopenできなかったときも |
| onError (CameraDevice camera, int error)  |  カメラデバイスがガチめなエラーを吐いたとき。<br>エラーの内容は第二引数のコード参照のこと |
| onOpened (CameraDevice camera)  | カメラデバイスのopenが正常に終了したとき  |

軽く調べてみたところ、onOpend()メソッドの引数以外にカメラデバイスを取得する方法はなさそう。

#### (b) CameraCharacteristicsインスタンスの取得

この項目。省略できるっちゃできます。
なぜならCameraCharacteristicsインスタンスはカメラの諸々の規定値を取得するためのものなので
今回みたいにとりあえずプレビューするだけならとる必要はないです。

取得できる項目は[こちら：android Developers - Documentation ChameraCharacteristics](https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics)を参照のこと。

正直な話、カメラにはほとほと疎いので、倍率くらいしかわかりません。
get()メソッドとCameraCharacteristicsに定義されてるKeyの組み合わせでいろいろわかるらしいっすよ。
例えば、内側カメラか外側カメラかはLENS_FACING Keyを利用することでFRONT/BACK/EXTERNALの3通りのデータが帰ってくるとかなんとか。

インスタンス自体はCameraManager.getCameraCharacteristics(カメラID）で取得できます。簡単。

#### カメラデバイスのOpen

ここまできたらカメラデバイスをopenしましょ。
CameraManager.ioenCamera(CameraID, CameraDevice.StateCallback , handler)

当然ながらこいつは非同期。

### セッション

ここまで来たら後ちょっと！
と言うわけで用意したSurfaceにopenしたカメラからの映像を紐付けてやりましょ。

![プレビュー_セッション.jpg](https://qiita-image-store.s3.amazonaws.com/0/259679/d316d030-47ab-32e9-083b-75dd7a9df477.jpeg)

1. CaptureRequestの取得
   1. CaptureRequestBuilderインスタンスの生成
   2. 描画先Surfaceの指定
   3. CaptureRequestインスタンスの生成

2. CameraCaptureSessionインスタンスの生成
3. CaptureRequestをCameraCaptureSessionに繰り返し投げるよう設定

今日はなんか記事と図描いただけで終わりそうなんですが…

#### CameraCaptureRequestの取得

これね。
いきなりポッと出てきたコイツはなんぞや？というお話。

参考：[android Developers - Documentation CaptureRequest](https://developer.android.com/reference/android/hardware/camera2/CaptureRequest)

上のページの一番最初の文が全てな気がしないでもない。
カメラデバイスからの**要求された単一イメージキャプチャ**出力と設定のためのパッケージ。

セッションの項目3で「繰り返し投げるように」とわざわざ書いたのはこれが原因で
１リクエストにつき１イメージしかキャプチャしてないわけです。
だから、CaptureRequestを繰り返し投げる必要があったんですね。

で、作り方は以下のとおり。

<details><summary>CaptureReauest.Builderのインスタンスを生成する</summary>
<div>

```
    CaptureRequest.Builder var = <CameraDevice>.createCaptureRequest( <CameraDevice>.(設定値) ) 
```

この設定値なんですが、マニュアルだったりプレビューだったりシャッターのラグをなくしたりと
色々テンプレートが用意されてます。
詳細は[android Developers - Documentation CameraDevice](https://developer.android.com/reference/android/hardware/camera2/CameraDevice)のConstantsの項目を御覧あれ。

とりあえず今回はプレビューなのでTEMPLATE_PREVIEWを設定値として進めます。
</div></details>
<details><summary>描画先Surfaceの指定</summary>
<div>

CaptureRequest.Builder.addTarget()メソッドを利用します。
用意したSurfaceインスタンスを引数として渡してやりましょう。

```
    <CaptureRequest.Builder>.addTarget( <Surface> );

```

</div></details>

<details><summary>CaptureRequestインスタンスの生成</summary>
<div>

後はCaptureRequest.Builder.build()メソッドを呼んでやればOK

```
CaptureRequest captureRequest = <CaptureRequest.Builder>.build();
```

</div></details>

#### CameraCaptureSessionインスタンスの生成

お次はセッションです。
コイツがないと要求した画像がSurfaceに届きません。

ので、CameraDevice.createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)メソッドを利用してセッションを作りましょう。
ちなみに出力先のSurfacer型リストはArrays.asList( Surface )でイケるみたいです。

コールバックの呼び出しタイミングは以下の通り。

| イベント名 |呼ばれるタイミング |
|:-:|:--|
| onActive(CameraCaptureSession session) | キャプチャリクエストの処理を開始したとき |
| onCaptureQueueEmpty(CameraCaptureSession session)  |  キャプチャリクエストキューがなくなったとき |
| onClosed(CameraCaptureSession session) |  セッションがクローズしたとき |
| onConfigureFailed(CameraCaptureSession session)  | セッションに対して要求された設定が出来なかったとき |
| onConfigured(CameraCaptureSession session) | セッションに対して要求された設定が完了したとき |
| onReady(CameraCaptureSession session)　| キャプチャリクエストがないときに常時呼ばれ続ける |
| onSurfacePrepared(CameraCaptureSession session, Surface surface)  | 出力先のSurface領域の事前確保が出来たとき？ |

これnew CameraCaptureSession()じゃ駄目なんかな？ちょっと試せてないですが。
とりあえず引数として渡されたCameraCaptureSessionインスタンスを利用することにします。

#### CaptureRequestをCameraCaptureSessionに繰り返し投げるよう設定

最後に、常にカメラからの映像を取得するように設定を行います。
繰り返しリクエストを投げるためのメソッドが既に用意されているわけですね。

それがこちら。
CameraCaptureSession.setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)メソッド。

第一引数には作りたてのCaptureRequestを指定。
これもコールバックが居てですね･･･（疲弊）

それがこんな感じ。

| イベント名 |呼ばれるタイミング |
|:-:|:--|
| onCaptureBufferLost(CameraCaptureSession session, CaptureRequest request, Surface target, long frameNumber)| キャプチャした画像が出力先のSurfaceに送れなかったとき |
| onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result)  |  キャプチャが出来、且つ、全てのメタデータが利用可能なとき |
| onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) |  キャプチャに失敗したとき↑の代わりに呼ばれる |
| onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) | キャプチャの一部が完了したとき |
| onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId) | 他のコールバックとは独立して呼ばれる。キャプチャ処理の結果が(成功/失敗にかかわらず）出る前にこのリスナを通して中断されたとき |
| onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, long frameNumber)　| 他のコールバックとは独立して呼ばれる。キャプチャ処理が（成功/失敗にかかわらず）このリスナを通して終了したとき |
| onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) | キャプチャした画像の出力が始まったとき |

ちなみにnull指定でも大丈夫ッス。

また、このsetRepeatingRequestは設定後特に何もしなくても動作し続けるので
止めたいときはstpoRepeat()メソッドを使えばOK、と。

### 画像を保存するには？

基本的にファイルを開く方法そのまんまなので簡単な記述のみで。

1. Fileインスタンスを作って
2. FileOutputStreamを開いて
3. TexttureView.getBitmap()メソッドつかって画像取得して
4. bitmap.compress()使ってFileOutputStreamに流す

これでOK

ってことで今回の御題は完了。

---

本日の稼動は06:36:33。
今回の御題全体だと13:00:35と。

ちょっと急がしかったっすねぇ･･･

今週の御題はどうなるかな