Kanade Labo

かなで研究所

Android Studio カメラアプリ作成その18(連続撮影1)

こんにちは かなで です。

久々の更新になります。

前回、 次は「起動したら、5分おきに保存を繰り返す」という仕組みを作りたい

と締めくくって、軽い感じで続きを書けると思っていたのですが、甘かったです。

いろいろ試行錯誤をしていたのですが、どうしてもうまくいかず。

しばらく放置していたのですが、その放置がよかったのか、硬くなった思考回路が少し柔らかくなったらしく

無事、「5分おきの撮影」を達成することができました。

前置きが長くなりましたが、Part17の続き、いきます!

まずは、一度Part17終了時点のソースを張り付けます。

package net.kanalabo.cam

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.common.util.concurrent.ListenableFuture
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors

//class MainActivity : AppCompatActivity() {
//    override fun onCreate(savedInstanceState: Bundle?) {
//        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
//    }
//}

class MainActivity : AppCompatActivity() {
    private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(cameraProvider)
        }, ContextCompat.getMainExecutor(this))
    }
    private fun bindPreview(cameraProvider : ProcessCameraProvider) {
        val preview : Preview = Preview.Builder()
            .build()

        val cameraSelector : CameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()
        val kanadeFinder: PreviewView = findViewById(R.id.viewFinder)
        preview.setSurfaceProvider(kanadeFinder.surfaceProvider)

        val imageCapture = ImageCapture.Builder()
            //.setTargetRotation(view.display.rotation)
            .build()

        var camera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, imageCapture, preview)
        //var camera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, preview)

        val fname = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(System.currentTimeMillis()) + ".jpg"
        val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fname)
        //Log.e(TAG, file.toString())

        val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
        val cameraExecutor = Executors.newSingleThreadExecutor()
        imageCapture.takePicture(outputFileOptions, cameraExecutor,
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(error: ImageCaptureException)
                {
                    // insert your code here.
                }
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    // insert your code here.
                }
            }
        )
    }
}

早速ですが、今回の肝はこちら

        Timer().schedule(0, 300000) {
            onLoop()
        }

onCreateの、この位置に、こちらを記載します。

onCreateっぽく、「onLoop」なんて書いてますが、もともと存在する関数ではなく、今から作るものです。

こちらの記載をすることで、「0」ミリ秒後から、「300000」ミリ秒間隔で、「onLoop()」をループさせる。

という意味になります。

言い換えると「起動直後から5分間隔でonLoop()の中身に記載されている内容を実行する」となります。

では何を実行するか。

それがこちらです。

    private fun onLoop() {
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(cameraProvider,"capture")
        }, ContextCompat.getMainExecutor(this))
    }

この位置に「onLoop()」用の記入を追加します。

ここに書いてある内容を5分おきに実行してくれるというわけです。

細かく見ていきます。

赤枠と緑枠を見比べてもらえるとわかるのですが、もともと緑枠に書いてあった内容と

【ほぼ】同じものを繰り返すようにしました。

「ほぼ」と書きましたが、何が違うのかですが

ここに「capture」というものを追加しました。

ちゃんと理解できてないので、うまく説明できないのですが、bindPreviewというのを繰り返す際に、2つ目の引数を追加して、ここの引数によって、bindPreviewの中の処理を変えようという考え方です。

こう書いてもピンとこないと思いますが、読み進めていただければわかってくると思います。たぶん。おそらく。

ここで1つ問題ビューを解決

None of the following functions can be called with the arguments supplied:
public open fun schedule(task: TimerTask!, firstTime: Date!, period: Long): Unit defined in java.util.Timer
public open fun schedule(task: TimerTask!, delay: Long, period: Long): Unit defined in java.util.Timer

これは最初に追加した「Timer().schedule」の行になります。

インポートができますので、インポートします。

これで「schedule」の部分が解決しました。

※今はやったことを最初からなぞって記事にしていますが、記事にしている今は何も出てこなかったのですが、最初にインポートをしようとしたときは、選択肢がたくさん出たと思います。
いつも通り、それっぽく適当に選んだのが「Kotlin」とつくものでした。
結果、import行に以下が追加されています。

import kotlin.concurrent.schedule

もしどれをimportすればいいかわからなければ上記1行をコピペするのもありだと思います。

も一つ出てる問題ビューですが

Too many arguments for private final fun bindPreview(cameraProvider: ProcessCameraProvider): Unit defined in net.kanalabo.cam.MainActivity

先ほど、以下を追加しましたが、

ここで、第二引数はcaptureだよと言ってるのに、bindPreviewは第二引数なんてないよ。という問題のようです。

そこで、binPreviewの関数に、第二引数を追加しました。

次に変更するのはこちら

private fun bindPreview(cameraProvider : ProcessCameraProvider,prm2:String) {

binPreviewという関数を使うときの引数に2つ目を追加しました。

この関数の中で、prm2という値を追加することができるようになりました。

これで、「 Too many arguments for private final fun bindPreview(cameraProvider: ProcessCameraProvider): Unit defined in net.kanalabo.cam.MainActivity 」はなくなりました。

これだけでは意味をなさないので続き。

上記のソースは、全部、画像を保存するときに使った部分です。

ファイル名を決めるところから、実際に保存するところまでの部分です。

これらを全部、if分で囲みます。

        if (prm2 == "capture") {
            val fname = SimpleDateFormat(
                "yyyyMMdd_HHmmss",
                Locale.US
            ).format(System.currentTimeMillis()) + ".jpg"
            val file = File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                fname
            )
            //Log.e(TAG, file.toString())

            val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
            val cameraExecutor = Executors.newSingleThreadExecutor()
            imageCapture.takePicture(outputFileOptions, cameraExecutor,
                object : ImageCapture.OnImageSavedCallback {
                    override fun onError(error: ImageCaptureException) {
                        // insert your code here.
                    }

                    override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                        // insert your code here.
                    }
                }
            )
        }

増やしたのは赤枠部分です。

【もし、prm2が"capture"ならば、ファイル名指定から画像保存までを実行】

という風に読むことができます。

ここで少し戻りますが、onLoopというのは5分ごとに実行される関数です。

そして、その実行される内容は、binPreviewの中のprm2がcaptureの部分です。(意味わからなくなる…

つまり、5分ごとに「ファイル名指定から画像保存まで」を実行してくれるという事です。

ころころと移動して申し訳ないですが、今問題ビューには

が出ています。

37行目は

これです。なんとなく予想できるかなと思いますが、第二引数のprm2というのを追加したのに、
ここの行では第二引数が指定されてないのでエラーになっているようです。

この行は最初にbinPreviewを扱うときの話なので"capture"を指定することはできません。(保存するときの処理をしたいわけではないので)

とりあえず"init"と書いておきます。

これで問題ビューの赤字が消えました。

ここまでで以下の挙動になる想定となります。

1.初回起動時はプレビューまで行う(画像保存は行わない)

2.5分おきにonLoopを実行する

3.onLoopの中で画像保存を行う=5分おきに画像保存される

まだ問題があるのはわかってるんですが(笑

一旦区切りとして、実行してみようと思います。

package net.kanalabo.cam

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.common.util.concurrent.ListenableFuture
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executors
import kotlin.concurrent.schedule

//class MainActivity : AppCompatActivity() {
//    override fun onCreate(savedInstanceState: Bundle?) {
//        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
//    }
//}

class MainActivity : AppCompatActivity() {
    private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(cameraProvider,"init")
        }, ContextCompat.getMainExecutor(this))
        Timer().schedule(0, 300000) {
            onLoop()
        }
    }
    private fun onLoop() {
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            bindPreview(cameraProvider,"capture")
        }, ContextCompat.getMainExecutor(this))
    }
    private fun bindPreview(cameraProvider : ProcessCameraProvider,prm2:String) {
        val preview : Preview = Preview.Builder()
            .build()

        val cameraSelector : CameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()
        val kanadeFinder: PreviewView = findViewById(R.id.viewFinder)
        preview.setSurfaceProvider(kanadeFinder.surfaceProvider)

        val imageCapture = ImageCapture.Builder()
            //.setTargetRotation(view.display.rotation)
            .build()

        var camera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, imageCapture, preview)
        //var camera = cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, preview)

        if (prm2 == "capture") {
            val fname = SimpleDateFormat(
                "yyyyMMdd_HHmmss",
                Locale.US
            ).format(System.currentTimeMillis()) + ".jpg"
            val file = File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                fname
            )
            //Log.e(TAG, file.toString())

            val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
            val cameraExecutor = Executors.newSingleThreadExecutor()
            imageCapture.takePicture(outputFileOptions, cameraExecutor,
                object : ImageCapture.OnImageSavedCallback {
                    override fun onError(error: ImageCaptureException) {
                        // insert your code here.
                    }

                    override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                        // insert your code here.
                    }
                }
            )
        }
    }
}

実行!

はいエラー

2022-01-16 12:38:37.474 4552-4552/net.kanalabo.cam E/AndroidRuntime: FATAL EXCEPTION: main
    Process: net.kanalabo.cam, PID: 4552
    java.lang.IllegalArgumentException: No supported surface combination is found for camera device - Id : 0.  May be attempting to bind too many use cases. Existing surfaces: [SurfaceConfig{configType=JPEG, configSize=MAXIMUM}, SurfaceConfig{configType=PRIV, configSize=ANALYSIS}] New configs: [androidx.camera.core.impl.ImageCaptureConfig@8a029d8, androidx.camera.core.impl.PreviewConfig@e01d131]
        at androidx.camera.lifecycle.LifecycleCameraRepository.bindToLifecycleCamera(LifecycleCameraRepository.java:278)
        at androidx.camera.lifecycle.ProcessCameraProvider.bindToLifecycle(ProcessCameraProvider.java:423)
        at androidx.camera.lifecycle.ProcessCameraProvider.bindToLifecycle(ProcessCameraProvider.java:274)
        at net.kanalabo.cam.MainActivity.bindPreview(MainActivity.kt:63)
        at net.kanalabo.cam.MainActivity.onLoop$lambda-2(MainActivity.kt:46)
        at net.kanalabo.cam.MainActivity.$r8$lambda$hkX6g47Ivy2LqKF2JKsxLCxQJBk(MainActivity.kt)
        at net.kanalabo.cam.MainActivity$$ExternalSyntheticLambda1.run(Unknown Source)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
2022-01-16 12:38:42.556 4552-4568/net.kanalabo.cam E/Camera2CameraImpl: Unable to configure camera 0, timeout!

Logcatを見ると、63行目、46行目にエラーのヒントがありそうです。

長くなるので、続きは別記事へ

最後までお読みいただきありがとうございました。
気になることがあったら、コメント頂けると嬉しいです。
自主学習も兼ねて記事にするかもしれません。

-AndroidStudio