Dopo aver richiesto e ottenuto le autorizzazioni necessarie, la tua app può accedere all'hardware degli occhiali audio o degli occhiali con display. La chiave per accedere all'hardware degli occhiali (anziché a quello dello smartphone) è utilizzare un contesto proiettato.
Esistono due modi principali per ottenere un contesto proiettato, a seconda di dove viene eseguito il codice:
Ottieni un contesto proiettato se il tuo codice è in esecuzione in un'attività proiettata
Se il codice della tua app viene eseguito dall'attività proiettata, il contesto dell'attività è già un contesto proiettato. In questo scenario, le chiamate effettuate all'interno di questa attività possono già accedere all'hardware degli occhiali.
Ottieni un contesto proiettato per il codice in esecuzione in un componente dell'app Telefono
Se una parte della tua app al di fuori dell'attività proiettata (ad esempio un'attività
telefonica o un servizio) deve accedere all'hardware degli occhiali, deve ottenere esplicitamente
un contesto proiettato. Per farlo, utilizza il metodo 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...") } } } }
Verifica la validità
Aggrega la chiamata createProjectedDeviceContext all'interno di
ProjectedContext.isProjectedDeviceConnected. Anche se questo metodo restituisce
true, il contesto proiettato rimane valido per il dispositivo connesso e l'attività dell'app o il servizio dell'app Telefono (ad esempio un CameraManager) può accedere all'hardware degli occhiali.
Pulizia alla disconnessione
Il contesto proiettato è legato al ciclo di vita del dispositivo connesso, quindi viene
eliminato quando il dispositivo si disconnette. Quando il dispositivo si disconnette,
ProjectedContext.isProjectedDeviceConnected restituisce false. L'app
deve rilevare questa modifica e ripulire tutti i servizi di sistema (come un
CameraManager) o le risorse create dall'app utilizzando il contesto proiettato.
Re-initialize on reconnect
Quando gli occhiali si riconnettono, l'app può ottenere un'altra istanza del contesto proiettato utilizzando createProjectedDeviceContext, quindi reinizializzare eventuali servizi o risorse di sistema utilizzando il nuovo contesto proiettato.
Registrare l'audio con il microfono degli occhiali
Puoi registrare l'audio dagli occhiali utilizzando il profilo vivavoce (HFP) Bluetooth. In questa modalità, gli occhiali si connettono allo smartphone utilizzando i profili Headset e Advanced Audio Distribution Profile (A2DP) standard, funzionando come una tipica periferica Bluetooth. Se la tua app è già progettata per la registrazione Bluetooth standard, puoi utilizzare questo metodo per registrare l'audio dagli occhiali senza ulteriori sviluppi.
Registra audio utilizzando il profilo Bluetooth HFP
Per registrare l'audio utilizzando il profilo Bluetooth HFP, richiedi prima le autorizzazioni di runtime necessarie, quindi registra l'audio utilizzando l'API AudioManager, come descritto nelle sezioni seguenti.
Richiedi autorizzazioni
Come per qualsiasi dispositivo audio Bluetooth standard, le autorizzazioni RECORD_AUDIO,
BLUETOOTH_CONNECT e altre autorizzazioni correlate sono controllate dallo
smartphone e non dal dispositivo connesso (ad esempio occhiali audio o occhiali con display).
Segui questi passaggi per richiedere le autorizzazioni:
Dichiara le seguenti autorizzazioni nel file manifest della tua app:
Richiedi le autorizzazioni
RECORD_AUDIOeBLUETOOTH_CONNECTin fase di runtime utilizzando il flusso di autorizzazioni Android standard.
Utilizza AudioManager per instradare l'audio
Dopo che l'utente ha concesso alla tua app le autorizzazioni di runtime necessarie, utilizza l'API AudioManager per impostare il dispositivo di comunicazione su TYPE_BLUETOOTH_SCO per instradare l'audio tramite Bluetooth HFP. In questo modo,
il sistema recupera l'audio dalla periferica 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() }
Acquisire un'immagine con la fotocamera degli occhiali
Per acquisire un'immagine con la fotocamera degli occhiali, configura e associa il ImageCapture caso d'uso di CameraX alla fotocamera degli occhiali utilizzando il contesto corretto per la tua 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)) } } } }
Punti chiave sul codice
- Ottiene un'istanza di
ProcessCameraProviderutilizzando il contesto del dispositivo proiettato. - Nell'ambito del contesto proiettato, la fotocamera principale degli occhiali, rivolta verso l'esterno, viene mappata a
DEFAULT_BACK_CAMERAquando si seleziona una fotocamera. - Un controllo di pre-associazione utilizza
cameraProvider.hasCamera(cameraSelector)per verificare che la fotocamera selezionata sia disponibile sul dispositivo prima di procedere. - Utilizza Camera2 Interop con
Camera2CameraInfoper leggere l'oggettoCameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAPsottostante, che può essere utile per controlli avanzati sulle risoluzioni supportate. - Viene creato un
ResolutionSelectorpersonalizzato per controllare con precisione la risoluzione dell'immagine di output perImageCapture. - Crea un caso d'uso
ImageCaptureconfigurato con unResolutionSelectorpersonalizzato. - Associa il caso d'uso
ImageCaptureal ciclo di vita dell'attività. In questo modo, l'apertura e la chiusura della fotocamera vengono gestite automaticamente in base allo stato dell'attività (ad esempio, la fotocamera viene interrotta quando l'attività è in pausa). - Configurazione per l'hardware a stream singolo: la pipeline della fotocamera degli occhiali è limitata a un singolo stream attivo alla volta. Quando
ImageCaptureè associato da solo, CameraX collega per impostazione predefinita uno stream ripetuto interno (MeteringRepeating) per la messa a fuoco e la misurazione, il che causa l'arresto della pipeline a stream singolo. Per associareImageCaptureda solo senza uno stream di anteprima, disattiva lo stream ripetuto forzato impostandosetRepeatingStreamForced(false)suCameraXConfig.Builder(ad esempio, implementandoCameraXConfig.Providernella classeApplicationo configurandoProcessCameraProviderprima dell'inizializzazione).
Dopo aver configurato la fotocamera degli occhiali, puoi acquisire un'immagine con la classe ImageCapture di CameraX. Consulta la documentazione di CameraX per scoprire come utilizzare takePicture per acquisire un'immagine.
Acquisisci uno stream con la fotocamera degli occhiali
Per acquisire stream anziché un'immagine, sostituisci il componente ImageCapture
dell'esempio di acquisizione di immagini con i componenti VideoCapture o
ImageAnalysis e modifica la logica di acquisizione.
Per saperne di più su queste API e su come utilizzarle, consulta la documentazione relativa all'acquisizione video di CameraX e all'analisi delle immagini di CameraX.
Scegli una risoluzione e una frequenza fotogrammi
Devi eseguire una query sul dispositivo per trovare le risoluzioni supportate e le frequenze fotogrammi massime corrispondenti. La tabella seguente elenca alcuni esempi di valori che potresti visualizzare, a seconda del caso d'uso della tua app. Per risparmiare batteria e ridurre il calore degli occhiali, scegli la risoluzione e la frequenza fotogrammi più basse adatte al tuo caso d'uso.
| Caso d'uso | Risoluzione | Frequenza fotogrammi |
|---|---|---|
| Riconoscimento del testo (OCR) o rilevamento del mondo | 1920 x 1080 | 1 FPS |
| Streaming video AI (assistenti multimodali) | 1024 x 768 | 1 FPS |
| Localizzazione e sistema di posizionamento visivo (VPS) | 640 x 360 o 640 x 480 | 10 FPS |
| Videochiamate | 640 x 360 o 640 x 480 | 24 FPS |
Esegui una query sulla frequenza fotogrammi massima per ogni risoluzione
CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES segnala i limiti complessivi della frequenza fotogrammi del dispositivo, che possono essere superiori alla frequenza di streaming supportata da una determinata risoluzione. Per ottenere il valore massimo effettivo per ogni risoluzione sugli occhiali con audio e display, leggi la durata minima del frame e convertila in FPS con 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 } }
Quindi, utilizza questo valore per calcolare i limiti minFps e maxFps per l'intervallo della frequenza fotogrammi target. Limita il limite inferiore (minFps) alla frequenza fotogrammi massima di streaming della risoluzione calcolata da getOutputMinFrameDuration e imposta un limite superiore più elevato (maxFps) come finestra di recupero:
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
Punti chiave sul codice
- Utilizza il formato corrispondente al tuo caso d'uso. Per
ImageAnalysis, sonoYUV_420_888. - Per
VideoCapture, utilizza invece gli overloadClass:getOutputSizes(MediaRecorder::class.java)egetOutputMinFrameDuration(MediaRecorder::class.java, size). - Specifica un intervallo in cui
maxFpsè maggiore diminFps(ad esempiominFps*2) anziché impostare entrambi i limiti sullo stesso valore, ad esempioRange(minFps, minFps). Il limite inferiore (minFps) imposta la frequenza fotogrammi di streaming target e deve essere al massimo il valore calcolato dagetOutputMinFrameDuration, mentre il limite superiore (maxFps) funge da finestra di recupero per i fotogrammi ritardati per evitare una latenza persistente. Ad esempio, [1,2] o [1,3] per uno stream a 1 FPS e [24,30] o [24,48] per uno stream a 24 FPS.
Configura la frequenza fotogrammi
Per configurare l'intervallo di frequenza fotogrammi in base al caso d'uso di 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()
Questo esempio mostra la configurazione sia per VideoCapture che per ImageAnalysis,
ma puoi implementare i casi d'uso supportati dalla tua app.
Comprendere il comportamento dello stream
Tieni presente i seguenti comportamenti del sistema quando utilizzi gli stream di acquisizione:
- Le risoluzioni non supportate vengono rifiutate: se richiedi una dimensione che non è presente nell'elenco, Camera2 rifiuta la configurazione prima dell'apertura dello stream.
- Richiesta di frequenze fotogrammi superiori al massimo: se richiedi una frequenza fotogrammi minima superiore al massimo supportato per la tua risoluzione, la sessione si apre comunque, ma la frequenza fotogrammi target degli occhiali viene limitata al massimo supportato, mentre il limite superiore continua a fungere da limite di frequenza di recupero. Ad esempio, la richiesta di [30,60] a 640 x 480 fissa la frequenza fotogrammi target a 24 f/s (comportandosi come [24,60]).
- Limitazione termica: quando gli occhiali si surriscaldano, la frequenza fotogrammi diminuisce, ma la risoluzione rimane invariata. Crea la tua app in modo che gestisca frequenze fotogrammi inferiori o irregolari, ad esempio controllando i timestamp dei fotogrammi anziché presupponendo una frequenza fissa.
Accedere all'hardware di uno smartphone da un'attività proiettata
Un'attività proiettata può anche accedere all'hardware dello smartphone (ad esempio la fotocamera o il microfono) utilizzando createHostDeviceContext(context) per ottenere il contesto del 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 } }
Quando accedi ad hardware o risorse specifici del dispositivo host (smartphone) in un'app ibrida (un'app contenente esperienze sia per dispositivi mobili che per occhiali), devi selezionare esplicitamente il contesto corretto per assicurarti che l'app possa accedere all'hardware corretto:
- Utilizza il contesto
Activitydello smartphoneActivityoProjectedContext.createHostDeviceContextper ottenere il contesto dello smartphone. - Non utilizzare
getApplicationContextperché il contesto dell'applicazione può restituire in modo errato il contesto degli occhiali se un'attività proiettata è stata il componente avviato più di recente.