Utilizzare un contesto proiettato per accedere all'hardware degli occhiali audio e degli occhiali con display

Dispositivi XR applicabili
Queste indicazioni ti aiutano a creare esperienze per questi tipi di dispositivi XR.
Audio e
occhiali con display

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 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. Mentre questo metodo restituisce true, il contesto proiettato rimane valido per il dispositivo connesso e l'attività o il servizio dell'app per smartphone (ad esempio un CameraManager) può accedere all'hardware degli occhiali AI.

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 (ad esempio un CameraManager) o le risorse create dall'app utilizzando il contesto proiettato.

Re-initialize on reconnect

Quando gli occhiali si riconnettono, la tua 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 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 standard Headset e Advanced Audio Distribution Profile (A2DP), 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:

  1. Dichiara le seguenti autorizzazioni nel file manifest della tua app:

  2. Richiedi le autorizzazioni RECORD_AUDIO e BLUETOOTH_CONNECT in 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 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()

Acquisire un'immagine con la fotocamera degli occhiali

Per acquisire un'immagine con la fotocamera degli occhiali, configura e associa il caso d'uso ImageCapture di CameraX alla fotocamera degli occhiali utilizzando il contesto corretto per la tua 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))
                }
            }
    }
}

Punti chiave sul codice

  • Ottiene un'istanza di ProcessCameraProvider utilizzando il contesto del dispositivo proiettato.
  • Nell'ambito del contesto proiettato, la fotocamera principale degli occhiali rivolta verso l'esterno viene mappata a DEFAULT_BACK_CAMERA quando 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 Camera2CameraInfo per leggere l'oggetto CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP sottostante, che può essere utile per controlli avanzati sulle risoluzioni supportate.
  • Un ResolutionSelector personalizzato è progettato per controllare con precisione la risoluzione dell'immagine di output per ImageCapture.
  • Crea un caso d'uso ImageCapture configurato con un ResolutionSelector personalizzato.
  • Associa il caso d'uso ImageCapture al 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 associare ImageCapture da solo senza uno stream di anteprima, disattiva lo stream ripetuto forzato impostando setRepeatingStreamForced(false) su CameraXConfig.Builder (ad esempio, implementando CameraXConfig.Provider nella classe Application o configurando ProcessCameraProvider prima 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.

Registra un video con la fotocamera degli occhiali

Per acquisire un video anziché un'immagine con la fotocamera degli occhiali, sostituisci i componenti ImageCapture con i componenti VideoCapture corrispondenti e modifica la logica di esecuzione dell'acquisizione.

Le modifiche principali riguardano l'utilizzo di un caso d'uso diverso, la creazione di un file di output diverso e l'avvio dell'acquisizione utilizzando il metodo di registrazione video appropriato. Per saperne di più sull'API VideoCapture e su come utilizzarla, consulta la documentazione sull'acquisizione video di CameraX.

La tabella seguente mostra la risoluzione e la frequenza fotogrammi consigliate in base al caso d'uso della tua app:

Caso d'uso Risoluzione Frequenza fotogrammi
Comunicazione video 1280 x 720 15 FPS
Visione artificiale 640 x 480 10 FPS
Streaming video AI 640 x 480 1 FPS

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 Activity dello smartphone Activity o ProjectedContext.createHostDeviceContext per ottenere il contesto dello smartphone.
  • Non utilizzare getApplicationContext perché il contesto dell'applicazione può restituire in modo errato il contesto degli occhiali se un'attività proiettata è stata il componente avviato più di recente.