Usar un contexto proyectado para acceder al hardware de los lentes de audio y los lentes de visualización

Dispositivos de realidad extendida correspondientes
Esta guía te ayuda a crear experiencias para estos tipos de dispositivos de realidad extendida.
Audio y
gafas de visualización

Después de solicitar y obtener los permisos necesarios, tu app puede acceder al hardware de los lentes con audio o de los lentes con pantalla. La clave para acceder al hardware de las gafas (en lugar del hardware del teléfono) es usar un contexto proyectado.

Existen dos formas principales de obtener un contexto proyectado, según dónde se ejecute el código:

Obtén un contexto proyectado si tu código se ejecuta en una actividad proyectada

Si el código de tu app se ejecuta desde tu actividad proyectada, su propio contexto de actividad ya es un contexto proyectado. En este caso, las llamadas realizadas dentro de esa actividad ya pueden acceder al hardware de las gafas.

Obtén un contexto proyectado para el código que se ejecuta en un componente de la app para teléfonos

Si una parte de tu app fuera de tu actividad proyectada (como una actividad o un servicio del teléfono) necesita acceder al hardware de las gafas, debe obtener explícitamente un contexto proyectado. Para ello, usa el createProjectedDeviceContext método:

@RequiresApi(Build.VERSION_CODES.BAKLAVA)
@OptIn(ExperimentalProjectedApi::class, ExperimentalCoroutinesApi::class)
private fun monitorProjectedConnectivity(activity: ComponentActivity) {
    activity.lifecycleScope.launch {
        // Before creating a projected context, check to see if the projected device is connected.
        // While this method returns true, the projected context remains valid.
        ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext)
            .collectLatest { isConnected ->
                if (isConnected) {
                    // From a phone Activity or Service, get a context for the audio and display glasses.
                    // Re-initialize on reconnect: Obtain another context instance.
                    val projectedContext = try {
                        ProjectedContext.createProjectedDeviceContext(activity)
                    } catch (e: IllegalStateException) {
                        Log.e(TAG, "Failed to create projected context", e)
                        return@collectLatest
                    }

                    // Use the projectedContext to initialize system services (e.g., CameraManager).
                    Log.i(TAG, "Projected device connected. Initializing hardware...")
                } else {
                    // The projected context is destroyed when the device disconnects.
                    // Clean up on disconnect: Listen for 'false' and release resources.
                    Log.i(TAG, "Projected device disconnected. Cleaning up hardware resources...")
                }
            }
    }
}

Verifica la validez

Incluye la llamada createProjectedDeviceContext dentro del ProjectedContext.isProjectedDeviceConnected. Mientras este método muestra true, el contexto proyectado sigue siendo válido para el dispositivo conectado, y la actividad o el servicio de la app para teléfonos (como CameraManager) pueden acceder al hardware de las gafas de IA.

Realiza una limpieza al desconectar

El contexto proyectado está vinculado al ciclo de vida del dispositivo conectado, por lo que se destruye cuando el dispositivo se desconecta. Cuando el dispositivo se desconecta, ProjectedContext.isProjectedDeviceConnected muestra false. Tu app debe detectar este cambio y limpiar cualquier servicio del sistema (como CameraManager) o recursos que haya creado con ese contexto proyectado.

Reinicializa al volver a conectar

Cuando las gafas se vuelven a conectar, tu app puede obtener otra instancia de contexto proyectado con createProjectedDeviceContext y, luego, reinicializar cualquier servicio o recurso del sistema con el nuevo contexto proyectado.

Graba audio con el micrófono de las gafas

Puedes grabar audio desde las gafas con dos métodos distintos:

Elige un método de grabación

El método que elijas depende de si necesitas procesamiento de audio de alta fidelidad y específico de XR, o entrada de audio Bluetooth estándar.

Método de registro Acceso al micrófono Caso de uso común

Contexto proyectado

Varios micrófonos

