Camera2サンプルでcaptureStillPicture()が何度も呼ばれる

Camera2 APIを使ってカメラアプリを作っています。いまはCameraX APIが新しく出てきてそちらが推奨されているようですが、まだまだCamera2を使っている人もいるかもしれない、そして同じ問題で悩んでいる人も一人くらいはいるかもしれない、、、そんな誰かの役に立てればと思って書き残しておきます。

ちなみにこの問題について調べているときに最新のCamera2サンプルを見てみましたが、私が入手した頃と実装が全然違っていました。なので最新サンプルで同じ問題が起きるかどうかは分かりません。

特定のデバイスでクラッシュするんですけど・・・

Googleのサンプルをもとに、Camera2 APIでカメラアプリを作っていました。順調に動作しているはずだったんですが、ある時ASUSのデバイスで写真を撮ろうとするとクラッシュするとの報告が。調査すると、一度の撮影でImageReader.OnImageAvailableListener#onImageAvailable()が何度も呼ばれ、ImageReader#acquireNextImage()の下記に引っかかっていました。

This operation will fail by throwing an IllegalStateException if maxImages have been acquired with acquireNextImage or acquireLatestImage. In particular a sequence of acquireNextImage or acquireLatestImage calls greater than maxImages without calling Image#close in-between will exhaust the underlying queue. At such a time, IllegalStateException will be thrown until more images are released with Image#close.

https://developer.android.com/reference/kotlin/android/media/ImageReader#acquirenextimage

captureStillPicture()が何度も呼ばれるんですけど・・・

とりあえずドキュメントの通り、ちゃんとclose()するようにしてみると、今度は一度シャッターを押すだけで写真が何枚も保存されてしまう状態になってしまいました。

そこでさらに原因を追っていくと、ImageReaderのコールバックを登録しているcaptureStillPicture()が何度も呼ばれていることが分かりました。だから何度もonImageAvailable()が呼ばれていたんですね。

原因は、CameraCaptureSession.CaptureCallbackに実装されている状態遷移に漏れがあることでした。

下記にこのバグについてのissueがありました。

https://github.com/googlearchive/android-Camera2Basic/issues/6

状態遷移の修正

上記のissueに、修正内容が画像で(笑)張り付けてありました。

https://github.com/googlearchive/android-Camera2Basic/issues/6

captureStillPicture()を読んだときに、状態を「撮影済み」にする必要があったということですね。私はkotlin版を使っているので、私の手元のコードの修正版も載せておきます。

private fun capturePicture(result: CaptureResult) {
    val afState = result.get(CaptureResult.CONTROL_AF_STATE)
    if (afState == null) {
        state = STATE_PICTURE_TAKEN
        captureStillPicture()
    } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
        || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
        // CONTROL_AE_STATE can be null on some devices
        val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
        if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
            state = STATE_PICTURE_TAKEN
            captureStillPicture()
        } else {
            runPrecaptureSequence()
        }
    }
}

4行目のstate = STATE_PICTURE_TAKENを追加しただけです。

これでcaptureStillPicture()が何度も呼ばれることもなくなり、onImageAvailable()の余分なコールバックもなくなり、ちゃんと写真を撮影できるようになりました。めでたしめでたし。