Über einen projizierten Kontext auf die Hardware von Audio- und Displaybrillen zugreifen

XR‑Geräte, für die der Leitfaden gilt
Dieser Leitfaden hilft Ihnen dabei, Erlebnisse für die folgenden Arten von XR-Geräten zu entwickeln.
Audio- und
Display-Brille

Nachdem du die erforderlichen Berechtigungen angefordert und erhalten hast, kann deine App auf die Hardware der Audio- oder Display-Brille zugreifen. Der Schlüssel für den Zugriff auf die Hardware der Brille (anstelle der Hardware des Smartphones) ist die Verwendung eines projizierten Kontexts.

Es gibt zwei Möglichkeiten, einen projizierten Kontext abzurufen, je nachdem, wo Ihr Code ausgeführt wird:

Projizierten Kontext abrufen, wenn Ihr Code in einer projizierten Aktivität ausgeführt wird

Wenn der Code Ihrer App innerhalb Ihrer projizierten Aktivität ausgeführt wird, ist der eigene Aktivitätskontext bereits ein projizierter Kontext. In diesem Szenario können Aufrufe, die innerhalb dieser Aktivität erfolgen, bereits auf die Hardware der Brille zugreifen.

Projizierten Kontext für Code abrufen, der in einer Telefon-App-Komponente ausgeführt wird

Wenn ein Teil Ihrer App außerhalb Ihrer projizierten Aktivität (z. B. eine Smartphone-Aktivität oder ein Dienst) auf die Hardware der Brille zugreifen muss, muss er explizit einen projizierten Kontext abrufen. Verwenden Sie dazu die Methode 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...")
                }
            }
    }
}

Gültigkeit prüfen

Schließen Sie den Aufruf createProjectedDeviceContext in ProjectedContext.isProjectedDeviceConnected ein. Während diese Methode true zurückgibt, bleibt der projizierte Kontext für das verbundene Gerät gültig und die Aktivität oder der Dienst Ihrer Smartphone-App (z. B. ein CameraManager) kann auf die Hardware der KI-Brille zugreifen.

Beim Trennen der Verbindung bereinigen

Der projizierte Kontext ist an den Lebenszyklus des verbundenen Geräts gebunden und wird daher zerstört, wenn die Verbindung zum Gerät getrennt wird. Wenn die Verbindung zum Gerät getrennt wird, gibt ProjectedContext.isProjectedDeviceConnected den Wert false zurück. Ihre App sollte auf diese Änderung warten und alle Systemdienste (z. B. CameraManager) oder Ressourcen bereinigen, die Ihre App mit diesem projizierten Kontext erstellt hat.

Bei Wiederverbindung neu initialisieren

Wenn die Datenbrille wieder verbunden ist, kann Ihre App mit createProjectedDeviceContext eine weitere projizierte Kontextinstanz abrufen und dann alle Systemdienste oder Ressourcen mit dem neuen projizierten Kontext neu initialisieren.

Audio mit dem Mikrofon der Brille aufnehmen

Sie können Audio von der Brille über das Bluetooth-Hands-Free Profile (HFP) aufnehmen. In diesem Modus wird die Brille über die Standardprofile „Headset“ und „Advanced Audio Distribution Profile“ (A2DP) mit dem Smartphone verbunden und funktioniert wie ein typisches Bluetooth-Peripheriegerät. Wenn deine App bereits für die Standard-Bluetooth-Aufnahme konzipiert ist, kannst du diese Methode verwenden, um Audio von der Brille aufzunehmen, ohne dass zusätzliche Entwicklungsarbeit erforderlich ist.

Audio über Bluetooth-HFP aufnehmen

Wenn Sie Audio mit Bluetooth HFP aufnehmen möchten, fordern Sie zuerst die erforderlichen Laufzeitberechtigungen an und nehmen Sie dann das Audio mit der AudioManager API auf, wie in den folgenden Abschnitten beschrieben.

Berechtigungen anfordern

Wie bei jedem Standard-Bluetooth-Audiogerät werden die Berechtigungen RECORD_AUDIO, BLUETOOTH_CONNECT und andere zugehörige Berechtigungen vom Smartphone und nicht vom verbundenen Gerät (z. B. Audio- oder Display-Brille) gesteuert.

So fordern Sie die Berechtigungen an:

  1. Deklarieren Sie die folgenden Berechtigungen in der Manifestdatei Ihrer App:

  2. Fordern Sie die Berechtigungen RECORD_AUDIO und BLUETOOTH_CONNECT zur Laufzeit über den Standard-Android-Berechtigungsablauf an.

AudioManager zum Weiterleiten von Audio verwenden

Nachdem der Nutzer Ihrer App die erforderlichen Laufzeitberechtigungen erteilt hat, verwenden Sie die AudioManager API, um das Kommunikationsgerät auf TYPE_BLUETOOTH_SCO festzulegen, damit das Audio über Bluetooth HFP geleitet wird. Dadurch wird das System angewiesen, Audio von der Bluetooth-Peripherie abzurufen.

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()