La grabación con un contexto proyectado permite que tu app acceda a varios micrófonos de las gafas y a sus funciones de hardware especializadas, como las siguientes:

  • Espacialización específica de XR
  • Reducción de ruido avanzada
  • Separación de voz que aísla la voz del usuario
  • Mantener el acceso a la grabación en entornos multidispositivo, incluso cuando las gafas no son el dispositivo Bluetooth activo

HFP de Bluetooth

Micrófono único

Se basa en el perfil de manos libres (HFP) de Bluetooth para una compatibilidad inmediata y lista para usar. En este modo, las gafas se conectan al teléfono con los perfiles estándar de auriculares y de perfil de distribución de audio avanzado (A2DP) profiles, y funcionan como un periférico Bluetooth típico.

Si tu app ya está diseñada para la grabación Bluetooth estándar, puedes usar este método para grabar audio desde las gafas sin integrar ninguna capacidad específica de XR.

Graba audio con un contexto proyectado

Para grabar audio con un contexto proyectado, primero solicita los permisos de tiempo de ejecución necesarios y, luego, graba el audio con la API de AudioRecord, como se describe en las siguientes secciones.

Cómo solicitar permisos de tiempo de ejecución

Para acceder a varios micrófonos en las gafas, debes solicitar permisos de audio específicamente para el dispositivo proyectado. El permiso RECORD_AUDIO estándar y con alcance del teléfono que un usuario otorgó para tu app en su dispositivo móvil no es suficiente.

Sigue estos pasos para solicitar los permisos:

  1. Declara el permiso RECORD_AUDIO en el archivo de manifiesto de la app.
  2. Solicita los permisos con alcance del dispositivo proyectado de una de las siguientes maneras, según dónde se ejecute el código:

Inicializa AudioRecord con un contexto proyectado

Para asegurarte de que el audio se grabe desde las gafas en lugar del teléfono host, debes asociar el objeto AudioRecord con el contexto del dispositivo proyectado.

El siguiente código usa AudioRecord.Builder y pasa el projectedDeviceContext al método setContext:

// Initialize AudioRecord with projected device context
val audioRecord = AudioRecord.Builder()
    .setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    .setAudioFormat(audioFormat)
    .setBufferSizeInBytes(bufferSize)
    // pass in the projected device context
    .setContext(projectedDeviceContext)
    .build()

audioRecord.startRecording()

Puntos clave sobre el código
  • Puedes configurar la fuente de audio en CAMCORDER, VOICE_RECOGNITION, VOICE_COMMUNICATION o UNPROCESSED para adaptar el procesamiento de audio a tu caso de uso específico.

    Por ejemplo, usa VOICE_COMMUNICATION si tu caso de uso necesita reducción de ruido automática o aislar la voz del usuario. VOICE_RECOGNITION se procesa con cancelación de eco acústico (AEC), lo que puede ser útil cuando hay reproducción de audio simultánea durante la grabación. Además, si necesitas audio sin procesar y sin alteraciones, selecciona UNPROCESSED o CAMCORDER.

  • Para garantizar la compatibilidad con las gafas, el objeto audioFormat debe definir una frecuencia de muestreo de 16 kHz y una configuración de canales mono o estéreo (con CHANNEL_IN_MONO o CHANNEL_IN_STEREO).

  • Usa AudioRecord.getMinBufferSize() para determinar el tamaño mínimo del búfer para crear el objeto AudioRecord. Sin embargo, para evitar que se pierda el audio de las gafas, debes leer este búfer en fragmentos cortos y frecuentes (idealmente, segmentos de 20 ms) en lugar de esperar a que se llene todo el búfer.

Realiza una limpieza después de usar

Cuando tu app ya no necesite el micrófono o cuando se detenga la actividad, llama a stop y release en el objeto AudioRecord.

Verifica los permisos de tiempo de ejecución antes de grabar

Antes de llamar a startRecording, verifica que el usuario haya otorgado el permiso de acceso al micrófono para las gafas con el contexto proyectado.

Graba audio con HFP de Bluetooth

Para grabar audio con HFP de Bluetooth, primero solicita los permisos de tiempo de ejecución necesarios y, luego, graba el audio con la AudioManager API, como se describe en las siguientes secciones.

