CameraX 시작하기

1. 시작하기 전에

이 Codelab에서는 CameraX를 사용하여 뷰파인더를 표시하고, 사진을 찍고, 동영상을 캡처하고, 카메라에서 이미지 스트림을 분석하는 카메라 앱을 만드는 방법을 알아봅니다.

이를 위해 뷰파인더 표시부터 동영상 캡처에 이르기까지 다양한 카메라 작업에 사용할 수 있는 사용 사례 개념을 CameraX에 도입합니다.

기본 요건

  • 기본 Android 개발 환경
  • MediaStore 지식(필수는 아니지만 도움이 됨)

실행할 작업

  • CameraX 종속 항목을 추가하는 방법을 알아봅니다.
  • 활동에서 카메라 미리보기를 표시하는 방법을 알아봅니다. Preview 사용 사례
  • 사진을 촬영하여 저장소에 저장할 수 있는 앱을 빌드합니다. ImageCapture 사용 사례
  • 실시간으로 카메라의 프레임을 분석하는 방법을 알아봅니다. ImageAnalysis 사용 사례
  • MediaStore에 동영상을 캡처하는 방법을 알아봅니다. VideoCapture 사용 사례

필요한 항목

  • Android 기기 또는 Android 스튜디오의 에뮬레이터
  • Android 10 이상 권장: MediaStore 동작이 범위 지정 저장소 가용성에 따라 다릅니다.
  • Android Emulator**를 사용하면 Android 11 이상에 기반하는 Android Virtual Device(AVD)를 사용하는 것이 좋습니다.**
  • CameraX에 지원되는 최소 API 수준은 21이어야 합니다.
  • Android 스튜디오 Arctic Fox 2020.3.1 이상
  • Kotlin 및 Android ViewBinding에 관한 이해

2. 프로젝트 만들기

  1. Android 스튜디오에서 새 프로젝트를 만들고 메시지가 표시되면 Empty Activity를 선택합니다.

ed0f21e863f9e38f.png

  1. 그런 다음 앱 이름을 'CameraXApp'으로 지정하고 패키지 이름을 'com.android.example.cameraxapp'으로 확인하거나 변경합니다. 언어는 Kotlin을 선택하고 최소 API 수준은 21(CameraX에 필요한 최소 수준)로 설정합니다. 이전 버전의 Android 스튜디오에는 AndroidX 아티팩트 지원이 포함되어야 합니다.

10f0a12f6c8b997c.png

Gradle 종속 항목 추가

  1. CameraXApp.app 모듈의 build.gradle 파일을 열고 CameraX 종속 항목을 추가합니다.
dependencies {
  def camerax_version = "1.1.0-beta01"
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  implementation "androidx.camera:camera-video:${camerax_version}"

  implementation "androidx.camera:camera-view:${camerax_version}"
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}
  1. CameraX에는 Java 8에 포함된 메서드가 필요하므로 이에 따라 컴파일 옵션을 설정해야 합니다. android 블록 끝에서 buildTypes 바로 뒤에 다음을 추가합니다.
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. 이 Codelab에서는 ViewBinding을 사용하므로 android{} 블록 끝에 다음을 사용하여 사용 설정하세요.
buildFeatures {
   viewBinding true
}

메시지가 표시될 때 Sync Now를 클릭하면 앱에서 CameraX를 사용할 수 있습니다.

Codelab 레이아웃 만들기

이 Codelab의 UI에는 다음을 사용합니다.

  • CameraX PreviewView(카메라 이미지/동영상 미리보기용)
  • 이미지 캡처를 제어하는 표준 버튼
  • 동영상 캡처를 시작/중지하는 표준 버튼
  • 버튼 2개를 배치하기 위한 세로 가이드라인

