Camera1을 CameraX로 이전

앱에서 Android 5.0(API 수준 21)부터 지원 중단된 원래 Camera 클래스('Camera1')를 사용하는 경우 최신 Android 카메라 API로 업데이트하는 것이 좋습니다. Android는 표준화된 강력한 Jetpack 카메라 API인 CameraX와 하위 수준 프레임워크 API인 Camera2를 제공합니다. 대부분의 경우 앱을 CameraX로 이전하는 것이 좋습니다. 이유는 다음과 같습니다.

  • 사용 편의성: CameraX는 하위 수준의 세부정보를 처리하므로, 카메라 환경을 처음부터 빌드하는 데 신경을 덜 쓰고 앱을 차별화하는 데 더 집중할 수 있습니다.
  • CameraX가 세분화 자동 처리: CameraX는 장기 유지보수 비용과 기기별 코드를 줄이고 사용자에게 더 높은 품질의 환경을 제공합니다. 자세한 내용은 CameraX를 통한 기기 호환성 향상 블로그 게시물을 참고하세요.
  • 고급 기능: CameraX는 고급 기능을 간편하게 앱에 통합할 수 있도록 세심하게 설계되었습니다. CameraX 확장 프로그램을 사용하여 빛망울 효과, 얼굴 보정, HDR(High Dynamic Range), 저조도 야간 캡처 모드 등을 사진에 쉽게 적용할 수 있습니다.
  • 업데이트 가능성: Android는 연중 내내 CameraX에 새로운 기능 및 버그 수정을 출시합니다. CameraX로 이전하면 앱이 연간 Android 버전 출시뿐 아니라 CameraX 출시마다 최신 Android 카메라 기술을 활용하게 됩니다.

이 가이드에서는 카메라 애플리케이션의 일반적인 시나리오를 설명합니다. 각 시나리오에는 비교를 위해 Camera1 구현과 CameraX 구현이 포함되어 있습니다.

이전할 때 기존 코드베이스와 통합 시 유연성이 더 필요한 경우가 있습니다. 이 가이드의 모든 CameraX 코드에는 CameraController 구현(예: 가장 간단한 방법으로 CameraX를 사용하려는 경우 적합)과 CameraProvider 구현(유연성이 더 필요할 때 적합)이 있습니다. 적합한 방법을 선택할 때 다음과 같은 장점을 고려하세요.

CameraController

CameraProvider

설정 코드가 거의 필요 없음 보다 세부적인 제어 가능
CameraX에서 더 많은 설정 프로세스를 처리하도록 허용하면 탭하여 초점 맞추기 및 손가락을 모으거나 펼쳐 확대/축소하기 같은 기능이 자동으로 작동합니다. 앱 개발자가 설정을 처리하므로 예를 들어 출력 이미지 회전을 사용 설정하거나 ImageAnalysis의 출력 이미지 형식을 설정하는 등 구성을 맞춤설정하는 기회가 더 많습니다.
카메라 미리보기에 PreviewView를 필수로 적용하면 얼굴 경계 상자 같은 ML 모델 결과 좌표를 미리보기 좌표에 직접 매핑할 수 있는 ML Kit 통합에서처럼 CameraX가 원활한 엔드 투 엔드 통합을 제공할 수 있습니다. 카메라 미리보기에 맞춤 `Surface`를 사용하는 기능은 앱의 다른 부분에 대한 입력이 될 수 있는 기존 `Surface` 코드를 사용하는 등 더 많은 유연성을 제공합니다.

이전하는 중 문제가 발생하는 경우 CameraX 토론방에서 문의하세요.

이전하기 전에

CameraX와 Camera1 사용 비교

코드는 달라 보일 수 있지만 Camera1과 CameraX의 기본 개념은 매우 유사합니다. CameraX는 일반적인 카메라 기능을 사용 사례에 추상화하며, 결과적으로 Camera1에서 개발자가 처리해야 했던 많은 작업이 CameraX에서는 자동으로 처리됩니다. CameraX에는 다양한 카메라 작업에 사용할 수 있는 Preview, ImageCapture, VideoCapture, ImageAnalysis의 네 가지 UseCase가 있습니다.

개발자를 위해 하위 수준 세부정보를 처리하는 CameraX의 한 가지 예로 활성 UseCase 간에 공유되는 ViewPort를 들 수 있습니다. 이를 통해 모든 UseCase의 픽셀이 정확히 동일하게 됩니다. Camera1에서는 이러한 세부정보를 개발자가 직접 관리해야 하며 기기의 카메라 센서와 화면에서 가로세로 비율의 변동성을 고려할 때 미리보기가 캡처된 사진 및 동영상과 일치하는지 확인하기 어려울 수 있습니다.

또 다른 예로, CameraX는 개발자가 전달한 Lifecycle 콜백을 Lifecycle 인스턴스에서 자동으로 처리합니다. 즉, CameraX는 전체 Android 활동 수명 주기 동안 앱의 카메라 연결을 처리합니다. 여기에는 앱이 백그라운드로 전환될 때 카메라 닫기, 화면에 더 이상 표시할 필요가 없을 때 카메라 미리보기 삭제, 다른 활동(예: 수신되는 영상 통화)이 우선적으로 포그라운드에 표시될 때 카메라 미리보기 일시중지 등과 같은 사례가 포함됩니다.

마지막으로, 개발자가 추가 코드를 작성하지 않고도 CameraX는 회전 및 크기 조정을 처리합니다. 방향이 잠금 해제된 Activity의 경우 방향 변경 시 시스템이 Activity를 삭제하고 다시 생성하기 때문에 기기가 회전할 때마다 UseCase 설정이 실행됩니다. 그 결과 UseCases에서 그때마다 디스플레이 방향과 기본적으로 일치하도록 타겟 회전이 설정됩니다. CameraX의 회전 자세히 알아보기

자세히 알아보기 전에 CameraX의 UseCase 및 Camera1 앱과의 연관성을 간략하게 살펴보겠습니다. (CameraX 개념은 파란색, Camera1 개념은 녹색입니다.)

CameraX

CameraController/CameraProvider 구성
프리뷰 ImageCapture 동영상 캡처 ImageAnalysis
미리보기 노출 영역 관리 및 카메라에서 설정 카메라에서 PictureCallback 설정 및 takePicture() 호출 특정 순서로 카메라 및 MediaRecorder 구성 관리 미리보기 노출 영역 위에 빌드된 맞춤 분석 코드
기기별 코드
기기 회전 및 크기 조정 관리
카메라 세션 관리(카메라 선택, 수명 주기 관리)

Camera1

CameraX의 호환성 및 성능

CameraX는 Android 5.0(API 수준 21) 이상을 실행하는 기기를 지원합니다. 이는 기존 Android 기기의 98%가 넘습니다. CameraX는 기기 간 차이를 자동으로 처리하도록 빌드되어 있어, 앱에 기기별 코드가 필요하지 않습니다. 또한 Google은 CameraX Test Lab에서 5.0 이후 모든 Android 버전을 사용하는 실제 기기를 150대 넘게 테스트합니다. 현재 Test Lab에 있는 기기의 전체 목록을 검토할 수 있습니다.

CameraX는 Executor를 사용하여 카메라 스택을 구동합니다. 앱에 특정 스레딩 요구사항이 있는 경우 CameraX에 자체 실행자를 설정할 수 있습니다. 설정하지 않으면 CameraX는 최적화된 기본 내부 Executor를 만들고 사용합니다. CameraX 빌드에 사용되는 대부분의 플랫폼 API는 하드웨어와의 프로세스 간 통신(IPC)을 차단할 것을 요구합니다. 응답하는 데 수백 밀리초가 걸릴 수 있기 때문입니다. 이런 이유로 CameraX는 백그라운드 스레드에서만 이러한 API를 호출하므로 기본 스레드가 차단되지 않고 UI가 유동적으로 유지됩니다. 스레드 자세히 알아보기

앱의 타겟 시장에 저사양 기기가 포함된 경우 CameraX를 사용하면 카메라 리미터로 설정 시간을 줄일 수 있습니다. 하드웨어 구성요소에 연결하는 프로세스는 특히 저사양 기기에서 상당한 시간이 걸릴 수 있으므로, 앱에 필요한 카메라 집합을 지정할 수 있습니다. CameraX는 설정 중에 이러한 카메라에만 연결됩니다. 예를 들어 애플리케이션이 후면 카메라만 사용하는 경우 DEFAULT_BACK_CAMERA로 이 구성을 설정하면 CameraX가 전면 카메라 초기화를 방지하여 지연 시간이 줄어듭니다.

