使用投影上下文访问音频眼镜和显示眼镜上的硬件

适用的 XR 设备
本指南可帮助您为以下类型的 XR 设备打造优质体验。
音频和
显示眼镜

在您请求并获得必要的权限后,您的应用 可以访问无屏眼镜或带屏眼镜上的硬件。访问眼镜硬件(而不是手机硬件)的关键是使用 投影上下文

根据代码的执行位置,获取投影上下文主要有两种方式:

如果代码在投影 activity 中运行,则获取投影上下文

如果应用的代码在您的 投影 activity 中运行,则其自身的 activity 上下文已是 投影上下文。在这种情况下,在该 activity 中进行的调用已可以访问眼镜的硬件。

为在手机应用组件中运行的代码获取投影上下文

如果投影 activity 之外的应用部分(例如手机 activity 或服务)需要访问眼镜的硬件,则必须明确获取投影上下文。为此,请使用 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)可以访问 AI 眼镜硬件。

断开连接时清理

投影上下文与已连接的设备的生命周期相关联,因此会在设备断开连接时被销毁。当设备断开连接时, ProjectedContext.isProjectedDeviceConnected 会返回 false。您的应用应监听此更改,并清理应用使用该投影上下文创建的任何系统服务(例如 CameraManager)或资源。

重新连接时重新初始化

当眼镜重新连接时,您的应用可以使用 createProjectedDeviceContext 获取另一个投影 上下文实例,然后 使用新的投影上下文重新初始化任何系统服务或资源。

使用眼镜的麦克风录制音频

您可以使用两种不同的方法从眼镜录制音频:

选择录制方法

您选择的方法取决于您是否需要高保真、XR 专用音频处理或标准蓝牙音频输入。

录制方法 麦克风使用权限 常见用例

投影上下文

多个麦克风

使用投影上下文进行录制可让您的应用访问眼镜的多个麦克风及其专用硬件功能,例如:

  • XR 专用空间化。
  • 高级降噪。
  • 语音分离,可隔离佩戴者的声音。
  • 即使眼镜不是活跃的蓝牙设备,也能在多设备环境中保持录制访问权限。

蓝牙 HFP

单个麦克风

依赖于蓝牙免触摸配置文件 (HFP),可实现即时开箱即用兼容性。在此模式下,眼镜使用标准耳机和高级音频分发配置文件 (A2DP) 配置文件连接到手机,就像典型的蓝牙外围设备一样。

如果您的应用已针对标准蓝牙录制而设计,则可以使用此方法从眼镜录制音频,而无需集成任何 XR 专用功能。

使用投影上下文录制音频

如需使用投影上下文录制音频,请先请求所需的运行时 权限,然后使用 AudioRecord API 录制音频,如 以下部分所述。

请求运行时权限

如需访问眼镜上的多个麦克风,您必须专门为投影设备请求音频权限。用户在其移动设备上为您的应用授予的标准、手机范围的 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()

代码要点
  • 您可以将音频源设置为 CAMCORDERVOICE_RECOGNITIONVOICE_COMMUNICATIONUNPROCESSED,以根据您的特定用例 定制音频处理。

    例如,如果您的用例需要自动降噪或隔离佩戴者的声音,请使用 VOICE_COMMUNICATIONVOICE_RECOGNITION 通过回声消除 (AEC) 进行处理,这在录制时同时播放音频时非常有用。如果您需要原始、未经修改的音频,请选择 UNPROCESSEDCAMCORDER

  • 为确保与眼镜兼容,audioFormat 对象必须 定义 16kHz 的采样率和单声道或 立体声的通道配置(使用 CHANNEL_IN_MONOCHANNEL_IN_STEREO)。

  • 使用 AudioRecord.getMinBufferSize() 确定创建 AudioRecord 对象的最小缓冲区 大小。但是,为防止眼镜出现音频丢帧,您应以较短的频率从该缓冲区读取数据块(最好是 20 毫秒的切片),而不是等待整个缓冲区填满。

使用后清理

当您的应用不再需要麦克风或 activity 停止时, 请对 AudioRecord 对象调用 stoprelease

在录制前检查运行时权限

在调用 startRecording 之前,请验证用户是否已使用投影上下文为眼镜授予 麦克风权限

使用蓝牙 HFP 录制音频

如需使用蓝牙 HFP 录制音频,请先请求所需的运行时 权限,然后使用 AudioManager API 录制音频,如 以下部分所述。

请求权限

与任何标准蓝牙音频设备一样,RECORD_AUDIOBLUETOOTH_CONNECT 和其他相关权限由 手机控制,而不是由已连接的设备(例如无屏眼镜或带屏眼镜)控制。

请按照以下步骤请求权限:

  1. 在应用的清单文件中声明以下权限

  2. 在运行时使用 标准 Android 权限流程 请求 RECORD_AUDIOBLUETOOTH_CONNECT 权限。

使用 AudioManager 路由音频

在用户向您的应用授予必要的运行时权限后,使用 AudioManager API 将通信设备设置为 TYPE_BLUETOOTH_SCO,以通过蓝牙 HFP 路由音频。这会指示系统从蓝牙外围设备检索音频。

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

使用眼镜的摄像头拍摄图片

如需使用眼镜的摄像头拍摄图片,请使用应用的正确 上下文设置 CameraX 的 ImageCapture 用例并将其绑定到眼镜的摄像头:

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

代码要点

设置眼镜的摄像头后,您可以使用 CameraX 的 ImageCapture 类拍摄图片。如需了解 如何使用 takePicture拍摄图片,请参阅 CameraX 的文档。

使用眼镜的摄像头拍摄视频

如需使用眼镜的摄像头拍摄视频而不是图片,请将 ImageCapture组件替换为相应的VideoCapture组件 并修改拍摄执行逻辑。

主要更改包括使用不同的用例、创建不同的输出文件,以及使用适当的视频录制方法启动拍摄。 如需详细了解 VideoCapture API 及其使用方法,请参阅 CameraX 的视频拍摄文档

下表显示了根据应用用例推荐的分辨率和帧速率:

使用场景 分辨率 帧速率
视频通信 1280 x 720 15 FPS
计算机视觉 640 x 480 10 FPS
AI 视频流式传输 640 x 480 1 FPS

从投影 activity 访问手机的硬件

投影 activity 还可以使用 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
    }
}

在混合应用(同时包含移动设备和眼镜体验的应用)中访问特定于宿主设备(手机)的硬件或资源时,您必须明确选择正确的上下文,以确保应用可以访问正确的硬件: