Começar a usar a CameraX

1. Antes de começar

Neste codelab, você aprenderá a criar um app de câmera que usa a CameraX para mostrar um visor, tirar fotos, capturar vídeos e analisar um stream de imagem da câmera.

Para isso, vamos apresentar o conceito de casos de uso na CameraX, que pode ser usada em diversas operações de câmera, como mostrar um visor para a captura de vídeos.

Pré-requisitos

  • Experiência básica de desenvolvimento para Android.
  • É recomendado ter conhecimento sobre a MediaStore, mas não obrigatório.

O que você vai fazer

  • Aprender a adicionar as dependências da CameraX.
  • Aprender a mostrar a visualização da câmera em uma atividade (caso de uso da classe Preview).
  • Criar um app que possa tirar uma foto e salvar essa imagem no armazenamento (caso de uso da classe ImageCapture).
  • Aprender a analisar frames da câmera em tempo real (caso de uso da classe ImageAnalysis).
  • Aprender a capturar vídeos para a MediaStore (caso de uso da API VideoCapture).

O que será necessário

  • Um dispositivo Android ou emulador do Android Studio:
  • Recomendamos o Android 10 e versões mais recentes: o comportamento da MediaStore depende da disponibilidade do armazenamento com escopo.
  • Com o Android Emulator**, recomendamos o uso de um Dispositivo virtual Android (AVD, na sigla em inglês) baseado no Android 11 ou versão mais recente**.
  • A CameraX exige apenas que o nível mínimo da API seja 21.
  • Android Studio Arctic Fox 2020.3.1 ou mais recente.
  • Entender o Kotlin e a ViewBinding do Android.

2. Criar o projeto

  1. No Android Studio, crie um projeto e selecione Empty Activity quando solicitado.

ed0f21e863f9e38f.png

  1. Em seguida, nomeie o app como "CameraXApp" e confirme ou mude o nome do pacote para "com.android.example.cameraxapp". Escolha a linguagem Kotlin e defina o nível mínimo de API como 21, necessário para a CameraX. Para versões mais antigas do Android Studio, inclua o suporte a artefatos do AndroidX.

10f0a12f6c8b997c.png

Adicionar as dependências do Gradle

  1. Abra o arquivo build.gradle para o módulo CameraXApp.app e adicione as dependências da 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. A CameraX precisa de alguns métodos que fazem parte do Java 8, então temos que definir nossas opções de compilação de acordo essa informação. No final do bloco android, logo após buildTypes, adicione o seguinte:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. Este codelab usa ViewBinding, então ative-a incluindo o seguinte no final do bloco android{}:
buildFeatures {
   viewBinding true
}

Quando solicitado, clique em Sync Now para podermos usar a CameraX no nosso app.

Criar o layout do codelab

Na interface deste codelab, usamos o seguinte:

  • Uma PreviewView da CameraX para visualizar imagem/vídeo da câmera.
  • Um botão padrão para controlar a captura de imagem.
  • Um botão padrão para iniciar/parar a captura de vídeo.
  • Uma diretriz vertical para posicionar os dois botões.

Vamos substituir o layout padrão para:

  1. Abrir o arquivo de layout activity_main em res/layout/activity_main.xml e fazer a substituição dele pelo código a seguir.
<?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. Atualizar o arquivo res/values/strings.xml com o seguinte.
<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>

Configurar o arquivo MainActivity.kt

  1. Substitua o código no MainActivity.kt pelo mostrado abaixo, mas deixe o nome do pacote intacto. Ele inclui instruções de importação, variáveis que serão instanciadas, funções que serão implementadas e constantes.

O onCreate() já foi implementado para verificar permissões de câmera, iniciar a câmera, definir o onClickListener() para os botões de foto e captura e implementar cameraExecutor. Mesmo que o onCreate() seja implementado para você, a câmera ainda não vai funcionar até que os métodos sejam implementados no arquivo.

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. Solicitar as permissões necessárias

Para abrir a câmera, o app precisa de permissão do usuário. Também é necessário permitir o acesso ao microfone para gravar áudio. No Android 9 (P) e versões anteriores, a MediaStore precisa da permissão de gravação no armazenamento externo. Nesta etapa, vamos implementar essas permissões.

  1. Abra AndroidManifest.xml e adicione estas linhas antes da tag 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" />

A adição de android.hardware.camera.any garante que o dispositivo tem uma câmera. Especificar .any significa que essa pode ser uma câmera frontal ou traseira.

  1. Copie este código no MainActivity.kt.. Os itens abaixo detalham o código que acabamos de copiar.
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()
       }
   }
}
  • Verifica se o código da solicitação está correto. Se não estiver, ela será ignorada.
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
  • Se as permissões tiverem sido concedidas, chama startCamera().
if (allPermissionsGranted()) {
   startCamera()
}
  • Se não tiverem, o usuário é avisado.
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. Execute o app.

Agora, ele vai solicitar permissão para usar a câmera e o microfone:

dcdf8aa3d87e74be.png

4. Implementar o caso de uso da Preview

Em um app de câmera, o visor é usado para permitir que o usuário visualize a foto que será tirada. Vamos implementar um visor usando a classe Preview da CameraX.

Para usar a Preview, primeiro precisamos definir uma configuração, que será usada para criar uma instância do caso de uso. A instância resultante é a que vinculamos ao ciclo de vida da CameraX.

  1. Copie esse código para a função startCamera().

Os itens abaixo detalham o código que acabamos de copiar.

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))
}
  • Crie uma instância da classe ProcessCameraProvider. Ela é usada para vincular o ciclo de vida das câmeras ao do proprietário. Isso elimina a tarefa de abrir e fechar a câmera, já que a CameraX tem reconhecimento de ciclo de vida.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • Adicione um listener à cameraProviderFuture. Adicione um Runnable como argumento. Vamos fazer esse preenchimento depois. Adicione ContextCompat.getMainExecutor() como segundo argumento. Isso retorna um Executor, que é usado na linha de execução principal.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • No Runnable, adicione um ProcessCameraProvider. Isso é usado para vincular o ciclo de vida da câmera ao LifecycleOwner no processo do app.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • Inicialize nosso objeto Preview, chame o build nele, use um provedor de superfície no visor e defina-o na visualização.
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
   }
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • Crie um bloco try. Dentro dele, confira se não há nada vinculado à cameraProvider e vincule a cameraSelector e o objeto de visualização à cameraProvider.
try {
   cameraProvider.unbindAll()
   cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
}
  • Existem algumas maneiras de esse código falhar, como se o app não estiver mais em foco. Una esse código em um bloco catch para criar um registro em caso de falha.
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. Execute o app. Agora temos uma visualização da câmera.

d61a4250f6a3ed35.png

5. Implementar o caso de uso da ImageCapture

Outros casos de uso funcionam de maneira muito semelhante à Preview. Primeiro, definimos um objeto de configuração usado para instanciar o objeto do caso de uso real. Para capturar fotos, implemente o método takePhoto(), que é chamado quando o botão Take photo é pressionado.

  1. Copie esse código para o método takePhoto().

Os itens abaixo detalham o código que acabamos de copiar.

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)
           }
       }
   )
}
  • Primeiro, encontre uma referência ao caso de uso da ImageCapture. Se o caso de uso for nulo, saia da função. O valor será nulo se tocarmos no botão de foto antes da configuração da captura de imagem. Sem a instrução return, o app falharia se o valor fosse null.
val imageCapture = imageCapture ?: return
  • Em seguida, crie um valor de conteúdo da MediaStore para armazenar a imagem. Use um carimbo de data/hora para que o nome de exibição na MediaStore seja exclusivo.
   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")
       }
   }
  • Crie um objeto OutputFileOptions. É nele que podemos especificar como queremos que a saída seja. Queremos que ela seja salva na MediaStore para que possa ser mostrada por outros apps. Então, adicione nossa entrada da MediaStore.
val outputOptions = ImageCapture.OutputFileOptions
       .Builder(contentResolver,
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues)
       .build()
  • Chame takePicture() no objeto imageCapture. Transmita outputOptions, o executor e um callback para quando a imagem for salva. Em seguida, preencha o callback.
imageCapture.takePicture(
   outputOptions, ContextCompat.getMainExecutor(this),
   object : ImageCapture.OnImageSavedCallback {}
)
  • Se a captura de imagem falhar ou não for salva, adicione um caso de erro para registrar o problema.
override fun onError(exc: ImageCaptureException) {
   Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
  • Se a captura não falhar, isso significa que a foto foi tirada. Salve a foto no arquivo criado anteriormente, mande um aviso para informar ao usuário que a operação foi concluída e mostre um log statement.
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. Acesse o método startCamera() e copie este código abaixo do para visualização.
imageCapture = ImageCapture.Builder().build()
  1. Por fim, atualize a chamada para bindToLifecycle() no bloco try para incluir o novo caso de uso:
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)

O método ficará assim:

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. Execute o app novamente e pressione Take photo. Vai aparecer um aviso na tela e uma mensagem nos registros.

54292eaa4ce3be0a.png

Visualizar a foto

Agora que as fotos recém-capturadas foram salvas, podemos usar qualquer app da MediaStore para visualizá-las. Por exemplo, com o app Google Fotos, faça o seguinte:

  1. Inicie o Google Fotos Fotos.
  2. Toque em "Biblioteca" (desnecessário se você não tiver feito login no app com sua conta) para verificar os arquivos de mídia ordenados. A pasta "CameraX-Image" é a nossa.

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. Toque no ícone de imagem para ver a foto completa. Depois, toque no botão Mais Mais no canto superior direito para visualizar os detalhes da foto capturada.

55e1a442ab5f25e7.png 70a8b27a76523f56.png

Se você está procurando apenas um app de câmera básico para tirar fotos, já terminamos. Simples assim! Se quiser implementar um analisador de imagens, continue lendo.

6. Implementar o caso de uso da ImageAnalysis

Uma ótima maneira de deixar o app de câmera mais interessante é usando o recurso ImageAnalysis. Ele permite definir uma classe personalizada que implementa a interface ImageAnalysis.Analyzer e que será chamada com os frames da câmera recebidos. Não precisamos gerenciar o estado da sessão da câmera nem descartar imagens. A vinculação ao ciclo de vida desejado do app é suficiente, como acontece com outros componentes com reconhecimento do ciclo de vida.

  1. Adicione esse analisador como uma classe interna no MainActivity.kt. O analisador registra a luminosidade média da imagem. Para criar um analisador, substituímos a função analyze em uma classe que implementa a interface ImageAnalysis.Analyzer.
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()
   }
}

Com nossa classe implementando a interface ImageAnalysis.Analyzer, basta instanciar LuminosityAnalyzer na ImageAnalysis, semelhante a outros casos de uso, e atualizar a função startCamera() novamente, antes da chamada para CameraX.bindToLifecycle():

  1. No método startCamera(), adicione este código abaixo de imageCapture.
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. Atualize a chamada de bindToLifecycle() na cameraProvider para incluir a imageAnalyzer.
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)

O método completo terá esta aparência:

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. Execute o app. Aproximadamente a cada segundo, ele vai produzir uma mensagem no Logcat semelhante a esta.
D/CameraXApp: Average luminosity: ...

7. Implementar o caso de uso da VideoCapture

A CameraX adicionou o caso de uso da VideoCapture na versão 1.1.0-alpha10 e fez outras melhorias desde então. A API VideoCapture tem suporte a vários recursos de captura de vídeo. Portanto, para manter este codelab gerenciável, ele demonstra apenas a captura de vídeo e áudio para uma MediaStore.

  1. Copie este código para o método captureVideo(). Ele controla o início e a interrupção do caso de uso da VideoCapture. Os itens abaixo detalham o código que acabamos de copiar.
// 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
                   }
               }
           }
       }
}
  • Verifique se o caso de uso da VideoCapture foi criado. Se não tiver sido, não faça nada.
val videoCapture = videoCapture ?: return
  • Desative a interface até que a ação de solicitação seja concluída pela CameraX. Ela é reativada dentro do nosso VideoRecordListener registrado em etapas posteriores.
viewBinding.videoCaptureButton.isEnabled = false
  • Se houver uma gravação ativa em andamento, ela deve ser interrompida e você precisa liberar a recording atual. Você vai receber uma notificação quando o arquivo de vídeo capturado estiver pronto para ser usado pelo nosso app.
val curRecording = recording
if (curRecording != null) {
    curRecording.stop()
    recording = null
    return
}
  • Para iniciar a gravar, criamos uma nova sessão de gravação. Primeiro, criamos nosso objeto de conteúdo de vídeo da MediaStore, com o carimbo de data/hora do sistema como nome de exibição para podermos capturar vários vídeos.
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)
  • Defina o vídeo contentValues criado para a MediaStoreOutputOptions.Builder e crie nossa instância de MediaStoreOutputOptions.
    .setContentValues(contentValues)
    .build()
    videoCapture
    .output
    .prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
  • Ative o áudio nessa gravação.
.apply {
   if (PermissionChecker.checkSelfPermission(this@MainActivity,
           Manifest.permission.RECORD_AUDIO) ==
       PermissionChecker.PERMISSION_GRANTED)
   {
       withAudioEnabled()
   }
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
   //lambda event listener
}
  • Quando a gravação da solicitação for iniciada pela câmera, mude o texto do botão "Start Capture" para "Stop Capture".
is VideoRecordEvent.Start -> {
    viewBinding.videoCaptureButton.apply {
        text = getString(R.string.stop_capture)
        isEnabled = true
    }
}
  • Quando a gravação ativa for concluída, notifique o usuário com um aviso, mude o botão "Stop Capture" para "Start Capture" e reative-o:
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. Em startCamera(), coloque o seguinte código após a linha de criação de preview. Isso vai criar o caso de uso da VideoCapture.
val recorder = Recorder.Builder()
   .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
   .build()
videoCapture = VideoCapture.withOutput(recorder)
  1. (Opcional) Dentro de startCamera(), desative os casos de uso da imageCapture e imageAnalyzer excluindo ou comentando o seguinte código:
