Depois de solicitar e receber as permissões necessárias, seu app poderá acessar o hardware nos óculos com áudio ou com tela. Para acessar o hardware dos óculos (em vez do hardware do smartphone), use 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 ou o serviço do app para smartphone (como um CameraManager) poderá acessar o hardware
dos óculos.
Limpar ao desconectar
O contexto projetado está vinculado ao ciclo de vida do dispositivo conectado, portanto, é
destruído quando o dispositivo se desconecta. Quando o dispositivo se desconecta,
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:
Declare as seguintes permissões no arquivo de manifesto do app:
Solicite as permissões
RECORD_AUDIOeBLUETOOTH_CONNECTno 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 hfpDevice = audioManager.availableCommunicationDevices .firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } ?: return // HFP input is limited to 16 kHz, with mono or stereo channel configurations. val sampleRate = 16_000 val channelMask = AudioFormat.CHANNEL_IN_MONO val encoding = AudioFormat.ENCODING_PCM_16BIT // Use a small multiple of the minimum buffer size to keep chunks short for low latency. val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, encoding) if (minBufferSize <= 0) return val audioRecord = AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) .setAudioFormat( AudioFormat.Builder() .setSampleRate(sampleRate) .setChannelMask(channelMask) .setEncoding(encoding) .build() ) .setBufferSizeInBytes(minBufferSize * 2) .build() try { // Route voice communication audio through the Bluetooth device. if (!audioManager.setCommunicationDevice(hfpDevice)) return audioRecord.startRecording() // Read audio with audioRecord.read() on a background thread. } finally { // Stop and release when done. if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) audioRecord.stop() audioRecord.release() audioManager.clearCommunicationDevice() }
Tirar uma foto com a câmera dos óculos
Para capturar uma imagem com a câmera dos óculos, configure e vincule o caso de uso
ImageCapture do CameraX à câmera dos óculos usando o contexto
correto para seu app:
private fun setUpGlassesCamera(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 dynamically using Camera2 Interop. val cameraInfo = cameraProvider.getCameraInfo(cameraSelector) val map = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP ) val supportedResolutions = map?.getOutputSizes(ImageFormat.JPEG)?.toList() ?: emptyList() Log.i(TAG, "Supported resolutions for JPEG: ${supportedResolutions.joinToString()}") // 4. Dynamically select the best target resolution from the queried list. val preferredResolution = Size(1920, 1080) val dynamicTargetResolution = when { // Case A: The preferred resolution is explicitly supported by the glasses. supportedResolutions.contains(preferredResolution) -> preferredResolution // Case B: Find the highest supported 16:9 resolution as a fallback. else -> { val targetAspectRatio = 16.0 / 9.0 val tolerance = 0.01 supportedResolutions .filter { abs((it.width.toDouble() / it.height) - targetAspectRatio) < tolerance } .maxByOrNull { it.width * it.height } // Case C: Absolute fallback if no 16:9 resolutions are found. ?: supportedResolutions.firstOrNull() ?: preferredResolution } } Log.i(TAG, "Selected target resolution: $dynamicTargetResolution") val resolutionStrategy = ResolutionStrategy( dynamicTargetResolution, ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER ) val resolutionSelector = ResolutionSelector.Builder() .setResolutionStrategy(resolutionStrategy) .build() // 5. 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
ProcessCameraProviderusando o contexto do dispositivo projetado. - No escopo do contexto projetado, a câmera principal dos óculos, que aponta para fora,
é mapeada para
DEFAULT_BACK_CAMERAao 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 do Camera2 com
Camera2CameraInfopara ler oCameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAPsubjacente, o que pode ser útil para verificações avançadas em resoluções compatíveis. - Um
ResolutionSelectorpersonalizado é criado para controlar com precisão a resolução da imagem de saída paraImageCapture. - Cria um caso de uso de
ImageCaptureconfigurado com umResolutionSelectorpersonalizado. - Vincula o caso de uso
ImageCaptureao 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 é
limitado 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 vincularImageCapturepor conta própria sem um stream de pré-lançamento, desative o stream de repetição forçada definindosetRepeatingStreamForced(false)emCameraXConfig.Builder(por exemplo, implementandoCameraXConfig.Providerna sua classeApplicationou configurandoProcessCameraProviderantes da inicialização).
Depois que a câmera dos óculos estiver configurada, você poderá capturar uma imagem com a classe
ImageCapture da CameraX. Consulte a documentação do CameraX para saber como usar
takePicture para capturar uma imagem.
Capturar um stream com a câmera dos óculos
Para capturar streams em vez de uma imagem, substitua o componente ImageCapture
do exemplo de captura de imagem pelos componentes VideoCapture ou
ImageAnalysis e mude a lógica de captura.
Para mais informações sobre essas APIs e como usá-las, consulte a documentação sobre captura de vídeo da CameraX e análise de imagens da CameraX.
Escolha uma resolução e um frame rate
Consulte o dispositivo para encontrar as resoluções compatíveis e as taxas de frames máximas correspondentes. A tabela a seguir lista exemplos de valores que podem aparecer, dependendo do caso de uso do app. Para economizar bateria e reduzir o calor nos óculos, escolha a menor resolução e frame rate que funcionem para seu caso de uso.
| Caso de uso | Resolução | Frame rate |
|---|---|---|
| Reconhecimento de texto (OCR) ou detecção de mundo | 1920 x 1080 | 1 QPS |
| Streaming de vídeo de IA (assistentes multimodais) | 1.024 x 768 | 1 QPS |
| Localização e posicionamento visual (VPS) | 640 x 360 ou 640 x 480 | 10 QPS |
| Videochamadas | 640 x 360 ou 640 x 480 | 24 QPS |
Consultar o frame rate máximo para cada resolução
CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES informa os limites gerais de frame rate do dispositivo, que podem ser maiores do que a taxa de streaming que uma determinada resolução aceita. Para conferir o máximo real de cada resolução em óculos de áudio e
tela, leia a duração mínima do frame e converta-a em QPS com
StreamConfigurationMap.getOutputMinFrameDuration(format, size):
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class) fun getMaxFpsBySize(cameraInfo: CameraInfo, format: Int): Map<Size, Int> { val map = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP ) ?: return emptyMap() return map.getOutputSizes(format).associateWith { size -> val minFrameDurationNs = map.getOutputMinFrameDuration(format, size) if (minFrameDurationNs > 0) (1_000_000_000L / minFrameDurationNs).toInt() else 0 } }
Em seguida, use esse valor para calcular os limites minFps e maxFps do
intervalo de frame rate desejado. Limite o limite inferior (minFps) na frame rate máxima de streaming da resolução calculada com base em getOutputMinFrameDuration e defina um limite superior mais alto (maxFps) como uma janela de recuperação:
val maxFpsBySize = getMaxFpsBySize(cameraInfo, ImageFormat.YUV_420_888) val maxSupportedFps = maxFpsBySize[Size(640, 480)] ?: return ImageAnalysis.Builder().build() // minFps is the target streaming frame rate and should be at most maxSupportedFps. val minFps = minOf(desiredFps, maxSupportedFps) // Set the upper bound to 2x minFps so the stream can catch up if frames are delayed. val maxFps = minFps * 2
Pontos principais sobre o código
- Use o formato que corresponde ao seu caso de uso. Para
ImageAnalysis, isso éYUV_420_888. - Para
VideoCapture, use as sobrecargas deClass:getOutputSizes(MediaRecorder::class.java)egetOutputMinFrameDuration(MediaRecorder::class.java, size). - Especifique um intervalo em que
maxFpsseja maior queminFps(comominFps*2) em vez de definir os dois limites com o mesmo valor, comoRange(minFps, minFps). O limite inferior (minFps) define a frame rate de streaming desejada e precisa ser, no máximo, o valor calculado degetOutputMinFrameDuration, enquanto o limite superior (maxFps) atua como uma janela de compensação para frames atrasados para evitar latência persistente. Por exemplo, [1,2] ou [1,3] para um stream de 1 QPS e [24,30] ou [24,48] para um stream de 24 QPS.
Configurar o frame rate
Para configurar o intervalo de frame rate com base no seu caso de uso do CameraX:
// Configure Resolution Strategy for the stream val resolutionStrategy = ResolutionStrategy( size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER ) val resolutionSelector = ResolutionSelector.Builder() .setResolutionStrategy(resolutionStrategy) .build() // Create and return the ImageAnalysis builder with proper frame rate ranges // ImageAnalysis: OCR, AI streaming, and VPS val analysisBuilder = ImageAnalysis.Builder() .setResolutionSelector(resolutionSelector) Camera2Interop.Extender(analysisBuilder) .setCaptureRequestOption( CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(minFps, maxFps) ) return analysisBuilder.build()
Este exemplo mostra a configuração para VideoCapture e ImageAnalysis,
mas é possível implementar qualquer caso de uso compatível com o app.
Entender como o stream se comporta
Lembre-se dos seguintes comportamentos do sistema ao trabalhar com streams de captura:
- Resoluções sem suporte são rejeitadas: se você solicitar um tamanho que não esteja na lista, a Camera2 vai rejeitar a configuração antes que o stream seja aberto.
- Solicitação de frame rates acima do máximo: se você solicitar um frame rate mínimo acima do máximo aceito para sua resolução, a sessão ainda será aberta, mas o frame rate desejado dos óculos será limitado ao máximo aceito, enquanto o limite superior continuará atuando como o limite de taxa de recuperação. Por exemplo, solicitar [30,60] a 640 x 480 limita o frame rate alvo a 24 QPS (comportando-se como [24,60]).
- Limitação térmica: quando os óculos esquentam, o frame rate cai, mas a resolução permanece a mesma. Crie seu app para lidar com frame rates mais baixos ou irregulares, por exemplo, verificando os carimbos de data/hora dos frames em vez de presumir uma taxa fixa.
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), é necessário selecionar explicitamente o contexto correto para garantir que o app possa acessar o hardware correto:
- Use o contexto
ActivitydoActivitydo smartphone ou oProjectedContext.createHostDeviceContextpara acessar o contexto do smartphone. - Não use
getApplicationContextporque o contexto do aplicativo pode retornar incorretamente o contexto dos óculos se uma atividade projetada foi o componente iniciado mais recentemente.