Solicita permisos

Al igual que con cualquier dispositivo de audio Bluetooth estándar, el teléfono controla RECORD_AUDIO, BLUETOOTH_CONNECT y otros permisos relacionados, y no el dispositivo conectado (como los lentes con audio o los lentes con pantalla).

Sigue estos pasos para solicitar los permisos:

  1. Declara los siguientes permisos en el archivo de manifiesto de la app:

  2. Solicita los permisos RECORD_AUDIO y BLUETOOTH_CONNECT en el tiempo de ejecución con el flujo de permisos estándar de Android.

Usa AudioManager para enrutar el audio

Después de que el usuario le otorgue a tu app los permisos de tiempo de ejecución necesarios, usa la AudioManager API para configurar el dispositivo de comunicación en TYPE_BLUETOOTH_SCO y enrutar el audio a través de HFP de Bluetooth. Esto indica al sistema que recupere el audio del periférico Bluetooth.

val audioManager = context.getSystemService(AudioManager::class.java) ?: return
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)
val hfpDevice = devices.find { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }

hfpDevice?.let { device ->
    val audioRecord = AudioRecord.Builder()
        .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
        .setAudioFormat(audioFormat)
        .setBufferSizeInBytes(bufferSize)
        .build()

    // Route recording to the Bluetooth device
    audioRecord.setPreferredDevice(device)
    audioManager.setCommunicationDevice(device)

    audioRecord.startRecording()

Captura una imagen con la cámara de las gafas

Para capturar una imagen con la cámara de las gafas, configura y vincula el caso de uso de CameraX ImageCapture a la cámara de las gafas con el contexto correcto para tu app:

private fun startCameraOnGlasses(activity: ComponentActivity) {
    activity.lifecycleScope.launch {
        // Before creating a projected context, check to see if the projected device is connected.
        ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext)
            .collectLatest { isConnected ->
                if (isConnected) {
                    // 1. Get the CameraProvider using the projected context.
                    // When using the projected context, DEFAULT_BACK_CAMERA maps to the audio and display glasses' camera.
                    val projectedContext = try {
                        ProjectedContext.createProjectedDeviceContext(activity)
                    } catch (e: IllegalStateException) {
                        Log.e(TAG, "Projected context could not be created", e)
                        return@collectLatest
                    }

                    val cameraProviderFuture = ProcessCameraProvider.getInstance(projectedContext)

                    cameraProviderFuture.addListener({
                        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
                        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                        // 2. Check for the presence of a camera.
                        if (!cameraProvider.hasCamera(cameraSelector)) {
                            Log.w(TAG, "The selected camera is not available.")
                            return@addListener
                        }

                        // 3. Query supported streaming resolutions using Camera2 Interop.
                        val cameraInfo = cameraProvider.getCameraInfo(cameraSelector)
                        val camera2CameraInfo = Camera2CameraInfo.from(cameraInfo)
                        val cameraCharacteristics = camera2CameraInfo.getCameraCharacteristic(
                            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
                        )

                        // 4. Define the resolution strategy.
                        val targetResolution = Size(1920, 1080)
                        val resolutionStrategy = ResolutionStrategy(
                            targetResolution,
                            ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
                        )
                        val resolutionSelector = ResolutionSelector.Builder()
                            .setResolutionStrategy(resolutionStrategy)
                            .build()

                        // 5. If you have other continuous use cases bound, such as Preview or ImageAnalysis,
                        // you can use  Camera2 Interop's CaptureRequestOptions to set the FPS
                        val fpsRange = Range(30, 60)
                        val captureRequestOptions = CaptureRequestOptions.Builder()
                            .setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange)
                            .build()

                        // 6. Initialize the ImageCapture use case with options.
                        val imageCapture = ImageCapture.Builder()
                            // Optional: Configure resolution, format, etc.
                            .setResolutionSelector(resolutionSelector)
                            .build()

                        try {
                            // Unbind use cases before rebinding.
                            cameraProvider.unbindAll()

                            // Bind use cases to camera using the Activity as the LifecycleOwner.
                            cameraProvider.bindToLifecycle(
                                activity,
                                cameraSelector,
                                imageCapture
                            )
                        } catch (exc: Exception) {
                            Log.e(TAG, "Use case binding failed", exc)
                        }
                    }, ContextCompat.getMainExecutor(activity))
                }
            }
    }
}