Android 개발 개념

이 가이드에서는 Android 개발에 관한 일반적인 지식을 가정합니다. 기본사항 외에 본격적으로 코드를 알아보기 전에 다음과 같은 개념을 알면 이해하는 데 도움이 됩니다.

  • 뷰 결합은 XML 레이아웃 파일의 결합 클래스를 생성하므로, 개발자는 아래의 코드 스니펫에서 하는 것처럼 활동에서 뷰를 쉽게 참조할 수 있습니다. 뷰 결합과 findViewById()(이전의 뷰 참조 방식) 간에 일부 차이점이 있지만 아래의 코드에서 뷰 결합 줄을 유사한 findViewById() 호출로 바꿀 수 있어야 합니다.
  • 비동기 코루틴은 Kotlin 1.3에서 추가된 동시 실행 설계 패턴으로, ListenableFuture를 반환하는 CameraX 메서드를 처리하는 데 사용할 수 있습니다. 버전 1.1.0부터 Jetpack Concurrent 라이브러리를 사용하면 이 작업이 더 쉬워집니다. 앱에 비동기 코루틴을 추가하려면 다음 단계를 따르세요.
    1. Gradle 파일에 implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")을 추가합니다.
    2. ListenableFuture를 반환하는 CameraX 코드를 launch 블록 또는 정지 함수에 배치합니다.
    3. ListenableFuture를 반환하는 함수 호출에 await() 호출을 추가합니다.
    4. 코루틴의 작동 방식에 관한 자세한 내용은 코루틴 시작 가이드를 참고하세요.

일반적인 시나리오 이전

이 섹션에서는 일반적인 시나리오를 Camera1에서 CameraX로 이전하는 방법을 설명합니다. 각 시나리오에서는 Camera1 구현, CameraX CameraProvider 구현, CameraX CameraController 구현을 다룹니다.

카메라 선택

카메라 애플리케이션에서 가장 먼저 제공해야 할 것은 다른 카메라를 선택하는 방법입니다.

Camera1

Camera1에서는 매개변수 없이 Camera.open()을 호출하여 첫 번째 후면 카메라를 열거나 열려는 카메라의 정수 ID를 전달할 수 있습니다. 예를 들면 다음과 같습니다.

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX: CameraController

CameraX에서 카메라 선택은 CameraSelector 클래스에 의해 처리됩니다. CameraX를 사용하면 기본 카메라를 사용하는 일반적인 사례가 쉬워집니다. 기본 전면 카메라와 기본 후면 카메라 중 무엇을 사용할지 지정할 수 있습니다. 또한 CameraX의 CameraControl 객체를 사용하면 손쉽게 앱의 확대/축소 수준을 설정할 수 있으므로 논리 카메라를 지원하는 기기에서 앱이 실행 중이라면 적절한 렌즈로 전환됩니다.

다음은 CameraController로 기본 후면 카메라를 사용하는 CameraX 코드입니다.

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX: CameraProvider

다음은 CameraProvider로 기본 전면 카메라를 선택하는 예입니다. 전면 또는 후면 카메라를 CameraController 또는 CameraProvider와 함께 사용할 수 있습니다.

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

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

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

어느 카메라가 선택될지 제어하고 싶다면 CameraX에서 가능합니다. CameraProvider를 사용하는 경우 getAvailableCameraInfos()를 호출하면 isFocusMeteringSupported()와 같은 특정 카메라 속성을 확인하는 CameraInfo 객체를 얻을 수 있습니다. 그런 다음 위의 예와 같이 CameraInfo.getCameraSelector() 메서드를 사용하여 CameraSelector로 변환하여 사용할 수 있습니다.

Camera2CameraInfo 클래스를 사용하여 각 카메라에 관한 세부정보를 가져올 수 있습니다. 원하는 카메라 데이터의 키로 getCameraCharacteristic()을 호출합니다. 쿼리할 수 있는 모든 키 목록은 CameraCharacteristics 클래스를 확인하세요.

다음은 직접 정의할 수 있는 맞춤 checkFocalLength() 함수를 사용하는 예입니다.

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

미리보기 표시

대부분의 카메라 애플리케이션은 특정 시점에 카메라 피드를 화면에 표시해야 합니다. Camera1에서는 수명 주기 콜백을 올바르게 관리해야 하며 미리보기의 회전 및 크기 조정도 결정해야 합니다.

또한 Camera1에서는 미리보기 노출 영역으로 TextureView를 사용할지 아니면 SurfaceView를 사용할지 결정해야 합니다. 두 옵션 모두 단점은 있지만 어느 경우든 Camera1을 사용할 때는 개발자가 회전과 크기 조정을 올바르게 처리해야 합니다. 반면 CameraX의 PreviewView에는 TextureViewSurfaceView의 기본 구현이 있습니다. CameraX는 기기 유형 및 앱을 실행하는 Android 버전과 같은 요소에 따라 가장 적합한 구현을 결정합니다. 두 구현 중 하나가 호환되는 경우 PreviewView.ImplementationMode를 사용하여 기본 설정을 선언할 수 있습니다. COMPATIBLE 옵션은 미리보기에 TextureView를 사용하고, PERFORMANCE 값은 가능한 경우 SurfaceView를 사용합니다.

Camera1

미리보기를 표시하려면 카메라 하드웨어에서 애플리케이션으로 이미지 데이터를 전달하는 데 사용되는 android.view.SurfaceHolder.Callback 인터페이스의 구현을 사용하여 Preview 클래스를 직접 작성해야 합니다. 그런 다음, 라이브 이미지 미리보기를 시작하려면 먼저 Preview 클래스를 Camera 객체에 전달해야 합니다.

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

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

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX: CameraController

CameraX에서는 개발자가 관리할 사항이 훨씬 적습니다. CameraController를 사용하는 경우 PreviewView도 사용해야 합니다. 즉, Preview UseCase가 함축되어 있으므로 설정 작업이 훨씬 줄어듭니다.

// CameraX: set up a camera preview with a CameraController.

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

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

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX: CameraProvider

CameraX의 CameraProvider를 사용하면 PreviewView를 사용할 필요가 없지만 Camera1보다 미리보기 설정이 크게 간소화됩니다. 시연을 위해 이 예에서는 PreviewView를 사용하지만, 더 복잡한 요구사항이 있는 경우 맞춤 SurfaceProvider를 작성하여 setSurfaceProvider()에 전달할 수 있습니다.

여기서는 Preview UseCaseCameraController의 경우처럼 함축되어 있지 않으므로 설정해야 합니다.

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

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

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

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

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

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

탭하여 초점 맞추기

카메라 미리보기가 화면에 표시될 때 일반적인 컨트롤은 사용자가 미리보기를 탭하는 경우의 포커스 포인트를 설정하는 것입니다.

Camera1

Camera1에서 탭하여 초점 맞추기를 구현하려면 최적의 포커스 Area을 계산하여 Camera가 포커스를 두어야 하는 위치를 나타내야 합니다. 이 AreasetFocusAreas()에 전달됩니다. 또한 Camera에서 호환되는 포커스 모드를 설정해야 합니다. 현재 포커스 모드가 FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO 또는 FOCUS_MODE_CONTINUOUS_PICTURE인 경우에만 포커스 영역이 효과가 있습니다.

Area는 지정된 가중치가 있는 직사각형입니다. 가중치는 1과 1,000 사이의 값이며 포커스 Areas가 여러 개 설정된 경우 우선순위를 지정하는 데 사용됩니다. 이 예에서는 하나의 Area만 사용하므로 가중치 값은 중요하지 않습니다. 직사각형의 좌표 범위는 -1000부터 1000까지입니다. 왼쪽 상단 지점은 (-1000, -1000)이고 오른쪽 하단 지점은 (1000, 1000)입니다. 방향은 센서 방향, 즉 센서에 감지되는 방향을 기준으로 합니다. 방향은 Camera.setDisplayOrientation()의 회전 또는 미러링에 영향을 받지 않으므로, 터치 이벤트 좌표를 센서 좌표로 변환해야 합니다.

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX: CameraController

CameraControllerPreviewView의 터치 이벤트를 수신 대기하여 '탭하여 초점 맞추기'를 자동으로 처리합니다. setTapToFocusEnabled()로 탭하여 초점 맞추기를 사용 설정하거나 사용 중지하고 해당하는 getter isTapToFocusEnabled()로 값을 확인합니다.

