Использовать проецируемый контекст для доступа к оборудованию на аудио- и видеоочках

Поддерживаемые XR-устройства
Это руководство поможет вам создавать продукты для таких XR-устройств.
Аудио- и
видеоочки

После того как вы запросите и получите необходимые разрешения, ваше приложение сможет получить доступ к аппаратным компонентам аудио- или видеоочков. Чтобы получить доступ к аппаратным компонентам очков, а не телефона, необходимо использовать проецируемый контекст.

Получить контекст проекции можно двумя основными способами в зависимости от того, где выполняется ваш код:

Получение контекста, если код выполняется в спроецированном действии

Если код приложения выполняется в рамках проецируемого действия, то контекст действия уже является проецируемым. В этом случае вызовы, сделанные в рамках этого действия, уже могут получить доступ к оборудованию очков.

Получение прогнозируемого контекста для кода, выполняемого в компоненте приложения "Телефон"

Если части приложения, не связанной с проецируемым действием (например, действию или сервису телефона), требуется доступ к аппаратным компонентам очков, она должна явным образом получить проецируемый контекст. Для этого используйте метод 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...")
                }
            }
    }
}

Проверка действительности

Оберните вызов createProjectedDeviceContext в ProjectedContext.isProjectedDeviceConnected. Хотя этот метод возвращает true, проецируемый контекст остается действительным для подключенного устройства, а действия или сервис приложения на телефоне (например, CameraManager) могут получить доступ к аппаратным средствам ИИ-очков.

Очистка при отключении

Контекст проекции привязан к жизненному циклу подключенного устройства, поэтому он уничтожается при отключении устройства. Когда устройство отключается, ProjectedContext.isProjectedDeviceConnected возвращает false. Приложение должно отслеживать это изменение и очищать все системные службы (например, CameraManager) или ресурсы, созданные с использованием этого контекста.

Повторная инициализация при повторном подключении

Когда очки снова подключатся, приложение сможет получить другой экземпляр проецируемого контекста с помощью createProjectedDeviceContext, а затем повторно инициализировать системные службы или ресурсы, используя новый проецируемый контекст.

Записывать аудио с помощью микрофона очков.

Вы можете записывать аудио с очков, используя профиль Bluetooth Hands-Free (HFP). В этом режиме очки подключаются к телефону с помощью стандартных профилей Headset и Advanced Audio Distribution Profile (A2DP) и работают как обычное периферийное устройство Bluetooth. Если ваше приложение уже поддерживает стандартную запись по Bluetooth, вы можете использовать этот метод для записи аудио с очков без дополнительной разработки.

Записывать аудио с помощью Bluetooth HFP

Чтобы записать аудио с помощью Bluetooth HFP, сначала запросите необходимые динамические разрешения, а затем запишите аудио с помощью AudioManager API, как описано в следующих разделах.

Запросить разрешения

Как и в случае с любым стандартным аудиоустройством Bluetooth, разрешения RECORD_AUDIO, BLUETOOTH_CONNECT и другие связанные разрешения контролируются телефоном, а не подключенным устройством (например, аудио- или видеоочками).

Чтобы запросить разрешения, выполните следующие действия:

  1. Объявите следующие разрешения в файле манифеста приложения:

  2. Запросите разрешения RECORD_AUDIO и BLUETOOTH_CONNECT во время выполнения, используя стандартный процесс получения разрешений Android.

Используйте AudioManager для маршрутизации аудио

После того как пользователь предоставит приложению необходимые динамические разрешения, используйте API AudioManager, чтобы задать для устройства связи значение TYPE_BLUETOOTH_SCO. Это позволит маршрутизировать аудио через Bluetooth HFP. Это указывает системе на необходимость получения аудио с периферийного устройства 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()

Сделайте снимок с помощью камеры очков.

Чтобы сделать снимок с помощью камеры очков, настройте и привяжите вариант использования ImageCapture CameraX к камере очков, используя правильный контекст для вашего приложения:

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

Важные сведения о коде

  • Получает экземпляр ProcessCameraProvider с помощью контекста проецируемого устройства.
  • В контексте проецирования основная камера очков, направленная наружу, сопоставляется с DEFAULT_BACK_CAMERA при выборе камеры.
  • Перед привязкой с помощью cameraProvider.hasCamera(cameraSelector) проверяется, доступна ли выбранная камера на устройстве.
  • Использует Camera2 Interop с Camera2CameraInfo для чтения базового CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP, что может быть полезно для расширенной проверки поддерживаемых разрешений.
  • Специальный ResolutionSelector создан для точного управления разрешением выходного изображения для ImageCapture.
  • Создает вариант использования ImageCapture, настроенный с помощью пользовательского ResolutionSelector.
  • Привязывает вариант использования ImageCapture к жизненному циклу действия. Это позволяет автоматически управлять открытием и закрытием камеры в зависимости от состояния действия (например, останавливать камеру, когда действие приостановлено).
  • Настройка для оборудования с одним потоком. Конвейер камеры очков ограничен одним активным потоком за раз. Если привязать только ImageCapture, CameraX по умолчанию подключит внутренний повторяющийся поток (MeteringRepeating) для фокусировки и замера, что приведет к остановке конвейера с одним потоком. Чтобы привязать ImageCapture без потока предварительного просмотра, отключите принудительный повторяющийся поток, установив для setRepeatingStreamForced(false) значение CameraXConfig.Builder (например, реализовав CameraXConfig.Provider в классе Application или настроив ProcessCameraProvider перед инициализацией).

После настройки камеры очков вы можете сделать снимок с помощью класса ImageCapture библиотеки CameraX. Информацию о том, как использовать takePicture для съемки фотографий, можно найти в документации CameraX.

Запишите видео с помощью камеры очков.

Чтобы снять видео, а не фото, замените компоненты ImageCapture на соответствующие компоненты VideoCapture и измените логику выполнения съемки.

Основные изменения включают использование другого варианта использования, создание другого выходного файла и запуск захвата с использованием соответствующего метода видеозаписи. Дополнительную информацию об API VideoCapture и его использовании можно найти в документации по записи видео с помощью CameraX.

В таблице ниже приведены рекомендуемые значения разрешения и частоты кадров в зависимости от сценария использования приложения.

Пример использования Разрешение Частота кадров
Видеосвязь 1280 x 720 15 кадров/с
Компьютерное зрение 640 x 480; 10 кадров/с
Потоковое ИИ-видео 640 x 480; 1 кадр/с

Доступ к аппаратным компонентам телефона из проецируемого действия

Проецируемое действие также может получить доступ к аппаратным компонентам телефона (например, камере или микрофону) с помощью createHostDeviceContext(context), чтобы получить контекст хост-устройства (телефона):

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

При обращении к оборудованию или ресурсам, которые относятся к хост-устройству (телефону), в гибридном приложении (приложении, которое работает и на мобильном устройстве, и на очках) необходимо явно выбрать правильный контекст, чтобы приложение могло получить доступ к нужному оборудованию.

  • Используйте контекст Activity из телефона Activity или ProjectedContext.createHostDeviceContext, чтобы получить контекст телефона.
  • Не используйте getApplicationContext, поскольку контекст приложения может неправильно возвращать контекст очков, если проецируемое действие было последним запущенным компонентом.