在您请求并获得必要的权限后,您的应用 可以访问无屏眼镜或带屏眼镜上的硬件。访问眼镜硬件(而不是手机硬件)的关键是使用 投影上下文。
根据代码的执行位置,获取投影上下文主要有两种方式:
如果代码在投影 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 获取另一个投影
上下文实例,然后
使用新的投影上下文重新初始化任何系统服务或资源。
使用眼镜的麦克风录制音频
您可以使用两种不同的方法从眼镜录制音频:
- 使用投影上下文。
- 使用蓝牙免触摸配置文件 (HFP)。
选择录制方法
您选择的方法取决于您是否需要高保真、XR 专用音频处理或标准蓝牙音频输入。
| 录制方法 | 麦克风使用权限 | 常见用例 |
|---|---|---|
投影上下文 |
多个麦克风 |
使用投影上下文进行录制可让您的应用访问眼镜的多个麦克风及其专用硬件功能,例如:
|
蓝牙 HFP |
单个麦克风 |
依赖于蓝牙免触摸配置文件 (HFP),可实现即时开箱即用兼容性。在此模式下,眼镜使用标准耳机和高级音频分发配置文件 (A2DP) 配置文件连接到手机,就像典型的蓝牙外围设备一样。 如果您的应用已针对标准蓝牙录制而设计,则可以使用此方法从眼镜录制音频,而无需集成任何 XR 专用功能。 |
使用投影上下文录制音频
如需使用投影上下文录制音频,请先请求所需的运行时
权限,然后使用 AudioRecord API 录制音频,如
以下部分所述。
请求运行时权限
如需访问眼镜上的多个麦克风,您必须专门为投影设备请求音频权限。用户在其移动设备上为您的应用授予的标准、手机范围的 RECORD_AUDIO 权限是不够的。
请按照以下步骤请求权限:
- 在应用的清单文件中声明
RECORD_AUDIO权限。 根据代码的执行位置,通过以下方式之一请求投影设备范围的权限:
- 从投影 activity 执行的代码:将
ActivityResultLauncher与ProjectedPermissionsResultContract搭配使用。如需详细了解如何使用此方法,请参阅请求硬件权限指南中的注册权限启动器部分及后续部分。 - 从宿主手机 activity 执行的代码:使用
Activity#requestPermissions(permissions, requestCode, deviceId)并提供从projectedDeviceContext获取的设备 ID, 如请求硬件权限指南的了解权限请求用户流 部分中所述。
- 从投影 activity 执行的代码:将
使用投影上下文初始化 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通过回声消除 (AEC) 进行处理,这在录制时同时播放音频时非常有用。如果您需要原始、未经修改的音频,请选择UNPROCESSED或CAMCORDER。为确保与眼镜兼容,
audioFormat对象必须 定义 16kHz 的采样率和单声道或 立体声的通道配置(使用CHANNEL_IN_MONO或CHANNEL_IN_STEREO)。使用
AudioRecord.getMinBufferSize()确定创建AudioRecord对象的最小缓冲区 大小。但是,为防止眼镜出现音频丢帧,您应以较短的频率从该缓冲区读取数据块(最好是 20 毫秒的切片),而不是等待整个缓冲区填满。
使用后清理
当您的应用不再需要麦克风或 activity 停止时,
请对 AudioRecord 对象调用 stop 和 release。
在录制前检查运行时权限
在调用 startRecording 之前,请验证用户是否已使用投影上下文为眼镜授予
麦克风权限。
使用蓝牙 HFP 录制音频
如需使用蓝牙 HFP 录制音频,请先请求所需的运行时
权限,然后使用 AudioManager API 录制音频,如
以下部分所述。
请求权限
与任何标准蓝牙音频设备一样,RECORD_AUDIO、
BLUETOOTH_CONNECT 和其他相关权限由
手机控制,而不是由已连接的设备(例如无屏眼镜或带屏眼镜)控制。
请按照以下步骤请求权限:
在应用的清单文件中声明以下权限:
在运行时使用 标准 Android 权限流程 请求
RECORD_AUDIO和BLUETOOTH_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)) } } } }
代码要点
- 使用
投影设备上下文 获取
ProcessCameraProvider的实例。 - 在投影上下文的范围内,选择摄像头时,眼镜的主摄像头(指向外部)会映射到
DEFAULT_BACK_CAMERA。 - 预绑定检查使用
cameraProvider.hasCamera(cameraSelector)来 验证所选摄像头在设备上是否可用,然后再 继续。 - 将 Camera2 Interop 与
Camera2CameraInfo搭配使用,以读取底层CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP,这对于对支持的分辨率进行高级检查非常有用。 - 构建自定义
ResolutionSelector以精确控制输出 图片分辨率,用于ImageCapture。 - 创建一个使用自定义
ResolutionSelector配置的ImageCapture用例。 - 将
ImageCapture用例绑定到 activity 的生命周期。这会根据 activity 的状态自动管理摄像头的打开和关闭(例如,在 activity 暂停时停止摄像头)。 - 针对单流硬件进行配置: 眼镜摄像头流水线一次只能有一个活跃流。当单独绑定
ImageCapture时,CameraX 默认会附加内部重复流 (MeteringRepeating) 以进行对焦和测光,这会导致单流流水线停滞。如需单独绑定ImageCapture而不使用 预览流,请通过在setRepeatingStreamForced(false)上设置强制重复流来停用强制重复流(例如,通过在CameraXConfig.Provider类中实现Application或在初始化之前配置ProcessCameraProvider)。CameraXConfig.Builder
设置眼镜的摄像头后,您可以使用 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 } }
在混合应用(同时包含移动设备和眼镜体验的应用)中访问特定于宿主设备(手机)的硬件或资源时,您必须明确选择正确的上下文,以确保应用可以访问正确的硬件:
- 使用手机
Activity中的Activity上下文或ProjectedContext.createHostDeviceContext获取手机的 上下文。 - 请勿使用
getApplicationContext,因为如果投影 activity 是 最近启动的组件,应用上下文 可能会错误地返回眼镜的上下文。