getTapToFocusState() 메서드는 CameraController의 포커스 상태 변경사항을 추적하기 위한 LiveData 객체를 반환합니다.

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX: CameraProvider

CameraProvider를 사용할 때는 탭하여 초점 맞추기를 작동시키기 위해 몇 가지 설정이 필요합니다. 이 예에서는 PreviewView를 사용한다고 가정합니다. 그렇지 않으면 맞춤 Surface에 적용할 로직을 조정해야 합니다.

PreviewView를 사용하는 단계는 다음과 같습니다.

  1. 탭 이벤트를 처리하도록 동작 감지기를 설정합니다.
  2. 탭 이벤트에 관해 MeteringPointFactory.createPoint()를 사용하여 MeteringPoint를 만듭니다.
  3. MeteringPoint를 사용하여 FocusMeteringAction을 만듭니다.
  4. Camera에서 CameraControl 객체(bindToLifecycle()에서 반환됨)를 사용하여 startFocusAndMetering()을 호출하고 FocusMeteringAction을 전달합니다.
  5. (선택사항) FocusMeteringResult에 응답합니다.
  6. PreviewView.setOnTouchListener()에서 터치 이벤트에 응답하도록 동작 감지기를 설정합니다.
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

손가락을 모으거나 펼쳐 확대/축소

미리보기 확대/축소는 카메라 미리보기의 또 다른 일반적인 직접 조작입니다. 기기의 카메라 수가 증가함에 따라 사용자는 확대/축소의 결과로 초점 거리가 최적인 렌즈가 자동으로 선택될 것을 기대합니다.

Camera1

Camera1을 사용하여 확대/축소하는 데는 두 가지 방법이 있습니다. Camera.startSmoothZoom() 메서드는 현재 확대/축소 수준에서 개발자가 전달한 확대/축소 수준으로 애니메이션 처리합니다. Camera.Parameters.setZoom() 메서드는 개발자가 전달한 확대/축소 수준으로 바로 이동합니다. 두 방법 중 하나를 사용하기 전에 각각 isSmoothZoomSupported() 또는 isZoomSupported()를 호출하여 필요한 관련 확대/축소 메서드를 카메라에서 사용할 수 있도록 합니다.

손가락을 모으거나 펼쳐 확대/축소하기를 구현하기 위해 이 예에서는 setZoom()을 사용합니다. 미리보기 노출 영역의 터치 리스너가 손가락 모으기 동작이 발생할 때 이벤트를 계속 발생시키므로 확대/축소 수준이 매번 즉시 업데이트되기 때문입니다. ZoomTouchListener 클래스는 아래에 정의되어 있으며 미리보기 노출 영역의 터치 리스너에 대한 콜백으로 설정해야 합니다.

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX: CameraController

탭하여 초점 맞추기와 마찬가지로 CameraController는 PreviewView의 터치 이벤트를 수신 대기하여 '손가락을 모으거나 펼쳐 확대/축소'를 자동으로 처리합니다. setPinchToZoomEnabled()로 탭하여 손가락을 모으거나 펼쳐 확대/축소를 사용 설정하거나 사용 중지하고 해당하는 getter isPinchToZoomEnabled()로 값을 확인합니다.

getZoomState() 메서드는 CameraController에서 ZoomState 변경사항을 추적하기 위해 LiveData 객체를 반환합니다.

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: CameraProvider

CameraProvider에서 손가락을 모으거나 펼쳐 확대/축소하려면 일부 설정이 필요합니다. PreviewView를 사용하지 않는 경우에는 맞춤 Surface에 적용할 로직을 조정해야 합니다.

PreviewView를 사용하는 단계는 다음과 같습니다.

  1. 손가락 모으기 이벤트를 처리하도록 크기 조정 동작 감지기를 설정합니다.
  2. bindToLifecycle()을 호출할 때 Camera 인스턴스가 반환되는 Camera.CameraInfo 객체에서 ZoomState를 가져옵니다.
  3. ZoomStatezoomRatio 값이 있으면 이 값을 현재 확대/축소 비율로 저장합니다. ZoomStatezoomRatio가 없으면 카메라의 기본 확대/축소 비율(1.0)을 사용합니다.
  4. 현재 확대/축소 비율과 scaleFactor의 곱을 구하여 새 확대/축소 비율을 결정하고 이 값을 CameraControl.setZoomRatio()에 전달합니다.
  5. PreviewView.setOnTouchListener()에서 터치 이벤트에 응답하도록 동작 감지기를 설정합니다.
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

사진 촬영

이 섹션에서는 촬영 버튼을 눌렀을 때 타이머가 경과한 후 또는 선택한 기타 이벤트 발생 시 필요에 따라 사진 캡처를 트리거하는 방법을 설명합니다.

Camera1

Camera1에서는 먼저, 요청 시 사진 데이터를 관리하도록 Camera.PictureCallback을 정의합니다. 다음은 JPEG 이미지 데이터를 처리하는 PictureCallback의 간단한 예입니다.

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

그런 다음, 사진을 촬영할 때마다 Camera 인스턴스에서 takePicture() 메서드를 호출합니다. 이 takePicture() 메서드에는 데이터 유형에 따라 세 가지 매개변수가 있습니다. 첫 번째 매개변수는 이 예에서는 정의되지 않은 ShutterCallback용입니다. 두 번째 매개변수는 PictureCallback이 원시(비압축) 카메라 데이터를 처리하기 위한 것입니다. 세 번째 매개변수는 이 예에서 사용하는 매개변수로, JPEG 이미지 데이터를 처리하기 위한 PictureCallback입니다.

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraX의 CameraController는 자체 takePicture() 메서드를 구현하여 이미지 캡처를 위한 Camera1의 단순성을 유지합니다. 여기서 MediaStore 항목을 구성하고 여기에 저장할 사진을 촬영하는 함수를 정의합니다.

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // 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(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

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

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

CameraX: CameraProvider

CameraProvider를 사용한 사진 촬영은 CameraController와 거의 같은 방식으로 작동하지만, 먼저 takePicture()를 호출할 객체가 있도록 ImageCapture UseCase를 만들고 결합해야 합니다.

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

그런 다음, 사진을 캡처할 때마다 ImageCapture.takePicture()를 호출할 수 있습니다. takePhoto() 함수의 전체 예는 이 섹션의 CameraController 코드를 참고하세요.

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

동영상 녹화

동영상 녹화는 지금까지 살펴본 시나리오보다 훨씬 더 복잡합니다. 프로세스의 각 부분은 일반적으로 특정 순서로 올바르게 설정되어야 합니다. 또한 동영상 및 오디오가 동기화되어 있는지 확인하거나 추가 기기 불일치를 처리해야 할 수도 있습니다.

앞으로 살펴보겠지만, CameraX는 이러한 다양한 복잡성을 자동으로 처리합니다.

Camera1

Camera1을 사용하여 동영상을 캡처하려면 CameraMediaRecorder를 신중하게 관리해야 하며 메서드를 특정 순서로 호출해야 합니다. 이러한 순서를 반드시 따라야 애플리케이션이 제대로 작동합니다.

  1. 카메라를 엽니다.
  2. 미리보기를 준비하고 시작합니다(앱에서 녹화 중인 동영상이 표시되는 경우).
  3. Camera.unlock()을 호출하여 MediaRecorder에서 사용하도록 카메라를 잠금 해제합니다.
  4. MediaRecorder에서 다음 메서드를 호출하여 녹화를 구성합니다.
    1. Camera 인스턴스를 setCamera(camera)와 연결합니다.
    2. setAudioSource(MediaRecorder.AudioSource.CAMCORDER)를 호출합니다.
    3. setVideoSource(MediaRecorder.VideoSource.CAMERA)를 호출합니다.
    4. setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))를 호출하여 화질을 설정합니다. 모든 화질 옵션은 CamcorderProfile을 참고하세요.
    5. setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())을 호출합니다.
    6. 앱에 동영상 미리보기가 있으면 setPreviewDisplay(preview?.holder?.surface)를 호출합니다.
    7. setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)을 호출합니다.
    8. setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)를 호출합니다.
    9. setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)를 호출합니다.
    10. prepare()를 호출하여 MediaRecorder 구성을 마무리합니다.
  5. 녹화를 시작하려면 MediaRecorder.start()를 호출합니다.
  6. 녹화를 중지하려면 이러한 메서드를 호출합니다. 다시 한 번 이 순서를 정확히 따릅니다.
    1. MediaRecorder.stop()을 호출합니다.
    2. 선택적으로, MediaRecorder.reset()을 호출하여 현재 MediaRecorder 구성을 삭제합니다.
    3. MediaRecorder.release()를 호출합니다.
    4. 향후 MediaRecorder 세션에서 Camera.lock()을 호출하여 카메라를 사용할 수 있도록 카메라를 잠급니다.
  7. 미리보기를 중지하려면 Camera.stopPreview()를 호출합니다.
  8. 마지막으로, 다른 프로세스에서 사용할 수 있도록 Camera를 해제하려면 Camera.release()를 호출합니다.

