관리 메뉴

나만을 위한 블로그

[Android] CameraX 코드랩 뜯어보기 - 1 - 본문

Android

[Android] CameraX 코드랩 뜯어보기 - 1 -

참깨빵위에참깨빵 2022. 6. 5. 15:48
728x90
반응형

카메라는 내게 많이 생소한 영역이기도 하고 예전에 CameraX인지 뭔지가 새로 나왔다고 들었어서 최근에 코드랩을 따라 쳐보고 공부하긴 했었는데, 블로그에 남겨두면 나중에 찾아보기 더 좋을 것 같아서 남겨둔다.

시작하기 전에 내가 본 코드랩 주소는 아래에 남겨둔다.

 

https://developer.android.com/codelabs/camerax-getting-started#0

 

Getting Started with CameraX  |  Android Developers

This codelab introduces how to create a camera app that uses CameraX to show a viewfinder, take photos and analyze an image stream from the camera.

developer.android.com

 

먼저 앱 수준 gradle에 의존성 몇 개를 추가한다. 22.06.05 기준으로 최신 버전은 1.2.0-alpha02지만 코드랩 기준으로 진행한다.

 

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"

 

그리고 카메라를 사용할 것이기 때문에 관련 권한들도 매니페스트에 까먹지 않고 정의해둔다. 영상 녹화까지 다루기 때문에 RECORD_AUDIO 퍼미션이 있는 게 보인다.

 

<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"
    tools:ignore="ScopedStorage" />

 

그리고 자바 1.8 이상을 사용하기 위해 아래 코드를 넣어야 하는데, 범블비 이상이라면 기본으로 설정돼 있을 것이다. 만약 없다면 추가한다. 그리고 뷰 바인딩도 추가해준다.

 

buildFeatures {
    viewBinding true
}
.
.
.
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

 

그리고 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>

 

그리고 메인 액티비티의 XML 코드를 복붙한다. 다른 이름의 액티비티에서 쓸 거라면 필요한 부분만 복붙한다.

 

<?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>

 

그리고 메인 액티비티 소스코드를 붙여넣는다.

 

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.camera.video.VideoCapture
import androidx.core.content.PermissionChecker
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import android.provider.MediaStore

import android.content.ContentValues
import android.os.Build

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()
   }
}

 

먼저 allPermissionsGranted()부터 보자. REQUIRED_PERMISSIONS는 CAMERA, RECORD_AUDIO 퍼미션이 담긴 수정 가능한 리스트에 안드로이드 파이 이하 버전인 경우 WRITE_EXTERNAL_STORAGE 퍼미션을 추가한 다음 toTypedArray()를 적용한 결과값이 담긴 프로퍼티다.

toTypedArray()는 List를 Array로 바꿔주는 함수인데, 이 함수를 써야 하는 이유는 아래와 같다.

 

 

ActivityCompat.requestPermissions() 안에서 빨간 줄이 생기며 컴파일 에러가 발생하는데, 함수가 요구하는 파라미터를 넣지 않았기 때문이다. 이 함수의 시그니처를 확인해 보면 toTypedArray()를 쓴 이유를 더 잘 이해할 수 있다. 자바로 돼 있지만 이해하는 덴 문제없을 것이다.

 

public static void requestPermissions(final @NonNull Activity activity,
            final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)

 

이렇게 생겨먹었기 때문에 toTypedArray()를 썼지만, 애초에 처음부터 배열로 만들었다면 이렇게 할 이유가 없지 않았을까 생각된다. 아무튼 이렇게 권한을 요청하는 방법도 있단 걸 알았으니 넘어간다.

 

다시 onCreate() 안으로 돌아와서 allPermissionsGranted()로 모든 권한이 승낙됐다면 카메라를 시작하고 아니면 다시 사용자에게 권한을 요청하는데 생소한 코드가 보인다.

 

private lateinit var cameraExecutor: ExecutorService

cameraExecutor = Executors.newSingleThreadExecutor()

 

ExecutorService와 newSingleThreadExecutor()는 뭘까? 먼저 ExecutorService부터 확인해본다.

 

https://www.javatpoint.com/java-executorservice

 

Java ExecutorService - Javatpoint