Puntos clave sobre el código

  • Obtiene una instancia de ProcessCameraProvider con el contexto del dispositivo proyectado.
  • Dentro del alcance del contexto proyectado, la cámara principal de las gafas que apunta hacia afuera se asigna a DEFAULT_BACK_CAMERA cuando se selecciona una cámara.
  • Una verificación previa a la vinculación usa cameraProvider.hasCamera(cameraSelector) para verificar que la cámara seleccionada esté disponible en el dispositivo antes de continuar.
  • Usa Camera2 Interop con Camera2CameraInfo para leer el CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP subyacente, que puede ser útil para verificaciones avanzadas de las resoluciones compatibles.
  • Se compila un ResolutionSelector personalizado para controlar con precisión la resolución de la imagen de salida para ImageCapture.
  • Crea un caso de uso ImageCapture que se configura con un ResolutionSelector personalizado.
  • Vincula el caso de uso ImageCapture al ciclo de vida de la actividad. Esto administra automáticamente la apertura y el cierre de la cámara según el estado de la actividad (por ejemplo, detener la cámara cuando la actividad está en pausa).
  • Configuración para hardware de transmisión única: La canalización de la cámara de las gafas está restringida a una sola transmisión activa a la vez. Cuando ImageCapture se vincula solo, CameraX adjunta una transmisión interna repetida (MeteringRepeating) para el enfoque y la medición de forma predeterminada, lo que provoca que la canalización de transmisión única se detenga. Para vincular ImageCapture por sí mismo sin una transmisión de vista previa, inhabilita la transmisión repetida forzada configurando setRepeatingStreamForced(false) en CameraXConfig.Builder (por ejemplo, implementando CameraXConfig.Provider en tu clase Application o configurando ProcessCameraProvider antes de la inicialización).

Después de configurar la cámara de las gafas, puedes capturar una imagen con la clase ImageCapture de CameraX. Consulta la documentación de CameraX para obtener información sobre el uso de takePicture para capturar una imagen.

Captura un video con la cámara de las gafas

Para capturar un video en lugar de una imagen con la cámara de las gafas, reemplaza los ImageCapture componentes con los componentes VideoCapture correspondientes y modifica la lógica de ejecución de captura.

Los cambios principales implican usar un caso de uso diferente, crear un archivo de salida diferente y iniciar la captura con el método de grabación de video adecuado. Para obtener más información sobre la API de VideoCapture y cómo usarla, consulta la documentación de captura de video de CameraX.

En la siguiente tabla, se muestran la resolución y la velocidad de fotogramas recomendadas según el caso de uso de tu app:

Caso de uso Solución Velocidad de fotogramas
Comunicación por video 1280 × 720 15 FPS
Visión artificial 640 × 480 10 FPS
Transmisión de video con IA 640 × 480 1 FPS

Accede al hardware de un teléfono desde una actividad proyectada

Una actividad proyectada también puede acceder al hardware del teléfono (como la cámara o el micrófono) con createHostDeviceContext(context) para obtener el contexto del dispositivo host (teléfono):

@OptIn(ExperimentalProjectedApi::class)
private fun getPhoneContext(activity: ComponentActivity): Context? {
    return try {
        // From a projected Activity, get a context for the phone.
        ProjectedContext.createHostDeviceContext(activity)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "Failed to create host device context", e)
        null
    }
}

Cuando accedas a hardware o recursos específicos del dispositivo host (teléfono) en una app híbrida (una app que contiene experiencias para dispositivos móviles y gafas), debes seleccionar explícitamente el contexto correcto para asegurarte de que tu app pueda acceder al hardware correcto: