Compose でカメラ映像を表示する - CameraXViewfinder

### はじめに

[CameraXViewfinder](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:camera/camera-compose/src/main/java/androidx/camera/compose/CameraXViewfinder.kt;drc=4b83f9cd9587f3e590afdfa82243dfcffd9b7750;l=67)

CameraX を Compose で使う場合、従来は PreviewView を AndroidView でラップする必要がありました。
が、[CameraX 1.5.0-alpha](https://developer.android.com/jetpack/androidx/releases/camera?hl=ja#1.5.0-alpha01) から CameraXViewfinder という専用の Composable が生えていて
 カメラプレビューを表示できるようになった様子。


### CameraXViewfinder


```kotlin
@Composable
fun CameraXViewfinder(
    surfaceRequest: SurfaceRequest,
    modifier: Modifier = Modifier,
    implementationMode: ImplementationMode = ImplementationMode.EXTERNAL,
    coordinateTransformer: MutableCoordinateTransformer? = null
)
```

| 引数 | 説明 |
| :- | - |
| surfaceRequest | Preview が発行する SurfaceRequest（カメラ映像の描画リクエスト） |
| implementationMode | EXTERNAL（低遅延） or EMBEDDED（互換性重視） |
| coordinateTransformer | タップ座標をカメラの座標系に変換するオブジェクト |

### コード

```kotlin
class CameraViewModel(context: Context) : ViewModel() {
    private val _surfaceRequests = MutableStateFlow<SurfaceRequest?>(null)
    val surfaceRequests: StateFlow<SurfaceRequest?> get() = _surfaceRequests.asStateFlow()

    val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

    init {
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
                it.setSurfaceProvider { newSurfaceRequest ->
                    _surfaceRequests.value = newSurfaceRequest
                }
            }

            val imageCapture = ImageCapture.Builder().build()
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    ProcessLifecycleOwner.get(),
                    cameraSelector,
                    preview,
                    imageCapture
                )
            } catch (exc: Exception) {
                exc.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(context))
    }
}
```

```kotlin
@Composable
fun MyCameraScreen(viewModel: CameraViewModel) {
    val currentSurfaceRequest: SurfaceRequest? by viewModel.surfaceRequests.collectAsState()
    val coordinateTransformer = remember { MutableCoordinateTransformer() }

    currentSurfaceRequest?.let { surfaceRequest ->
        CameraXViewfinder(
            surfaceRequest = surfaceRequest,
            implementationMode = ImplementationMode.EXTERNAL,
            modifier = Modifier.pointerInput(Unit) {
                detectTapGestures {
                    with(coordinateTransformer) {
                        val surfaceCoords = it.transform()
                        println("Tap at (${surfaceCoords.x}, ${surfaceCoords.y})")
                    }
                }
            },
            coordinateTransformer = coordinateTransformer
        )
    }
}
```

カメラ画像を画面に表示する　ってことをやっていて
- Preview を用意
- CameraProvider　にバインド
- Previewから発行されるSurfaceRequest を Composableに食わせる

という手順で描画が可能。

libs は こちら
```toml
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" }
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
androidx-camera-compose = { group = "androidx.camera", name = "camera-compose", version.ref = "camerax" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
```

今回書いたコードだと、再コンポーズ で CameraXViewfinder 自体の 表示 非表示切り替えたりすると描画がうまくいかないことがある。
適切なlifeCycle の管理が必要そう。

### SurfaceRequestとは

やや独自解釈が入ってるかもですが、Previewの仕組みとして

- 描画側 : CameraDevice（Camera2 API）
- 映像の受け皿 : Surface（SurfaceTexture, SurfaceView など）
- 表示処理： PreviewView / CameraXViewfinder 

と役割が分かれていて、以下流れがある。
- CameraDevice がSurface にカメラ映像を書き込む
- Surface は描画された映像を保持し、映像をPreviewViewやCameraXViewfinder に受け渡す
- CameraXViewfinder や PreviewView が Surface を受け取り、画面に表示する

Previewユースケースは解像度などの表示要素の設定値を持っていて、自身の設定値からSurfaceを描画する情報をまとめて 描画のRequestを発行する。 これがSurfaceRequest。
Preview と CameraXViewfinderの橋渡し的オブジェクト。


https://developer.android.com/reference/androidx/camera/core/SurfaceRequest


### implementationModeの違い

| **Mode**       | **内部の仕組み**          | **メリット**                            | **デメリット**                        |
|---------------|--------------------------|--------------------------------|--------------------------------|
| `EXTERNAL`    | `SurfaceView` を使用       | 低遅延 & 省電力                  | 一部の端末で動作が不安定になる可能性 |
| `EMBEDDED`    | `TextureView` を使用      | 端末互換性が高い                 | 若干の遅延が発生することがある |

特に理由がなければ EXTERNAL でよいはず。

内部実装的には 以下となっていて

```kotlin
when (implementationMode) {
    ImplementationMode.EXTERNAL -> {
        AndroidExternalSurface(modifier = surfaceModifier, onInit = onInit)
    }
    ImplementationMode.EMBEDDED -> {
        val displayRotationDegrees =
            key(LocalConfiguration.current) {
                surfaceRotationToRotationDegrees(LocalView.current.display.rotation)
            }

        // For TextureView, correct the orientation to match the display rotation.
        val correctionMatrix = remember { Matrix() }

        transformationInfo.let {
            correctionMatrix.setFrom(
                SurfaceTransformationUtil.getTextureViewCorrectionMatrix(
                    displayRotationDegrees,
                    resolution
                )
            )
        }

        AndroidEmbeddedExternalSurface(
            modifier = surfaceModifier,
            transform = correctionMatrix,
            onInit = onInit
        )
    }
}
```

それぞれ SurfaceView と TextureView を呼び出している。
`EXTERNAL` と `EMBEDDED` の違いはほぼ このViewの違いと言って良さそう。
蛇足で、たしか[CameraController](https://developer.android.com/reference/androidx/camera/view/CameraController) （CameraProvider の簡易版のようなもの）は TextureView だったはず。

https://developer.android.com/reference/android/view/SurfaceView.html

https://developer.android.com/reference/android/view/TextureView


### 終わり

アーティファクト構成が少しややこしいのですが
この記事では `androidx.camera:camera-*:1.5.0-alpha*` のアーティファクトに入っているトップレベルの viewfinder の Composable を使ってます。 
コレとは別に`androidx.camera:camera-viewfinder-compose:1.0.0-alpha*`があるのですがこれは CameraXViewfinderが参照している [ViewFinder](https://android.googlesource.com/platform/frameworks/support/+/6841e51d55fb6f14ec10245140146bcff6a391d8/camera/camera-viewfinder-compose/src/main/java/androidx/camera/viewfinder/compose/Viewfinder.kt) などが入ったComponentを空間になります。
一応最新のメンテ状況は androidx.camera:camera-*:1.5.0-alpha* の方が更新がされている様子です。