Java ExecutorService with oops, string, exceptions, multithreading, collections, jdbc, rmi, fundamentals, programs, swing, javafx, io streams, networking, sockets, classes, objects etc,

www.javatpoint.com

쓰레드에서 작업을 비동기적으로 실행할 수 있게 해주는 인터페이스다. java.util.concurrent 패키지에 있다. 쓰레드풀을 유지관리하고 작업을 할당하는 데 도움이 된다. 작업 수가 사용 가능한 쓰레드보다 많은 경우 사용 가능한 쓰레드가 있을 때까지 작업을 대기열(queue)에 추가하는 기능을 제공한다

 

https://jaeryo2357.tistory.com/50

 

[Android] Executor - 기능 별 Thread 분리

최근 Google Architecture MVP Sample project를 참고하여 MVP를 공부하던 도중 Executor 의 클래스를 처음 보게 되었다. 그 후 구글링을 통해 네트워크 IO 통신과 디스크 IO 작업, UI 작업등 Background 작업을..

jaeryo2357.tistory.com

구조로만 보면 Runnable 객체를 실행하는 단순한 구조다. 클래스 자체로 어떤 백그라운드 프로세스가 생성되는 게 아닌 틀만 제공해준다. 따라서 대부분의 사용자는 Executor를 구현해서 백그라운드 작업을 수행하는 클래스를 정의한다
Executor의 구조를 보면 별도 쓰레드를 만들지 않는 걸 볼 수 있다. 이 말은 해당 객체를 호출한 쓰레드에서 Runnable을 실행한다는 뜻이다. 따라서 네트워크 I/O 작업을 메인 쓰레드에서 진행할 수 없고 UI가 멈춘 채 Runnable이 실행되면 사용자에게 불친절하기 때문에 별도의 쓰레드 풀을 갖고 있어야 한다. Executors 클래스는 ExecutorService라는 Executor 인터페이스를 확장하고 쓰레드 사이클을 관리하는 쓰레드 풀 객체를 만들어주는 여러 팩토리 함수를 갖고 있다
쓰레드 개수가 무분별하게 늘어나다 보면 그걸 관리하는 CPU에 과부하가 생긴다. Executors.newFixedThreadPool(int poolsize)는 동시 실행되는 쓰레드 개수를 매개변수로 받은 size만큼 조정한다. 2를 받았다면 3개 작업이 들어왔을 때 마지막으로 들어온 작업 1개는 앞의 2개 작업이 끝날 때까지 대기한다

 

ExecutorService는 쓰레드 풀을 관리하고 비동기 작업을 수행할 때 사용하는 인터페이스로 작업 개수가 사용 가능한 쓰레드보다 많은 경우 어떤 큐에 작업을 추가한다. 이 큐는 작업을 순서대로 갖고 있다가 메인 쓰레드에 순서대로 할당한다. onDestroy()에서 이 객체를 통해 shutDown()을 명시적으로 호출한 걸 보니 한 번 사용했다면 반드시 닫아줘야 하는 객체인 듯하다.

newSingleThreadExecutor()는 쓰레드가 1개인 ExecutorService를 리턴하는 메서드로 싱글 쓰레드에서 동작해야 하는 작업을 처리할 때 사용한다. 이 메서드의 정보는 아래 사이트에서 확인할 수 있다.

 

https://developer.android.com/reference/java/util/concurrent/Executors#newSingleThreadExecutor() 

 

Executors  |  Android Developers

android.net.wifi.hotspot2.omadm

developer.android.com

무제한 큐에서 작동하는 단일 워커 쓰레드를 쓰는 실행기를 만든다. 그러나 이 단일 쓰레드가 종료 전 실행 중 실패로 인해 종료되면 후속 작업을 실행하는 데 필요한 경우 새 쓰레드가 그 자리를 차지한다. 작업은 순차 실행되고 하나 이상의 작업이 활성화되지 않는다. newFixedThreadPool(1)과 달리 반환된 실행자는 추가 쓰레드를 사용하도록 재구성되지 않게 보장된다

 

그 외에는 시그니처만 있고 구현부는 없는 메서드거나, 간단한 상수 뿐이라서 넘겨도 될 듯하다.

반응형
Comments