/* 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. Vincule casos de uso da Preview + VideoCapture a uma câmera de ciclo de vida. Ainda no startCamera(), substitua a chamada de cameraProvider.bindToLifecycle() pelo seguinte:
   // Bind use cases to camera
   cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)

Neste ponto, o método startCamera() ficará assim:

   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. Crie e execute. A interface das etapas anteriores será mostrada.
  2. Grave alguns clipes:
  • Pressione o botão "START CAPTURE". A legenda vai mudar para "STOP CAPTURE".
  • Grave um vídeo por alguns segundos ou minutos.
  • Pressione o botão "STOP CAPTURE", o mesmo usado para iniciar a captura.

ef2a6005defc4977.png 8acee41fd0f4af0f.png

Visualizar o vídeo (igual à visualização do arquivo de imagem de captura).

Vamos usar o app Google Fotos para ver o vídeo capturado:

  1. Inicie o Google Fotos Fotos.
  2. Toque em "Biblioteca" para acessar os arquivos de mídia ordenados. Toque no ícone da pasta "CameraX-Video" para acessar uma lista dos videoclipes disponíveis.

71f07e32d5f4f268.png 596819ad391fac37.png

  1. Toque no ícone para abrir o videoclipe recém-capturado. Quando a reprodução for concluída, toque no botão Mais Mais no canto superior direito para inspecionar os detalhes do clipe.

7c7125726af9e429.png 44da18b15ad2f607.png

Isso é tudo o que precisamos para gravar um vídeo. No entanto, a VideoCapture da CameraX tem muitos outros recursos, incluindo:

  • Pausar/retomar a gravação
  • Capturar em File ou FileDescriptor
  • Entre outros

Para instruções sobre o uso deles, consulte a documentação oficial.

8. (Opcional) Combinar a VideoCapture com outros casos de uso

A etapa VideoCapture anterior demonstrou a combinação da Preview e VideoCapture, que pode ser usada em todos os dispositivos, conforme documentado na tabela de recursos do dispositivo. Nesta etapa, vamos adicionar os casos de uso da ImageCapture à combinação VideoCapture + Preview para demonstrar Preview + ImageCapture + VideoCapture.

  1. Com o código da etapa anterior, remova a marca de comentário e ative a criação de imageCapture no startCamera():
imageCapture = ImageCapture.Builder().build()
  1. Adicione uma FallbackStrategy à criação da QualitySelector. Isso permite que a CameraX escolha uma resolução com suporte se a Quality.HIGHEST necessária não puder ser usada com o caso de uso da imageCapture.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. Ainda na startCamera(), vincule o caso de uso da imageCapture aos casos da Preview e VideoCapture já existentes. Observação: não vincule a imageAnalyzer, já que não há suporte para uma combinação de preview + imageCapture + videoCapture + imageAnalysis:
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, videoCapture)

A função startCamera() final ficará assim:

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. Crie e execute. A interface das etapas anteriores será mostrada, só que desta vez, os botões Take Photo e Start Capture vão funcionar.
  2. Faça algumas capturas:
  • Toque no botão START CAPTURE para iniciar a captura.
  • Toque em TAKE PHOTO para capturar uma imagem.
  • Aguarde a captura de imagem ser concluída. Vai aparecer um aviso, como antes.
  • Toque no botão STOP CAPTURE para interromper a gravação.

Estamos realizando a captura de imagem enquanto a visualização e a captura de vídeo estão em andamento.

ef2a6005defc4977.png 16bc70ec3346fa66.png

  1. Visualize os arquivos de imagem e vídeo capturados, como fizemos no app Google Fotos nas etapas anteriores. Desta vez, duas fotos e dois videoclipes vão aparecer.

3f3feb19c8c73532.png

  1. (Opcional) Substitua imageCapture pelo caso de uso da ImageAnalyzer nas etapas acima (1 a 4): vamos usar Preview + ImageAnalysis + VideoCapture. Observação: a combinação Preview + Analysis + ImageCapture + VideoCapture pode não ter suporte, mesmo em dispositivos com câmeras LEVEL_3.

9. Parabéns!

Você implementou o seguinte em um novo app Android do zero:

  • Incluiu dependências da CameraX em um novo projeto.
  • Mostrou um visor da câmera com o caso de uso da Preview.
  • Implementou a captura de fotos e o salvamento de imagens no armazenamento usando o caso de uso da ImageCapture.
  • Implementou a análise de frames da câmera em tempo real com o caso de uso da ImageAnalysis.
  • Implementou a captura de vídeo com o caso de uso da VideoCapture.

Para saber mais sobre a CameraX e o que você pode fazer com ela, confira a documentação ou clone o exemplo oficial (link em inglês).