Bild mit der Kamera der Datenbrille aufnehmen

Wenn du ein Bild mit der Kamera der Datenbrille aufnehmen möchtest, richte den ImageCapture-Anwendungsfall von CameraX ein und binde ihn mit dem richtigen Kontext für deine App an die Kamera der Datenbrille:

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

Wichtige Punkte zum Code

  • Ruft eine Instanz von ProcessCameraProvider mit dem Kontext des Geräts mit der Projektionsfläche ab.
  • Im Rahmen des projizierten Kontexts wird die primäre, nach außen gerichtete Kamera der Brille bei der Auswahl einer Kamera der DEFAULT_BACK_CAMERA zugeordnet.
  • Bei einer Vorabprüfung wird mit cameraProvider.hasCamera(cameraSelector) geprüft, ob die ausgewählte Kamera auf dem Gerät verfügbar ist, bevor fortgefahren wird.
  • Verwendet Camera2 Interop mit Camera2CameraInfo, um die zugrunde liegende CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP zu lesen. Das kann für erweiterte Prüfungen der unterstützten Auflösungen nützlich sein.
  • Ein benutzerdefinierter ResolutionSelector wird erstellt, um die Auflösung des Ausgabebilds für ImageCapture genau zu steuern.
  • Erstellt einen ImageCapture-Anwendungsfall, der mit einem benutzerdefinierten ResolutionSelector konfiguriert ist.
  • Bindet den Anwendungsfall ImageCapture an den Lebenszyklus der Aktivität. Dadurch wird das Öffnen und Schließen der Kamera automatisch basierend auf dem Status der Aktivität verwaltet, z. B. wird die Kamera gestoppt, wenn die Aktivität pausiert wird.
  • Konfiguration für Single-Stream-Hardware:Die Kamerapipeline der Brille ist auf einen einzelnen aktiven Stream beschränkt. Wenn ImageCapture allein gebunden ist, hängt CameraX standardmäßig einen internen sich wiederholenden Stream (MeteringRepeating) für Fokus und Messung an, was dazu führt, dass die Single-Stream-Pipeline ins Stocken gerät. Wenn Sie ImageCapture ohne Vorschaustream binden möchten, deaktivieren Sie den erzwungenen Wiederholungsstream, indem Sie setRepeatingStreamForced(false) für CameraXConfig.Builder festlegen. Implementieren Sie dazu beispielsweise CameraXConfig.Provider in Ihrer Application-Klasse oder konfigurieren Sie ProcessCameraProvider vor der Initialisierung.

Nachdem die Kamera der Brille eingerichtet ist, kannst du mit der ImageCapture-Klasse von CameraX ein Bild aufnehmen. Informationen zur Verwendung von takePicture zum Aufnehmen eines Bildes finden Sie in der CameraX-Dokumentation.

Video mit der Kamera der Datenbrille aufnehmen

Wenn Sie mit der Kamera der Brille ein Video statt eines Bildes aufnehmen möchten, ersetzen Sie die ImageCapture-Komponenten durch die entsprechenden VideoCapture-Komponenten und ändern Sie die Logik für die Aufnahmeausführung.

Die wichtigsten Änderungen betreffen die Verwendung eines anderen Anwendungsfalls, die Erstellung einer anderen Ausgabedatei und die Initiierung der Aufnahme mit der entsprechenden Videoaufzeichnungsmethode. Weitere Informationen zur VideoCapture API und ihrer Verwendung finden Sie in der Dokumentation zur Videoaufnahme mit CameraX.

In der folgenden Tabelle finden Sie die empfohlene Auflösung und Framerate je nach Anwendungsfall Ihrer App:

Anwendungsfall Auflösung Framerate
Videokommunikation 1280 × 720 15 fps
Computer Vision 640 x 480 10 fps
KI-Videostreaming 640 x 480 1 fps

Über eine projizierte Aktivität auf die Hardware eines Smartphones zugreifen

Eine projizierte Aktivität kann auch auf die Hardware des Smartphones (z. B. Kamera oder Mikrofon) zugreifen. Dazu wird createHostDeviceContext(context) verwendet, um den Kontext des Hostgeräts (Smartphone) abzurufen:

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

Wenn du in einer Hybrid-App (einer App, die sowohl für Smartphones als auch für Datenbrillen geeignet ist) auf Hardware oder Ressourcen zugreifst, die spezifisch für das Hostgerät (Smartphone) sind, musst du explizit den richtigen Kontext auswählen, damit deine App auf die richtige Hardware zugreifen kann:

  • Verwenden Sie den Kontext Activity vom Smartphone Activity oder ProjectedContext.createHostDeviceContext, um den Kontext des Smartphones abzurufen.
  • Verwende nicht getApplicationContext, da der Anwendungskontext fälschlicherweise den Kontext der Brille zurückgeben kann, wenn eine projizierte Aktivität die zuletzt gestartete Komponente war.