Capturar fotos y vídeo
Disparar una foto parece la operación más simple de una cámara y es la que más decisiones esconde: dónde escribir el resultado, cuánta latencia aceptar, cómo se controla la exposición y el enfoque cuando el usuario toca la pantalla, y qué metadatos acompañan al fichero. Esta lección recorre `ImageCapture` y `VideoCapture` de principio a fin, incluyendo el control fino de exposición y enfoque, la grabación con calidades negociadas y la escritura en almacenamiento acotado sin pedir permisos innecesarios.
Entre pulsar el obturador y tener un fichero en la galería hay una cadena de decisiones que la mayoría de las aplicaciones toma por omisión y luego lamenta. La latencia del disparo determina si el usuario captura el momento que quería o el instante posterior. El modo de captura decide si la imagen pasa por el procesamiento de reducción de ruido del dispositivo o sale casi cruda. La rotación declarada decide si la foto se ve derecha en otra aplicación. Y el destino de escritura decide si tu manifiesto necesita permisos que el revisor de la tienda mirará con lupa o si el fichero aparece en la galería sin pedir nada a nadie. Ninguna de esas decisiones es difícil por separado; lo difícil es que se toman en sitios distintos del código y se manifiestan como fallos que llegan meses después, en dispositivos concretos, en forma de fotos giradas, vídeos sin sonido o ficheros que existen pero que la galería del usuario nunca indexa. Esta lección los recorre juntos, porque juntos es como se razonan.
- Configurar
ImageCaptureeligiendo con criterio entre latencia mínima y calidad máxima, y escribir el resultado en el destino adecuado. - Controlar exposición, enfoque y zoom mediante el control de cámara y las secuencias de medición por punto.
- Grabar vídeo con
VideoCaptureyRecorder, gestionando calidades, pausas y el permiso de audio. - Guardar medios respetando el almacenamiento acotado, con los metadatos correctos y sin permisos superfluos.
El disparo: latencia, calidad y destino
ImageCapture ofrece dos modos que representan extremos de un compromiso real. El modo de latencia mínima entrega el cuadro cuanto antes, sacrificando parte del procesamiento posterior. El modo de calidad máxima permite que el dispositivo aplique su cadena completa de reducción de ruido y fusión de exposiciones, a costa de un retardo perceptible. La elección no es estética: en una aplicación de escaneo de documentos o de comercio, la latencia gana; en una aplicación de fotografía, pierde.
La salida se expresa mediante opciones de destino, y ahí aparece la primera decisión de almacenamiento. Puedes volcar a un fichero de tu directorio privado, a un flujo en memoria si vas a subir la imagen sin guardarla, o directamente a una colección del proveedor de medios. Esta última vía es la interesante, porque escribir contenido propio en el espacio compartido no requiere ningún permiso de almacenamiento en las versiones modernas de la plataforma.
val valores = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "captura-${System.currentTimeMillis()}.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/MiApp")
}
val opciones = ImageCapture.OutputFileOptions.Builder(
contexto.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
valores,
).setMetadata(ImageCapture.Metadata().apply { isReversedHorizontal = usandoFrontal })
.build()
captura.takePicture(
opciones,
ContextCompat.getMainExecutor(contexto),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(resultado: ImageCapture.OutputFileResults) {
mostrarConfirmacion(resultado.savedUri)
}
override fun onError(error: ImageCaptureException) {
registrarFallo(error)
}
},
)
El detalle del espejo horizontal en los metadatos es de los que solo se descubren con usuarios reales. La vista previa de la cámara frontal se muestra invertida porque así se comporta un espejo y así lo espera la persona que se está mirando, pero la imagen capturada no debe guardarse invertida salvo que lo declares. Decidir mal aquí produce fotografías con el texto de las camisetas al revés.
Un fichero escrito con rutas manuales en el almacenamiento compartido puede existir en el disco y no aparecer jamás en la galería del usuario, porque el índice del proveedor de medios no se entera de que está ahí. Escribir a través del proveedor con ContentValues resuelve el problema de raíz: el registro y el fichero se crean juntos. Si por alguna razón heredada escribes por ruta, tendrás que notificar al escáner de medios a mano, y ese camino es exactamente el que el almacenamiento acotado vino a cerrar.
Exposición, enfoque y zoom
El control de cámara obtenido al enlazar es el punto de entrada a todo el ajuste fino. La operación más visible es el toque para enfocar, que se implementa creando un punto de medición a partir de las coordenadas del visor y lanzando una acción de enfoque y medición. La fábrica de puntos que expone el visor traduce las coordenadas de pantalla al espacio del sensor, lo cual es imprescindible porque ambos sistemas de coordenadas casi nunca coinciden.
val fabrica = visor.meteringPointFactory
val punto = fabrica.createPoint(x, y)
val accion = FocusMeteringAction.Builder(punto, FocusMeteringAction.FLAG_AF)
.addPoint(punto, FocusMeteringAction.FLAG_AE)
.setAutoCancelDuration(4, TimeUnit.SECONDS)
.build()
val futuro = camara.cameraControl.startFocusAndMetering(accion)
La cancelación automática merece atención. Si no la configuras, el enfoque queda fijado donde el usuario tocó hasta que alguien lo cambie, lo que produce visores permanentemente desenfocados cuando la persona se mueve. Con ella, el sistema vuelve al modo continuo pasado el plazo. El comportamiento correcto para una cámara general es fijar un plazo corto; para una aplicación de macro o de escaneo, mantener el bloqueo puede ser justo lo que se busca.
El resultado de la secuencia de enfoque llega como un futuro que indica si la medición tuvo éxito, y conviene usarlo para dar realimentación visual en lugar de asumir que todo fue bien. Un dispositivo con enfoque fijo devolverá un fallo inmediato, y una interfaz que muestre indefinidamente el retículo de enfoque en ese caso parecerá colgada cuando en realidad está esperando algo que nunca llegará.
La compensación de exposición se expresa en pasos discretos cuyo rango y tamaño declara el propio dispositivo, y hay que consultarlos antes de ofrecer un control al usuario. El zoom tiene dos interfaces: una por relación absoluta y otra por proporción lineal entre cero y uno, esta última pensada para deslizadores porque distribuye el recorrido de forma perceptualmente uniforme.
Enfoque y medición
Punto traducido al espacio del sensor, acción con banderas de enfoque y exposición, y cancelación automática para no dejar el visor bloqueado.
Exposición
Índice entero dentro del rango que publica el dispositivo. Consulta siempre el rango y el paso antes de construir el control visible.
Grabar vídeo sin sorpresas
La grabación se articula alrededor de VideoCapture con un Recorder, que negocia la calidad a partir de un selector con respaldo. Pedir calidad de ultra alta definición sin declarar un plan alternativo produce un fallo en cualquier dispositivo que no la soporte, y esa es la mitad del catálogo.
val grabador = Recorder.Builder()
.setQualitySelector(
QualitySelector.from(Quality.FHD, FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)),
)
.build()
val videoCapture = VideoCapture.withOutput(grabador)
La sesión de grabación se inicia sobre unas opciones de salida y devuelve un objeto de grabación activa con el que se pausa, se reanuda y se detiene. Los eventos llegan por un oyente y conviene tratarlos como una máquina de estados explícita, porque el estado real de la grabación no se puede inferir de las llamadas que tú hiciste: una grabación puede terminar por sí sola al agotarse el espacio o al alcanzar un límite de duración.
grabacion = videoCapture.output
.prepareRecording(contexto, opcionesDeSalida)
.apply { if (tienePermisoDeAudio) withAudioEnabled() }
.start(ContextCompat.getMainExecutor(contexto)) { evento ->
when (evento) {
is VideoRecordEvent.Start -> marcarGrabando()
is VideoRecordEvent.Status -> actualizarDuracion(evento.recordingStats)
is VideoRecordEvent.Finalize -> finalizar(evento.hasError(), evento.outputResults)
}
}
El audio es un permiso aparte y su ausencia no debe impedir grabar. Una aplicación bien construida comprueba la concesión en el momento de preparar la grabación y, si no la tiene, graba en silencio en lugar de fallar. La misma lógica se aplica al espacio disponible: los eventos de estado informan del tamaño acumulado y permiten avisar antes de que el sistema corte la grabación por su cuenta.
stateDiagram-v2 [*] --> Inactivo Inactivo --> Preparando: usuario pulsa grabar Preparando --> Grabando: evento Start Grabando --> Pausado: pause Pausado --> Grabando: resume Grabando --> Finalizando: stop o limite alcanzado Finalizando --> Inactivo: evento Finalize Finalizando --> Error: fallo de escritura
Hay un cambio conceptual escondido en este material que conviene hacer explícito, porque reorganiza toda la manera de pensar el almacenamiento de medios en un sistema operativo moderno. Durante décadas, capturar significó producir un fichero, y el fichero era el objeto real: existía en una ruta, tenía un nombre elegido por el programa y su presencia en el disco constituía la prueba del trabajo hecho. El sistema de ficheros era simultáneamente el almacén y el índice, y esa coincidencia parecía natural porque nadie más iba a mirar. Lo que rompe esa identidad no es la privacidad, aunque sea el argumento visible, sino la multiplicidad de consumidores: en cuanto la galería, el selector del sistema, la copia de seguridad en la nube, el buscador local y una docena de aplicaciones necesitan encontrar y describir el mismo contenido, el nombre de fichero deja de ser suficiente y hace falta un modelo de datos. La entrada del proveedor de medios lo aporta: tipo, dimensiones, duración, fecha de captura, orientación, ubicación si se autoriza, estado de pendiente, marca de papelera y propietario. El fichero pasa a ser un adjunto de ese registro y no al revés. Las consecuencias prácticas son inmediatas y explican comportamientos que de otro modo parecen arbitrarios: por eso escribir por ruta produce contenido invisible; por eso existe un estado de pendiente que evita que otras aplicaciones lean material a medio escribir; por eso borrar contenido ajeno exige confirmación, porque se está tocando un registro con dueño. La regla que conviene interiorizar, y que sobrevive a esta plataforma concreta, es que en cuanto un dato tiene más de un consumidor, la representación autoritativa deja de ser el fichero y pasa a ser el registro. Escribir un fichero es fácil. Publicar un dato es un contrato, y publicar mal deja contenido que técnicamente existe pero que, para todos los efectos del sistema, nunca ocurrió.
- Captura una foto directamente en la colección de imágenes del proveedor de medios sin declarar ningún permiso de almacenamiento.
- Compara el tiempo entre pulsación y fichero guardado en los dos modos de captura y anota la diferencia en un dispositivo real.
- Implementa el toque para enfocar con cancelación automática y verifica que el visor recupera el enfoque continuo.
- Consulta el rango de compensación de exposición del dispositivo y construye un deslizador que respete su paso real.
- Graba un vídeo con audio denegado y comprueba que la grabación se completa en silencio en lugar de fallar.