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

Поддерживаемые 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, а затем повторно инициализировать любые системные сервисы или ресурсы, используя новый проецируемый контекст.

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

Записывать аудио с помощью очков можно двумя способами:

Как выбрать способ записи

Выбор метода зависит от того, нужна ли вам высококачественная обработка звука, предназначенная для XR, или стандартный аудиовход Bluetooth.

Способ записи Доступ к микрофону Распространенный вариант использования

Прогнозируемый контекст

Несколько микрофонов

Запись с использованием проецируемого контекста позволяет приложению получать доступ к нескольким микрофонам очков и их специализированным аппаратным функциям, таким как:

  • Преобразование в 3D для XR.
  • Расширенное шумоподавление.
  • Функция выделения голоса, которая изолирует голос пользователя.
  • Сохранять доступ к записи в среде с несколькими устройствами, даже если очки не являются активным устройством Bluetooth.

Bluetooth HFP

Один микрофон

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

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

Запись аудио с использованием спроецированного контекста

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

Как запросить динамические разрешения

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

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

  1. Объявите разрешение RECORD_AUDIO в файле манифеста приложения.
  2. Запросите разрешения, действующие в рамках проецируемого устройства, одним из следующих способов в зависимости от того, где выполняется ваш код:

Инициализация AudioRecord с помощью спроецированного контекста

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

В следующем коде используется AudioRecord.Builder и передается projectedDeviceContext методу setContext:

// Initialize AudioRecord with projected device context
val audioRecord = AudioRecord.Builder()
    .setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    .setAudioFormat(audioFormat)
    .setBufferSizeInBytes(bufferSize)
    // pass in the projected device context
    .setContext(projectedDeviceContext)
    .build()

audioRecord.startRecording()

Ключевые моменты
  • Вы можете выбрать источник звука: CAMCORDER, VOICE_RECOGNITION, VOICE_COMMUNICATION или UNPROCESSED. Это позволит настроить обработку звука в соответствии с вашими потребностями.

    Например, используйте VOICE_COMMUNICATION, если вам нужно автоматическое шумопонижение или выделение голоса пользователя. VOICE_RECOGNITION обрабатывается с помощью функции эхоподавления, которая может быть полезна при одновременном воспроизведении аудио и записи. Если вам нужен необработанный звук, выберите UNPROCESSED или CAMCORDER.

  • Чтобы обеспечить совместимость с очками, в объекте audioFormat должна быть задана частота дискретизации 16 кГц и конфигурация каналов "моно" или "стерео" (с помощью CHANNEL_IN_MONO или CHANNEL_IN_STEREO).

  • Используйте AudioRecord.getMinBufferSize(), чтобы определить минимальный размер буфера для создания объекта AudioRecord. Однако, чтобы избежать пропусков звука, считывайте данные из буфера небольшими фрагментами (идеально – по 20 мс) как можно чаще, а не ждите, пока буфер заполнится полностью.

Уборка после использования

Когда микрофон больше не нужен приложению или когда действие остановлено, вызовите stop и release для объекта AudioRecord.

Проверьте наличие динамических разрешений перед записью

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

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

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

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

Как и в случае с любым стандартным аудиоустройством 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 к жизненному циклу объекта Activity. Это позволяет автоматически управлять открытием и закрытием камеры в зависимости от состояния активности (например, останавливать камеру, когда активность приостановлена).
  • Настройка для оборудования с одним потоком. Конвейер камеры очков ограничен одним активным потоком за раз. Если ImageCapture используется отдельно, CameraX по умолчанию подключает внутренний повторяющийся поток (MeteringRepeating) для фокусировки и замера, из-за чего однопоточный конвейер останавливается. Чтобы привязать ImageCapture без предварительного просмотра, отключите принудительное повторение потока, задав setRepeatingStreamForced(false) на CameraXConfig.Builder (например, реализовав CameraXConfig.Provider в классе Application или настроив ProcessCameraProvider перед инициализацией).

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

Как снять видео с помощью камеры очков

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

Основные изменения связаны с использованием другого варианта использования, созданием другого выходного файла и запуском захвата с помощью подходящего метода записи видео. Дополнительную информацию о VideoCapture API и о том, как его использовать, можно найти в документации по видеосъемке с помощью 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
    }
}

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

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