다음은 이러한 모든 단계를 결합한 것입니다.

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX: CameraController

CameraX의 CameraController를 사용하는 경우에는 UseCase 목록을 동시에 사용할 수 있다면 ImageCapture, VideoCapture, ImageAnalysis UseCase를 독립적으로 전환할 수 있습니다. ImageCaptureImageAnalysis UseCase는 기본적으로 사용 설정되어 있으므로 사진 촬영을 위해 setEnabledUseCases()를 호출하지 않아도 되었습니다.

동영상 녹화에 CameraController를 사용하려면 먼저 setEnabledUseCases()를 사용하여 VideoCapture UseCase를 허용해야 합니다.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

동영상 녹화를 시작하려면 CameraController.startRecording() 함수를 호출하면 됩니다. 이 함수는 아래 예에서 볼 수 있듯이 녹화된 동영상을 File에 저장할 수 있습니다. 또한 ExecutorOnVideoSavedCallback을 구현하는 클래스를 전달하여 성공 및 오류 콜백을 처리해야 합니다. 녹화를 종료해야 할 때 CameraController.stopRecording()을 호출합니다.

참고: CameraX 1.3.0-alpha02 이상을 사용하는 경우 동영상의 오디오 녹음을 사용 설정하거나 사용 중지할 수 있는 추가 AudioConfig 매개변수가 있습니다. 오디오 녹음을 사용 설정하려면 마이크 권한이 있는지 확인해야 합니다. 또한 stopRecording() 메서드가 1.3.0-alpha02에서 삭제되며, startRecording()은 동영상 녹화를 일시중지, 다시 시작, 중지하는 데 사용할 수 있는 Recording 객체를 반환합니다.

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX: CameraProvider

CameraProvider를 사용 중인 경우 VideoCapture UseCase를 만들고 Recorder 객체를 전달해야 합니다. Recorder.Builder에서 동영상 화질을 설정할 수 있으며 선택적으로 원하는 품질 사양을 충족하지 못하는 사례를 처리하는 FallbackStrategy를 설정할 수 있습니다. 그런 다음, VideoCapture 인스턴스를 다른 UseCase와 함께 CameraProvider에 결합합니다.

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

이 시점에서 videoCapture.output 속성에서 Recorder에 액세스할 수 있습니다. RecorderFile, ParcelFileDescriptor 또는 MediaStore에 저장되는 동영상 녹화를 시작할 수 있습니다. 이 예에서는 MediaStore를 사용합니다.

Recorder에서는 준비에 필요한 몇 가지 메서드가 호출됩니다. prepareRecording()을 호출하여 MediaStore 출력 옵션을 설정합니다. 앱에 기기의 마이크를 사용할 권한이 있다면 withAudioEnabled()도 호출합니다. 그런 다음, start()를 호출하여 녹화를 시작하고 동영상 녹화 이벤트를 처리하도록 컨텍스트 및 Consumer<VideoRecordEvent> 이벤트 리스너를 전달합니다. 성공하면 반환된 Recording을 사용하여 녹화를 일시중지, 다시 시작 또는 중지할 수 있습니다.

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.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)
       .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
                   }
               }
           }
       }
}

추가 리소스

카메라 샘플 GitHub 저장소에 완전한 CameraX 앱이 몇 가지 있습니다. 이 샘플은 이 가이드의 시나리오가 완전한 Android 앱에 어떻게 적용되는지 보여줍니다.

CameraX로의 이전과 관련한 추가 지원이 필요하거나 Android Camera API 제품군에 관해 궁금한 점이 있으면 CameraX 토론방에서 문의하세요.