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
- No Android Studio, crie um projeto e selecione Empty Activity quando solicitado.
- 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.
Adicionar as dependências do Gradle
- Abra o arquivo
build.gradle
para o móduloCameraXApp.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}"
}
- 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ósbuildTypes
, adicione o seguinte:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- 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:
- Abrir o arquivo de layout
activity_main
emres/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>
- 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
- 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.
- Abra
AndroidManifest.xml
e adicione estas linhas antes da tagapplication
.
<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.
- 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()
}
- Execute o app.
Agora, ele vai solicitar permissão para usar a câmera e o microfone:
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.
- 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 umRunnable
como argumento. Vamos fazer esse preenchimento depois. AdicioneContextCompat
.getMainExecutor()
como segundo argumento. Isso retorna umExecutor
, que é usado na linha de execução principal.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
- No
Runnable
, adicione umProcessCameraProvider
. Isso é usado para vincular o ciclo de vida da câmera aoLifecycleOwner
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)
}
- Crie um objeto
CameraSelector
e selecioneDEFAULT_BACK_CAMERA
.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
- Crie um bloco
try
. Dentro dele, confira se não há nada vinculado àcameraProvider
e vincule acameraSelector
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)
}
- Execute o app. Agora temos uma visualização da câmera.
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.
- 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çãoreturn
, o app falharia se o valor fossenull
.
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 objetoimageCapture
. TransmitaoutputOptions
, 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)
}
- Acesse o método
startCamera()
e copie este código abaixo do para visualização.
imageCapture = ImageCapture.Builder().build()
- Por fim, atualize a chamada para
bindToLifecycle()
no blocotry
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))
}
- Execute o app novamente e pressione Take photo. Vai aparecer um aviso na tela e uma mensagem nos registros.
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:
- Inicie o Google Fotos
.
- 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.
- Toque no ícone de imagem para ver a foto completa. Depois, toque no botão Mais
no canto superior direito para visualizar os detalhes da foto capturada.
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.
- 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çãoanalyze
em uma classe que implementa a interfaceImageAnalysis.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()
:
- No método
startCamera()
, adicione este código abaixo deimageCapture
.
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
- Atualize a chamada de
bindToLifecycle()
nacameraProvider
para incluir aimageAnalyzer
.
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))
}
- 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
.
- Copie este código para o método
captureVideo()
. Ele controla o início e a interrupção do caso de uso daVideoCapture
. 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")
}
}
- Crie uma classe
MediaStoreOutputOptions.Builder
com a opção de conteúdo externo.
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
- Defina o vídeo
contentValues
criado para aMediaStoreOutputOptions.Builder
e crie nossa instância deMediaStoreOutputOptions
.
.setContentValues(contentValues)
.build()
- Configure a opção de saída para a classe
Recorder
deVideoCapture<Recorder>
e ative a gravação de áudio:
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()
}
}
- Inicie a nova gravação e registre um listener
VideoRecordEvent
de lambda.
.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
}
}
- Em
startCamera()
, coloque o seguinte código após a linha de criação depreview
. Isso vai criar o caso de uso daVideoCapture
.
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
- (Opcional) Dentro de
startCamera()
, desative os casos de uso daimageCapture
eimageAnalyzer
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")
})
}
*/
- Vincule casos de uso da
Preview
+VideoCapture
a uma câmera de ciclo de vida. Ainda nostartCamera()
, substitua a chamada decameraProvider.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))
}
- Crie e execute. A interface das etapas anteriores será mostrada.
- 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.
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:
- Inicie o Google Fotos
.
- 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.
- Toque no ícone para abrir o videoclipe recém-capturado. Quando a reprodução for concluída, toque no botão Mais
no canto superior direito para inspecionar os detalhes do clipe.
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
ouFileDescriptor
- 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
.
- Com o código da etapa anterior, remova a marca de comentário e ative a criação de
imageCapture
nostartCamera()
:
imageCapture = ImageCapture.Builder().build()
- Adicione uma
FallbackStrategy
à criação daQualitySelector
. Isso permite que a CameraX escolha uma resolução com suporte se aQuality.HIGHEST
necessária não puder ser usada com o caso de uso daimageCapture
.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- Ainda na
startCamera()
, vincule o caso de uso daimageCapture
aos casos da Preview e VideoCapture já existentes. Observação: não vincule aimageAnalyzer
, já que não há suporte para uma combinação depreview + 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))
}
- 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.
- 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.
- 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.
- (Opcional) Substitua
imageCapture
pelo caso de uso daImageAnalyzer
nas etapas acima (1 a 4): vamos usarPreview
+ImageAnalysis
+VideoCapture
. Observação: a combinaçãoPreview
+Analysis
+ImageCapture
+VideoCapture
pode não ter suporte, mesmo em dispositivos com câmerasLEVEL_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).