Usar um contexto projetado para acessar hardware em óculos de áudio e óculos de exibição

Dispositivos XR relevantes
Estas orientações ajudam você a criar experiências para esses tipos de dispositivos XR.
Áudio e
óculos com tela virtual

Depois de solicitar e receber as permissões necessárias, seu app poderá acessar o hardware nos óculos com áudio ou com tela. A chave para acessar o hardware dos óculos (em vez do hardware do smartphone) é usar um contexto projetado.

Há duas maneiras principais de conseguir um contexto projetado, dependendo de onde o código está sendo executado:

Receber um contexto projetado se o código estiver sendo executado em uma atividade projetada

Se o código do app estiver sendo executado na atividade projetada, o contexto da atividade já será um contexto projetado. Nesse cenário, as chamadas feitas nessa atividade já podem acessar o hardware dos óculos.

Receber um contexto projetado para o código em execução em um componente de app Telefone

Se uma parte do app fora da atividade projetada (como uma atividade de smartphone ou um serviço) precisar acessar o hardware dos óculos, ela precisará receber explicitamente um contexto projetado. Para fazer isso, use o método createProjectedDeviceContext:

@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...")
                }
            }
    }
}

Verificar a validade

Una a chamada createProjectedDeviceContext ao ProjectedContext.isProjectedDeviceConnected. Enquanto esse método retornar true, o contexto projetado vai permanecer válido para o dispositivo conectado, e a atividade no app Telefone ou o serviço (como um CameraManager) poderá acessar o hardware dos óculos com IA.

Limpar ao desconectar

O contexto projetado está vinculado ao ciclo de vida do dispositivo conectado, então ele é destruído quando o dispositivo se desconecta. Quando o dispositivo é desconectado, ProjectedContext.isProjectedDeviceConnected retorna false. O app precisa detectar essa mudança e limpar todos os serviços do sistema (como um CameraManager) ou recursos que o app criou usando esse contexto projetado.

Reinicializar ao reconectar

Quando os óculos se reconectarem, o app poderá receber outra instância de contexto projetado usando createProjectedDeviceContext e, em seguida, reinicializar todos os serviços ou recursos do sistema usando o novo contexto projetado.

Gravar áudio com o microfone dos óculos

É possível gravar áudio dos óculos usando o perfil de viva-voz (HFP, na sigla em inglês) do Bluetooth. Nesse modo, os óculos se conectam ao smartphone usando os perfis padrão de fone de ouvido e perfil de distribuição de áudio avançado (A2DP), funcionando como um periférico Bluetooth comum. Se o app já foi projetado para gravação Bluetooth padrão, use esse método para gravar áudio dos óculos sem nenhum desenvolvimento adicional.

Gravar áudio usando o HFP do Bluetooth

Para gravar áudio usando o HFP do Bluetooth, primeiro solicite as permissões de execução necessárias e, em seguida, grave o áudio usando a API AudioManager, conforme descrito nas seções a seguir.

Solicitar permissões

Como em qualquer dispositivo de áudio Bluetooth padrão, as permissões RECORD_AUDIO, BLUETOOTH_CONNECT e outras relacionadas são controladas pelo smartphone, e não pelo dispositivo conectado (como óculos com áudio ou óculos com tela).

Siga estas etapas para solicitar as permissões:

  1. Declare as seguintes permissões no arquivo de manifesto do app:

  2. Solicite as permissões RECORD_AUDIO e BLUETOOTH_CONNECT no momento da execução usando o fluxo de permissões padrão do Android.

Usar o AudioManager para rotear áudio

Depois que o usuário conceder ao app as permissões de execução necessárias, use a API AudioManager para definir o dispositivo de comunicação como TYPE_BLUETOOTH_SCO para rotear o áudio pelo Bluetooth HFP. Isso direciona o sistema para recuperar o áudio do 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()

Tirar uma foto com a câmera dos óculos

Para capturar uma imagem com a câmera dos óculos, configure e vincule o ImageCapture caso de uso do CameraX à câmera dos óculos usando o contexto correto para seu 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))
                }
            }
    }
}

Pontos principais sobre o código

  • Acessa uma instância de ProcessCameraProvider usando o contexto do dispositivo projetado.
  • No escopo do contexto projetado, a câmera principal dos óculos, que fica voltada para fora, é mapeada para DEFAULT_BACK_CAMERA ao selecionar uma câmera.
  • Uma verificação de pré-vinculação usa cameraProvider.hasCamera(cameraSelector) para verificar se a câmera selecionada está disponível no dispositivo antes de prosseguir.
  • Usa a interoperabilidade da Camera2 com Camera2CameraInfo para ler o CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP subjacente, o que pode ser útil para verificações avançadas em resoluções compatíveis.
  • Um ResolutionSelector personalizado é criado para controlar com precisão a resolução da imagem de saída para ImageCapture.
  • Cria um caso de uso de ImageCapture configurado com um ResolutionSelector personalizado.
  • Vincula o caso de uso ImageCapture ao ciclo de vida da atividade. Isso gerencia automaticamente a abertura e o fechamento da câmera com base no estado da atividade (por exemplo, interrompendo a câmera quando a atividade é pausada).
  • Configuração para hardware de stream único:o pipeline da câmera dos óculos é restrito a um único stream ativo por vez. Quando ImageCapture é vinculado sozinho, o CameraX anexa um stream de repetição interno (MeteringRepeating) para foco e medição por padrão, o que faz com que o pipeline de stream único seja interrompido. Para vincular ImageCapture por conta própria sem um stream de pré-lançamento, desative o stream de repetição forçada definindo setRepeatingStreamForced(false) em CameraXConfig.Builder (por exemplo, implementando CameraXConfig.Provider na sua classe Application ou configurando ProcessCameraProvider antes da inicialização).

Depois que a câmera dos óculos estiver configurada, você poderá capturar uma imagem com a classe ImageCapture do CameraX. Consulte a documentação do CameraX para saber como usar takePicture para capturar uma imagem.

Gravar um vídeo com a câmera dos óculos

Para capturar um vídeo em vez de uma imagem com a câmera dos óculos, substitua os componentes ImageCapture pelos componentes VideoCapture correspondentes e modifique a lógica de execução da captura.

As principais mudanças envolvem o uso de um caso de uso diferente, a criação de um arquivo de saída diferente e o início da captura usando o método de gravação de vídeo apropriado. Para mais informações sobre a API VideoCapture e como usá-la, consulte a documentação de captura de vídeo do CameraX.

A tabela a seguir mostra a resolução e o frame rate recomendados de acordo com o caso de uso do app:

Caso de uso Resolução Frame rate
Comunicação por vídeo 1280 x 720 15 QPS
Visão computacional 640 x 480 10 QPS
Streaming de vídeo com IA 640 x 480 1 QPS

Acessar o hardware de um smartphone em uma atividade projetada

Uma atividade projetada também pode acessar o hardware do smartphone (como a câmera ou o microfone) usando createHostDeviceContext(context) para acessar o contexto do dispositivo host (smartphone):

@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
    }
}

Ao acessar hardware ou recursos específicos do dispositivo host (smartphone) em um app híbrido (um app que contém experiências para dispositivos móveis e óculos), selecione explicitamente o contexto correto para garantir que o app possa acessar o hardware correto: