Pular para o conteúdo principal

Capítulo 24: CameraX

Resumo

Ao chegar a este capítulo, você já dominou a API Camera2 bruta: abrindo instâncias de CameraDevice manualmente, construindo objetos CaptureRequest.Builder, gerenciando ciclos de vida de CameraCaptureSession, lidando com três tipos diferentes de callbacks e liberando cuidadosamente cada recurso em cada caso extremo. Você ganhou suas cicatrizes. Agora damos um passo atrás e perguntamos: e se 80% desse código clichê (boilerplate) pudesse desaparecer?

O CameraX é a biblioteca Jetpack do Google que envolve o Camera2 em uma API declarativa, baseada em casos de uso e ciente do ciclo de vida. Ele não substitui o Camera2 — ele é o Camera2 sob o capô. O que ele substitui são centenas de linhas de código de configuração de sessão, tratamento de peculiaridades específicas de dispositivos e contabilidade manual de ciclo de vida. Neste capítulo, você aprenderá a arquitetura do CameraX, entenderá o modelo UseCase, verá como injetar parâmetros Camera2 brutos dentro do CameraX via Camera2Interop e sairá com uma tabela de decisão para saber exatamente quando recorrer ao CameraX e quando você deve descer para o Camera2 bruto.

Para acompanhar e inspecionar cada capacidade de câmera em seu próprio dispositivo antes de decidir qual camada visar, instale o Android Camera Parameters da Google Play ou navegue pelo código-fonte em github.com/zoozooll/AndroidCameraParameters.


Arquitetura do CameraX

O CameraX é distribuído como cinco artefatos Jetpack: camera-core, camera-camera2, camera-lifecycle, camera-view e camera-extensions. A espinha dorsal arquitetural é o modelo UseCase — em vez de pensar em superfícies (surfaces) e sessões, você pensa no que deseja que a câmera faça.

O Modelo UseCase

Existem quatro casos de uso canônicos, e você vincula qualquer subconjunto deles simultaneamente a um ciclo de vida:

UseCasePropósito
PreviewTransmite quadros para um PreviewView ou Surface. Análogo à configuração de uma solicitação repetida visando um SurfaceTexture.
ImageAnalysisTransmite quadros ImageProxy para seu analisador em uma thread de segundo plano. Substitui a criação manual de um ImageReader com YUV_420_888 e a ligação de seu listener a uma solicitação repetida.
ImageCaptureCaptura de foto de disparo único ou sequencial. Gerencia a solicitação de captura, a ligação do ImageReader, a rotação e o EXIF para você.
VideoCaptureMesclado ao CameraX a partir da versão 1.1; envolve um pipeline MediaRecorder ou ParcelFileDescriptor com semântica correta de pausa/retomada e roteamento de áudio.

Vincular todos os quatro é perfeitamente legal — o CameraX resolve internamente a combinação de fluxos contra o SCALER_STREAM_CONFIGURATION_MAP e chama isSessionConfigurationSupported em seu nome, voltando para resoluções mais baixas se sua combinação exata não for suportada. Esta é uma das maiores vitórias individuais: você nunca mais passará três horas descobrindo que o Samsung intermediário de 2019 em sua matriz de teste não suporta 4:3 PRIV + 16:9 JPEG_MAX simultaneamente. O CameraX simplesmente funciona.

ProcessCameraProvider e Consciência do Ciclo de Vida

O ponto de vinculação é o ProcessCameraProvider, um singleton de propriedade do processo da sua aplicação. A linha principal é:

val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val camera: Camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture,
imageAnalysis
)
}, ContextCompat.getMainExecutor(context))

É só isso. Sem o inferno de callbacks do openCamera, sem StateCallback, sem callback de configuração de sessão, sem desmontagem. Quando o lifecycleOwner (seu Fragment ou Activity) atinge ON_STOP, o CameraX fecha o CameraDevice. No ON_DESTROY, ele desmonta a sessão e libera cada superfície. Vazamentos de recursos do tipo que você caçou no Capítulo 7 simplesmente não podem acontecer — o contrato do ciclo de vida o impede.

O CameraX Envolve Internamente o Camera2

Internamente, o CameraX é Camera2. O artefato camera-camera2 contém Camera2Camera, Camera2CameraCaptureResult e Camera2RequestProcessor, todos os quais traduzem suas declarações de UseCase de alto nível nas chamadas exatas de CameraManager.openCamera, createCaptureSession e setRepeatingRequest que você escreveu manualmente nos 23 capítulos anteriores. As soluções alternativas específicas de fornecedores são codificadas em arquivos XML por dispositivo dentro da biblioteca — o famoso "banco de dados de peculiaridades (quirks) do CameraX".

A arquitetura completa se parece com isso:

Siga as setas da esquerda para a direita: seu app declara o que deseja (casos de uso), o CameraX resolve como obtê-lo (tamanhos de superfície, configuração de sessão, peculiaridades) e, em seguida, emite as chamadas idênticas do Camera2 que você teria escrito. O valor agregado são as duas caixas do meio — centenas de milhares de linhas de código de compatibilidade de dispositivos criadas pelo Google que você não precisa escrever.


Camera2Interop: Injetando Parâmetros Camera2 no CameraX

O CameraX é brilhante para os 80% dos casos. Mas você, caro leitor, é um mestre em Camera2. Você sabe o que CONTROL_AE_MODE_OFF significa. Você conhece a diferença entre SENSOR_SENSITIVITY e CONTROL_AE_EXPOSURE_COMPENSATION. Quando a especificação do produto diz "permita ao usuário travar o ISO em 400 e a exposição em 1/60s mesmo usando CameraX", você não reescreve todo o recurso em Camera2 bruto. Você recorre ao Camera2Interop.

O Padrão Extender

Cada UseCase.Builder tem um Camera2Interop.Extender correspondente. Chame-o antes do build() para injetar chaves Camera2 brutas no nível da sessão ou no nível de cada solicitação:

MétodoEquivalente Camera2
extender.setCaptureRequestOption(key, value)CaptureRequest.Builder.set(key, value)
extender.setSessionOption(key, value)Parâmetros de inicialização de sessão (menos usados)

O extender é aditivo: o CameraX ainda define seus próprios padrões para cada chave que você não substitui. Se você definir apenas SENSOR_SENSITIVITY, o CameraX ainda gerencia AF, AWB, rotação e metadados.

Exemplo do Mundo Real: ISO e Exposição Manual no CameraX

Aqui está um construtor ImageCapture completo que trava a câmera no AE manual com um ISO fixo de 400 e um tempo de exposição de 1/60 de segundo, e então tira uma foto:

val iso = 400
val exposureTimeNanos = 1_000_000_000L / 60 // 1/60 s

val imageCapture = ImageCapture.Builder()
.setTargetResolution(Size(4032, 3024))
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.apply {
val interop = Camera2Interop.Extender(this)
interop.setCaptureRequestOption(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_OFF
)
interop.setCaptureRequestOption(
CaptureRequest.CONTROL_AE_LOCK,
false
)
interop.setCaptureRequestOption(
CaptureRequest.SENSOR_SENSITIVITY,
iso
)
interop.setCaptureRequestOption(
CaptureRequest.SENSOR_EXPOSURE_TIME,
exposureTimeNanos
)
interop.setCaptureRequestOption(
CaptureRequest.CONTROL_MODE,
CaptureRequest.CONTROL_MODE_OFF
)
}
.build()

cameraProvider.bindToLifecycle(
viewLifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture
)

// Mais tarde, dispare a foto:
imageCapture.takePicture(
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(imageProxy: ImageProxy) {
// imageProxy contém o quadro exposto manualmente
imageProxy.close()
}

override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "Falha na captura: ${exception.imageCaptureError}", exception)
}
}
)

Ressalva crítica: definir CONTROL_MODE_OFF desativa todo o 3A. Se você deseja apenas travar a exposição, mas ainda executar AF e AWB, defina apenas CONTROL_AE_MODE_OFF (ou CONTROL_AE_LOCK = true) e deixe CONTROL_MODE no padrão (CONTROL_MODE_AUTO). O CameraX usa o padrão para cada chave que você não toca.

E sim — você pode fazer a mesma coisa com Preview.Builder e ImageAnalysis.Builder para fluxos manuais repetidos. O extender se aplica a cada solicitação repetida ou única emitida durante a vida útil daquele UseCase.

Lendo Resultados do Camera2 de Volta

Ir na outra direção — extrair um TotalCaptureResult de um callback do CameraX — é igualmente simples via Camera2CameraCaptureResult:

imageCapture.takePicture(
executor,
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(imageProxy: ImageProxy) {
val camera2Result = imageProxy.imageInfo
.cameraCaptureResult as? Camera2CameraCaptureResult
val totalResult: TotalCaptureResult? = camera2Result?.captureResult
val actualIso = totalResult?.get(CaptureResult.SENSOR_SENSITIVITY)
Log.d(TAG, "ISO real no sensor: $actualIso")
imageProxy.close()
}
}
)

Isso permite verificar se os parâmetros injetados realmente chegaram ao sensor. Use o Android Camera Parameters para conferir qual SENSOR_INFO_SENSITIVITY_RANGE seu dispositivo declara — se o seu ISO injetado cair fora dessa faixa, o CameraX o limita silenciosamente (ou o HAL faz), e ler o resultado de volta é a única maneira de saber.


Escolhendo CameraX vs Camera2

A questão arquitetural mais difícil não é "como eu uso o CameraX?", mas "devo usar o CameraX de todo?". Aqui está a estrutura de decisão destilada do trabalho de produção real.