기본 레이아웃을 다음 코드로 바꿔 보겠습니다.

  1. res/layout/activity_main.xml에서 activity_main 레이아웃 파일을 열고 다음 코드로 바꿉니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <androidx.camera.view.PreviewView
       android:id="@+id/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

   <Button
       android:id="@+id/image_capture_button"
       android:layout_width="110dp"
       android:layout_height="110dp"
       android:layout_marginBottom="50dp"
       android:layout_marginEnd="50dp"
       android:elevation="2dp"
       android:text="@string/take_photo"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />

   <Button
       android:id="@+id/video_capture_button"
       android:layout_width="110dp"
       android:layout_height="110dp"
       android:layout_marginBottom="50dp"
       android:layout_marginStart="50dp"
       android:elevation="2dp"
       android:text="@string/start_capture"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toEndOf="@id/vertical_centerline" />

   <androidx.constraintlayout.widget.Guideline
       android:id="@+id/vertical_centerline"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:orientation="vertical"
       app:layout_constraintGuide_percent=".50" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 다음을 사용하여 res/values/strings.xml 파일을 업데이트합니다.
<resources>
   <string name="app_name">CameraXApp</string>
   <string name="take_photo">Take Photo</string>
   <string name="start_capture">Start Capture</string>
   <string name="stop_capture">Stop Capture</string>
</resources>

MainActivity.kt 설정

  1. MainActivity.kt의 코드를 다음 코드로 바꿉니다. 단, 패키지 이름은 그대로 둡니다. 여기에는 import 문, 인스턴스화할 변수, 구현할 함수, 상수가 포함됩니다.

카메라 권한을 확인하고, 카메라를 시작하고, 사진 및 캡처 버튼의 onClickListener()를 설정하고, cameraExecutor를 구현할 수 있도록 onCreate()가 이미 구현되어 있습니다. onCreate()가 구현되어 있어도 파일에서 메서드를 구현할 때까지는 카메라가 작동하지 않습니다.

package com.android.example.cameraxapp

import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.VideoRecordEvent
import androidx.core.content.PermissionChecker
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.Locale

typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
   private lateinit var viewBinding: ActivityMainBinding

   private var imageCapture: ImageCapture? = null

   private var videoCapture: VideoCapture<Recorder>? = null
   private var recording: Recording? = null

   private lateinit var cameraExecutor: ExecutorService

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       viewBinding = ActivityMainBinding.inflate(layoutInflater)
       setContentView(viewBinding.root)

       // Request camera permissions
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           ActivityCompat.requestPermissions(
               this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
       }

       // Set up the listeners for take photo and video capture buttons
       viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
       viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }

       cameraExecutor = Executors.newSingleThreadExecutor()
   }

   private fun takePhoto() {}

   private fun captureVideo() {}

   private fun startCamera() {}

   private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
       ContextCompat.checkSelfPermission(
           baseContext, it) == PackageManager.PERMISSION_GRANTED
   }

   override fun onDestroy() {
       super.onDestroy()
       cameraExecutor.shutdown()
   }

   companion object {
       private const val TAG = "CameraXApp"
       private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
       private const val REQUEST_CODE_PERMISSIONS = 10
       private val REQUIRED_PERMISSIONS =
           mutableListOf (
               Manifest.permission.CAMERA,
               Manifest.permission.RECORD_AUDIO
           ).apply {
               if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                   add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
               }
           }.toTypedArray()
   }
}

3. 필요한 권한 요청

앱이 카메라를 열려면 사용자의 권한이 필요합니다. 오디오를 녹음하려면 마이크 권한도 필요합니다. Android 9(P) 이하에서는 MediaStore에 외부 저장소 쓰기 권한이 필요합니다. 이 단계에서는 이러한 필요한 권한을 구현합니다.

  1. AndroidManifest.xml을 열고 application 태그 앞에 다음 줄을 추가합니다.
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

android.hardware.camera.any를 추가하면 기기에 카메라가 있는지 확인할 수 있습니다. .any를 지정하면 전면 카메라 또는 후면 카메라가 될 수 있음을 나타냅니다.

  1. 이 코드를 MainActivity.kt.에 복사합니다. 아래 글머리기호에서 방금 복사한 코드를 분석합니다.
override fun onRequestPermissionsResult(
   requestCode: Int, permissions: Array<String>, grantResults:
   IntArray) {
   if (requestCode == REQUEST_CODE_PERMISSIONS) {
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           Toast.makeText(this,
               "Permissions not granted by the user.",
               Toast.LENGTH_SHORT).show()
           finish()
       }
   }
}
  • 요청 코드가 올바른지 확인합니다. 올바르지 않으면 무시하세요.
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
  • 권한이 부여되면 startCamera()를 호출합니다.
if (allPermissionsGranted()) {
   startCamera()
}
  • 권한이 부여되지 않으면 사용자에게 권한이 부여되지 않았다고 알리는 토스트 메시지를 표시합니다.
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. 앱을 실행합니다.

이제 카메라와 마이크 사용 권한을 요청해야 합니다.

dcdf8aa3d87e74be.png

4. Preview 사용 사례 구현

카메라 애플리케이션에서 뷰파인더를 사용하면 사용자가 촬영할 사진을 미리 볼 수 있습니다. CameraX Preview 클래스를 사용하여 뷰파인더를 구현합니다.

Preview를 사용하려면 먼저 구성을 정의해야 이를 사용하여 사용 사례 인스턴스를 만들 수 있습니다. 결과 인스턴스는 CameraX 수명 주기에 바인딩하는 대상입니다.

  1. 이 코드를 startCamera() 함수에 복사합니다.

아래 글머리기호는 방금 복사한 코드를 분석합니다.

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
          .build()
          .also {
              it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
          }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  • ProcessCameraProvider 인스턴스를 만듭니다. 이는 카메라의 수명 주기를 수명 주기 소유자와 바인딩하는 데 사용합니다. CameraX가 수명 주기를 인식하므로 카메라를 열고 닫는 작업이 필요하지 않게 됩니다.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • cameraProviderFuture에 리스너를 추가합니다. Runnable을 하나의 인수로 추가합니다. 나중에 채워넣습니다. ContextCompat.getMainExecutor()를 두 번째 인수로 추가합니다. 그러면 기본 스레드에서 실행되는 Executor가 반환됩니다.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • Runnable에서 ProcessCameraProvider를 추가합니다. 이는 카메라의 수명 주기를 애플리케이션 프로세스 내의 LifecycleOwner에 바인딩하는 데 사용합니다.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • Preview 객체를 초기화하고 이 객체에서 build를 호출하고 뷰파인더에서 노출 영역 제공자를 가져온 다음 미리보기에서 설정합니다.
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
   }
  • CameraSelector 객체를 만들고 DEFAULT_BACK_CAMERA를 선택합니다.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • try 블록을 만듭니다. 이 블록 내에서 cameraProvider에 바인딩된 항목이 없도록 한 다음 cameraSelector 및 미리보기 객체를 cameraProvider에 바인딩합니다.
try {
   cameraProvider.unbindAll()
   cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
}
  • 앱에 더 이상 포커스가 없는 경우와 같이 이 코드는 몇 가지 방법으로 실패할 수 있습니다. 이 코드를 catch 블록에 래핑하여 실패가 발생한 경우 기록합니다.
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. 앱을 실행합니다. 이제 카메라 미리보기가 표시됩니다.

d61a4250f6a3ed35.png

5. ImageCapture 사용 사례 구현

다른 사용 사례도 Preview와 매우 유사한 방식으로 작동합니다. 먼저 실제 사용 사례 객체를 인스턴스화하는 데 사용하는 구성 객체를 정의합니다. 사진을 캡처하려면 Take Photo 버튼을 누르면 호출되는 takePhoto() 메서드를 구현합니다.

  1. 이 코드를 takePhoto() 메서드에 복사합니다.

아래 글머리기호는 방금 복사한 코드를 분석합니다.

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions
           .Builder(contentResolver,
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    contentValues)
           .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken
   imageCapture.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun
               onImageSaved(output: ImageCapture.OutputFileResults){
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}
  • 먼저 ImageCapture 사용 사례에 대한 참조를 가져옵니다. 사용 사례가 null이면 함수를 종료합니다. 이미지 캡처가 설정되기 전에 사진 버튼을 탭하면 null이 됩니다. return 문이 없으면 null인 경우 앱이 비정상 종료됩니다.
val imageCapture = imageCapture ?: return
  • 다음으로 이미지를 보관할 MediaStore 콘텐츠 값을 만듭니다. MediaStore의 표시 이름이 고유하도록 타임스탬프를 사용합니다.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }
  • OutputFileOptions 객체를 만듭니다. 이 객체에서 원하는 출력 방법에 관한 사항을 지정할 수 있습니다. 출력을 MediaStore에 저장하여 다른 앱에서 표시할 수 있도록 MediaStore 항목을 추가합니다.
val outputOptions = ImageCapture.OutputFileOptions
       .Builder(contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues)
       .build()
  • imageCapture 객체에서 takePicture()를 호출합니다. outputOptions, 실행자, 이미지가 저장될 때의 콜백을 전달합니다. 그다음에 콜백을 작성합니다.
imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this),
   object : ImageCapture.OnImageSavedCallback {}
)
  • 이미지 캡처에 실패하거나 이미지 캡처 저장에 실패하는 경우 오류 사례를 추가하여 실패했음을 기록합니다.
override fun onError(exc: ImageCaptureException) {
   Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
  • 캡처에 실패하지 않으면 사진이 성공적으로 촬영된 것입니다. 앞서 만든 파일에 사진을 저장하고, 사용자에게 사진 촬영이 완료되었음을 알리는 토스트 메시지를 표시하고, 로그 구문을 출력합니다.
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
   val savedUri = Uri.fromFile(photoFile)
   val msg = "Photo capture succeeded: $savedUri"
   Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
   Log.d(TAG, msg)
}
  1. startCamera() 메서드로 이동하여 이 코드를 미리보기용 코드 아래에 복사합니다.
imageCapture = ImageCapture.Builder().build()
  1. 그런 다음 새 사용 사례를 포함하도록 try 블록에서 bindToLifecycle() 호출을 업데이트합니다.
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)

이 시점에서 메서드는 다음과 같습니다.

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
                 it.setSurfaceProvider(viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 앱을 다시 실행하고 Take Photo를 누릅니다. 화면에 토스트 메시지가 표시되고 로그에 메시지가 표시됩니다.

54292eaa4ce3be0a.png

사진 보기

이제 새로 캡처한 사진이 MediaStore에 저장되므로 MediaStore 애플리케이션을 사용하여 사진을 볼 수 있습니다. 예를 들어 Google 포토 앱에서 다음을 실행합니다.

  1. Google 포토 사진를 시작합니다.
  2. '라이브러리'를 탭하여(계정으로 포토 앱에 로그인하지 않은 경우 불필요) 정렬된 미디어 파일을 보려면 "CameraX-Image" 폴더를 사용합니다.

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. 이미지 아이콘을 탭하여 전체 사진을 검토합니다. 오른쪽 상단의 더보기 버튼 더보기을 탭하여 캡처된 사진의 세부정보를 확인합니다.

55e1a442ab5f25e7.png 70a8b27a76523f56.png

사진을 찍을 간단한 카메라 앱만 찾는다면 다 완료되었습니다. 정말 간단합니다. 이미지 분석 도구를 구현하려는 경우에는 계속 읽어보세요.

6. ImageAnalysis 사용 사례 구현

ImageAnalysis 기능을 사용하면 카메라 앱을 더 흥미롭게 만들 수 있습니다. 이를 통해 ImageAnalysis.Analyzer 인터페이스를 구현하고, 수신되는 카메라 프레임으로 호출되는 맞춤 클래스를 정의할 수 있습니다. 카메라 세션 상태를 관리하거나 이미지를 삭제할 필요도 없습니다. 다른 수명 주기 인식 구성요소와 마찬가지로 앱에서 원하는 수명 주기에 바인딩하는 것으로 충분합니다.

  1. 이 분석 도구를 MainActivity.kt의 내부 클래스로 추가합니다. 분석 도구는 이미지의 평균 광도를 기록합니다. 분석 도구를 만들려면 ImageAnalysis.Analyzer 인터페이스를 구현하는 클래스에서 analyze 함수를 재정의합니다.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

   private fun ByteBuffer.toByteArray(): ByteArray {
       rewind()    // Rewind the buffer to zero
       val data = ByteArray(remaining())
       get(data)   // Copy the buffer into a byte array
       return data // Return the byte array
   }

   override fun analyze(image: ImageProxy) {

       val buffer = image.planes[0].buffer
       val data = buffer.toByteArray()
       val pixels = data.map { it.toInt() and 0xFF }
       val luma = pixels.average()

       listener(luma)

       image.close()
   }
}

