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

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

请求并获得必要的权限后,您的应用便可访问音频眼镜或显示眼镜上的硬件。访问眼镜硬件(而非手机硬件)的关键在于使用投影上下文

获取投影上下文主要有两种方式,具体取决于代码的执行位置:

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

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

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

如果应用中投影 activity 之外的部分(例如手机 activity 或服务)需要访问眼镜的硬件,则必须明确获取投影上下文。为此,请使用 createProjectedDeviceContext 方法:

@OptIn(ExperimentalProjectedApi::class)
private fun getGlassesContext(context: Context): Context? {
    return try {
        // From a phone Activity or Service, get a context for the AI glasses.
        ProjectedContext.createProjectedDeviceContext(context)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "Failed to create projected device context", e)
        null
    }
}

检查有效性

createProjectedDeviceContext 调用封装在 ProjectedContext.isProjectedDeviceConnected 中。虽然此方法返回 true,但投影的上下文对已连接的设备仍然有效,并且手机应用 activity 或服务(例如 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 对象必须定义 16 kHz 的采样率和单声道或立体声的声道配置(使用 CHANNEL_IN_MONOCHANNEL_IN_STEREO)。

  • 虽然对缓冲区大小没有固定要求,但建议获取最小缓冲区大小,以最大限度减少感知到的延迟时间。

使用后清理

当应用不再需要麦克风或 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) {
    // 1. Get the CameraProvider using the projected context.
    // When using the projected context, DEFAULT_BACK_CAMERA maps to the AI glasses' camera.
    val projectedContext = try {
        ProjectedContext.createProjectedDeviceContext(activity)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "AI Glasses context could not be created", e)
        return
    }

    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 类拍摄图像。请参阅 CameraX 的文档,了解如何使用 takePicture 拍摄图片

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

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

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

下表显示了建议的分辨率和帧速率,具体取决于应用的使用情形:

使用场景 分辨率 帧速率
视频通信 1280x720 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 an AI glasses Activity, get a context for the phone.
        ProjectedContext.createHostDeviceContext(activity)
    } catch (e: IllegalStateException) {
        Log.e(TAG, "Failed to create host device context", e)
        null
    }
}

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