Fluxograma de Decisão

Tabela de Decisão

CenárioCameraXCamera2 Bruto
Preview estilo Instagram + foto com um toque + vídeo
QR code / código de barras / detecção facial ML Kit sem customização
Exposição manual com ISO fixo + obturador (Camera2Interop cobre)⚠️
Máquina de estados 3A personalizada substituindo algoritmos OEM
Fotografia profissional RAW_SENSOR / RAW_PRIVATE / DNG
Zero Shutter Lag (Capítulo 23) com superfícies de entrada de reprocessamento
Acesso a fluxo físico de multicâmera lógica (Capítulo 20)
Alta velocidade 120/240fps com constrained-high-speed-sessions
Extensões de Câmera (Noturno / Bokeh / HDR) via extensões OEM
Câmera de ré automotiva com migração EVS de boot antecipado✅ (NDK)
Compatibilidade entre dispositivos é o requisito #1 não funcional⚠️

O meio-termo (⚠️) é onde o julgamento importa. O controle de exposição manual via Camera2Interop funciona de forma confiável em dispositivos HARDWARE_LEVEL_FULL, mas falha silenciosamente em dispositivos LEGACY porque os HALs LEGACY ignoram completamente o CONTROL_MODE_OFF. Execute o Android Camera Parameters em sua frota de teste, verifique o INFO_SUPPORTED_HARDWARE_LEVEL para cada dispositivo e, se 20% de sua frota for LEGACY, desça para o Camera2 bruto com um caminho de fallback ou aceite que os controles manuais não funcionarão nesses dispositivos.

Configuração Básica de Preview + ImageCapture no CameraX (Completa)

Para referência, aqui está a configuração minimalista e completa que substitui as ~300 linhas do código Camera2 bruto que você escreveu nos Capítulos 6–9.

class CameraXFragment : Fragment() {

private lateinit var viewBinding: FragmentCameraXBinding
private lateinit var imageCapture: ImageCapture

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding = FragmentCameraXBinding.bind(view)

val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
bindCameraUseCases(cameraProvider)
}, ContextCompat.getMainExecutor(requireContext()))
}

private fun bindCameraUseCases(cameraProvider: ProcessCameraProvider) {
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(viewBinding.previewView.surfaceProvider)
}

imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setTargetRotation(viewBinding.previewView.display.rotation)
.build()

val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()

cameraProvider.unbindAll()
try {
cameraProvider.bindToLifecycle(
viewLifecycleOwner,
cameraSelector,
preview,
imageCapture
)
} catch (exc: Exception) {
Log.e(TAG, "Falha na vinculação de UseCase", exc)
}
}

private fun takePhoto() {
val photoFile = File(
outputDirectory,
"PHOTO_${System.currentTimeMillis()}.jpg"
)
val outputOptions = ImageCapture.OutputFileOptions
.Builder(photoFile)
.build()

imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(requireContext()),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
Toast.makeText(requireContext(),
"Salvo: $savedUri", Toast.LENGTH_SHORT).show()
}
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Falha na captura de foto: ${exc.message}", exc)
}
}
)
}

companion object {
private const val TAG = "CameraXFragment"
}
}

Essa é toda a pipeline de visualização + foto. Observe a ausência total de HandlerThread, CameraDevice.StateCallback, CameraCaptureSession.StateCallback, ImageReader.OnImageAvailableListener ou chamadas manuais para close(). O CameraX gerencia cada um deles.


Resumo

O CameraX é o Camera2 com uma fachada declarativa, baseada em casos de uso e ciente do ciclo de vida, apoiada pelo banco de dados de peculiaridades entre dispositivos do Google. A arquitetura empilha seu app → UseCases → CameraX Core → Camera2 → HAL, e a chamada ProcessCameraProvider.bindToLifecycle() substitui centenas de linhas de configuração manual. Para os 20% dos parâmetros que o CameraX não expõe no nível de UseCase, o Camera2Interop.Extender injeta chaves de CaptureRequest brutas e lê os valores de TotalCaptureResult brutos de volta. A decisão de quando usá-lo é simples: o CameraX é o padrão, a menos que seu recurso exija explicitamente RAW, ZSL, fluxos multicâmera físicos, vídeo de alta velocidade ou uma topologia de sessão personalizada que o resolvedor do CameraX não possa expressar.

O Que Vem a Seguir

O CameraX ainda é código Dalvik/ART Java/Kotlin sentado acima do limite do Binder. E se mesmo essa sobrecarga for excessiva para o orçamento de quadros de 16ms do seu mecanismo de AR? No Capítulo 25, cruzamos a linha do JNI inteiramente e abrimos a câmera diretamente do C++ usando a stack de câmera nativa do NDK, vinculando quadros como texturas Vulkan com zero cópias.