ImageAnalysis.Analyzer 인터페이스를 구현하는 클래스를 사용하면 다른 사용 사례와 마찬가지로 ImageAnalysis,에서 LuminosityAnalyzer 인스턴스를 인스턴스화하고 CameraX.bindToLifecycle() 호출 전에 startCamera() 함수를 다시 한번 업데이트하기만 하면 됩니다.

  1. startCamera() 메서드에서 imageCapture 코드 아래에 다음 코드를 추가합니다.
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. imageAnalyzer를 포함하도록 cameraProvider에서 bindToLifecycle() 호출을 업데이트합니다.
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)

전체 메서드는 다음과 같습니다.

private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
           }

       imageCapture = ImageCapture.Builder()
           .build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture, imageAnalyzer)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 이제 앱을 실행합니다. 약 1초마다 logcat에 다음과 유사한 메시지가 생성됩니다.
D/CameraXApp: Average luminosity: ...

7. VideoCapture 사용 사례 구현

CameraX는 버전 1.1.0-alpha10에 VideoCapture 사용 사례를 추가했고 그 후로 계속 개선되고 있습니다. VideoCapture API는 여러 동영상 캡처 기능을 지원하므로 이 Codelab을 쉽게 관리할 수 있도록 이 Codelab에서는 MediaStore에 동영상 및 오디오를 캡처하는 방법만 보여줍니다.

  1. 다음 코드를 captureVideo() 메서드에 복사합니다. 이 코드는 VideoCapture 사용 사례의 시작과 중지를 모두 제어합니다. 아래 글머리기호는 방금 복사한 코드를 분석합니다.
// Implements VideoCapture use case, including start and stop capturing.
private fun captureVideo() {
   val videoCapture = this.videoCapture ?: return

   viewBinding.videoCaptureButton.isEnabled = false

   val curRecording = recording
   if (curRecording != null) {
       // Stop the current recording session.
       curRecording.stop()
       recording = null
       return
   }

   // create and start a new recording session
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()
   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .apply {
           if (PermissionChecker.checkSelfPermission(this@MainActivity,
                   Manifest.permission.RECORD_AUDIO) ==
               PermissionChecker.PERMISSION_GRANTED)
           {
               withAudioEnabled()
           }
       }
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
                            .show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "Video capture ends with error: " +
                           "${recordEvent.error}")
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}
  • VideoCapture 사용 사례가 생성되었는지 확인합니다. 생성되지 않은 경우 아무것도 하지 않습니다.
val videoCapture = videoCapture ?: return
  • CameraX에서 요청 작업을 완료할 때까지 UI를 사용 중지합니다. 이후 단계에서 등록된 VideoRecordListener 내에서 다시 사용 설정됩니다.
viewBinding.videoCaptureButton.isEnabled = false
  • 진행 중인 활성 녹화가 있으면 중지하고 현재 recording을 해제합니다. 캡처된 동영상 파일을 애플리케이션에서 사용할 준비가 되면 알림이 전송됩니다.
val curRecording = recording
if (curRecording != null) {
    curRecording.stop()
    recording = null
    return
}
  • 녹화를 시작하기 위해 새 녹화 세션을 만듭니다. 먼저 시스템 타임스탬프를 표시 이름으로 사용하여, 의도한 MediaStore 동영상 콘텐츠 객체를 만듭니다. 이렇게 하면 여러 동영상을 캡처할 수 있습니다.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
           .format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH,
               "Movies/CameraX-Video")
       }
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
      .Builder(contentResolver,
               MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
  • 만든 동영상 contentValuesMediaStoreOutputOptions.Builder로 설정하고 MediaStoreOutputOptions 인스턴스를 빌드합니다.
    .setContentValues(contentValues)
    .build()
    videoCapture
    .output
    .prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
  • 이 녹음에서 오디오를 사용 설정합니다.
.apply {
   if (PermissionChecker.checkSelfPermission(this@MainActivity,
           Manifest.permission.RECORD_AUDIO) ==
       PermissionChecker.PERMISSION_GRANTED)
   {
       withAudioEnabled()
   }
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
   //lambda event listener
}
  • 카메라 기기에서 요청 녹화를 시작하면 'Start Capture' 버튼 텍스트를 'Stop Capture'로 전환합니다.
is VideoRecordEvent.Start -> {
    viewBinding.videoCaptureButton.apply {
        text = getString(R.string.stop_capture)
        isEnabled = true
    }
}
  • 활성 녹화가 완료되면 토스트 메시지로 사용자에게 알리고 'Stop Capture' 버튼을 다시 'Start Capture'로 전환한 후 다시 사용 설정합니다.
is VideoRecordEvent.Finalize -> {
   if (!recordEvent.hasError()) {
       val msg = "Video capture succeeded: " +
                 "${recordEvent.outputResults.outputUri}"
       Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
            .show()
       Log.d(TAG, msg)
   } else {
       recording?.close()
       recording = null
       Log.e(TAG, "Video capture succeeded: " +
                  "${recordEvent.outputResults.outputUri}")
   }
   viewBinding.videoCaptureButton.apply {
       text = getString(R.string.start_capture)
       isEnabled = true
   }
}
  1. startCamera()에서 preview 생성 줄 뒤에 다음 코드를 삽입합니다. 이렇게 하면 VideoCapture 사용 사례가 생성됩니다.
val recorder = Recorder.Builder()
   .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
   .build()
videoCapture = VideoCapture.withOutput(recorder)
  1. (선택사항) startCamera() 내에서 다음 코드를 삭제하거나 주석 처리하여 imageCaptureimageAnalyzer 사용 사례를 사용 중지합니다.
/* comment out ImageCapture and ImageAnalyzer use cases
imageCapture = ImageCapture.Builder().build()

val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
*/
  1. Preview + VideoCapture 사용 사례를 수명 주기 카메라에 바인딩합니다. startCamera() 내에서 cameraProvider.bindToLifecycle() 호출을 다음으로 바꿉니다.
   // Bind use cases to camera
   cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)

이 시점에서 startCamera()는 다음과 같이 표시됩니다.

   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener({
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
           }

       val recorder = Recorder.Builder()
           .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
           .build()
       videoCapture = VideoCapture.withOutput(recorder)

       /*
       imageCapture = ImageCapture.Builder().build()

       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }
       */

       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()

           // Bind use cases to camera
           cameraProvider
               .bindToLifecycle(this, cameraSelector, preview, videoCapture)
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  1. 빌드하고 실행합니다. 이전 단계의 익숙한 UI가 표시됩니다.
  2. 클립을 녹화합니다.
  • 'START CAPTURE' 버튼을 누릅니다. 설명이 'STOP CAPTURE'로 변경됩니다.
  • 동영상을 몇 초/분 동안 녹화합니다.
  • 'STOP CAPTURE' 버튼(캡처 시작과 동일한 버튼)을 누릅니다.

ef2a6005defc4977.png 8acee41fd0f4af0f.png

동영상 보기(캡처 이미지 파일 보기와 동일)

Google 포토 앱을 사용하여, 캡처된 동영상을 검토합니다.

  1. Google 포토 사진를 시작합니다.
  2. 정렬된 미디어 파일을 보려면 '라이브러리'를 탭합니다. "CameraX-Video" 폴더 아이콘을 탭하여 사용 가능한 동영상 클립 목록을 표시합니다.

71f07e32d5f4f268.png 596819ad391fac37.png

  1. 아이콘을 탭하여 방금 캡처한 동영상 클립을 재생합니다. 재생이 완료되면 오른쪽 상단에서 더보기 버튼 더보기을 탭하여 클립 세부정보를 확인합니다.

7c7125726af9e429.png 44da18b15ad2f607.png

이제 동영상을 녹화하기만 하면 됩니다. 하지만 CameraX VideoCapture에는 다음을 비롯한 더 많은 기능이 있습니다.

  • 녹화 일시중지/재개
  • File 또는 FileDescriptor에 캡처
  • 기타 등등

사용 방법에 관한 안내는 공식 문서를 참고하세요.

8. (선택사항) VideoCapture를 다른 사용 사례와 결합

이전 VideoCapture 단계에서는 기기 기능 표에 설명된 대로 모든 기기에서 지원되는 PreviewVideoCapture 조합을 설명했습니다. 이 단계에서는 ImageCapture 사용 사례를 기존 VideoCapture + Preview 조합에 추가하여 Preview + ImageCapture + VideoCapture를 보여줍니다.

  1. 이전 단계의 기존 코드를 사용하여 다음과 같이 주석 처리를 삭제하고 startCamera()에서 imageCapture 생성을 사용 설정합니다.
imageCapture = ImageCapture.Builder().build()
  1. 기존 QualitySelector 생성에 FallbackStrategy를 추가합니다. 이렇게 하면 필요한 Quality.HIGHESTimageCapture 사용 사례에서 지원되지 않는 경우 CameraX가 지원되는 해상도를 선택할 수 있습니다.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. 또한 startCamera()에서 imageCapture 사용 사례를 기존 preview 및 videoCapture 사용 사례에 바인딩합니다(참고: preview + imageCapture + videoCapture + imageAnalysis 조합은 지원되지 않으므로 imageAnalyzer는 바인딩하지 마세요).
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, videoCapture)

이제 최종 startCamera() 함수가 다음과 같이 표시됩니다.

private fun startCamera() {
       val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

       cameraProviderFuture.addListener({
           // Used to bind the lifecycle of cameras to the lifecycle owner
           val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

           // Preview
           val preview = Preview.Builder()
               .build()
               .also {
                   it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
               }
           val recorder = Recorder.Builder()
               .setQualitySelector(QualitySelector.from(Quality.HIGHEST,
                    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
               .build()
           videoCapture = VideoCapture.withOutput(recorder)

           imageCapture = ImageCapture.Builder().build()

           /*
           val imageAnalyzer = ImageAnalysis.Builder().build()
               .also {
                   setAnalyzer(
                       cameraExecutor,
                       LuminosityAnalyzer { luma ->
                           Log.d(TAG, "Average luminosity: $luma")
                       }
                   )
               }
           */

           // Select back camera as a default
           val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

           try {
               // Unbind use cases before rebinding
               cameraProvider.unbindAll()

               // Bind use cases to camera
               cameraProvider.bindToLifecycle(
                   this, cameraSelector, preview, imageCapture, videoCapture)

           } catch(exc: Exception) {
               Log.e(TAG, "Use case binding failed", exc)
           }

       }, ContextCompat.getMainExecutor(this))
   }
  1. 빌드하고 실행합니다. 이전 단계의 익숙한 UI가 표시됩니다. 단, 이번에는 Take PhotoStart Capture 버튼이 모두 작동합니다.
  2. 다음과 같이 캡처를 실행합니다.
  • START CAPTURE 버튼을 탭하여 캡처를 시작합니다.
  • TAKE PHOTO를 탭하여 이미지를 캡처합니다.
  • 이미지 캡처가 완료될 때까지 기다립니다. 이전에 본 것처럼 토스트 메시지가 표시됩니다.
  • STOP CAPTURE 버튼을 탭하여 녹화를 중지합니다.

미리보기와 동영상 캡처가 진행되는 동안 이미지를 캡처하고 있습니다.

ef2a6005defc4977.png 16bc70ec3346fa66.png

  1. 이전 단계에서 Google 포토 앱으로 한 것처럼 캡처된 이미지와 동영상 파일을 봅니다. 이번에는 사진 2개와 동영상 클립 2개가 표시됩니다.

3f3feb19c8c73532.png

  1. (선택사항) 위 단계(1단계~4단계)에서 imageCaptureImageAnalyzer 사용 사례로 바꿉니다. Preview + ImageAnalysis + VideoCapture 조합을 사용합니다(Preview + Analysis + ImageCapture + VideoCapture 조합은 LEVEL_3 카메라 기기에서도 지원되지 않을 수 있음).

9. 축하합니다

새 Android 앱에 다음 사항을 처음부터 구현했습니다.

  • 새 프로젝트에 CameraX 종속 항목을 포함했습니다.
  • Preview 사용 사례를 사용하여 카메라 뷰파인더를 표시했습니다.
  • ImageCapture 사용 사례를 사용하여 사진 캡처를 구현하고 저장소에 이미지를 저장했습니다.
  • ImageAnalysis 사용 사례를 사용하여 카메라의 프레임 분석을 실시간으로 구현했습니다.
  • VideoCapture 사용 사례를 사용하여 동영상 캡처를 구현했습니다.

CameraX와 그 용도에 관한 자세한 내용을 알아보려면 문서를 확인하거나 공식 샘플을 클론하세요.