Camera2 APIを使ってカメラアプリを作っています。いまはCameraX APIが新しく出てきてそちらが推奨されているようですが、まだまだCamera2を使っている人もいるかもしれない、そして同じ問題で悩んでいる人も一人くらいはいるかもしれない、、、そんな誰かの役に立てればと思って書き残しておきます。
ちなみにこの問題について調べているときに最新のCamera2サンプルを見てみましたが、私が入手した頃と実装が全然違っていました。なので最新サンプルで同じ問題が起きるかどうかは分かりません。
特定のデバイスでクラッシュするんですけど・・・
Googleのサンプルをもとに、Camera2 APIでカメラアプリを作っていました。順調に動作しているはずだったんですが、ある時ASUSのデバイスで写真を撮ろうとするとクラッシュするとの報告が。調査すると、一度の撮影でImageReader.OnImageAvailableListener#onImageAvailable()
が何度も呼ばれ、ImageReader#acquireNextImage()
の下記に引っかかっていました。
This operation will fail by throwing an
https://developer.android.com/reference/kotlin/android/media/ImageReader#acquirenextimageIllegalStateException
ifmaxImages
have been acquired withacquireNextImage
oracquireLatestImage
. In particular a sequence ofacquireNextImage
oracquireLatestImage
calls greater thanmaxImages
without callingImage#close
in-between will exhaust the underlying queue. At such a time,IllegalStateException
will be thrown until more images are released withImage#close
.
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()
の余分なコールバックもなくなり、ちゃんと写真を撮影できるようになりました。めでたしめでたし。