使用投影內容存取智慧音訊眼鏡和智慧螢幕眼鏡的硬體

適用的 XR 裝置
這份指南可協助您為這類 XR 裝置打造體驗。
音訊和螢幕眼鏡

要求並取得必要權限後,應用程式就能存取音訊眼鏡或螢幕眼鏡的硬體。如要存取智慧眼鏡的硬體 (而非手機的硬體),請使用投影環境

視程式碼的執行位置而定,取得預測內容的方式主要有兩種:

如果程式碼在投影活動中執行,則取得投影內容

如果應用程式的程式碼是從投影活動內執行,其活動環境定義本身就是投影環境定義。在這種情況下,該活動內發出的呼叫已可存取眼鏡的硬體。

取得在手機應用程式元件中執行的程式碼預計環境

如果應用程式中預計活動以外的部分 (例如電話活動或服務) 需要存取智慧眼鏡的硬體,就必須明確取得投影環境。如要這麼做,請使用 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,但投影的內容仍對連結裝置有效,且手機應用程式活動或服務 (例如 CameraManager) 可以存取 AI 眼鏡硬體。

中斷連線時清理

投影的內容會繫結至連結裝置的生命週期,因此裝置中斷連線時,投影內容就會遭到刪除。裝置中斷連線時,ProjectedContext.isProjectedDeviceConnected 會傳回 false。應用程式應監聽這項變更,並清理應用程式使用該投影內容建立的任何系統服務 (例如 CameraManager) 或資源。

重新連線時重新初始化

眼鏡重新連線後,應用程式可以使用 createProjectedDeviceContext 取得另一個投影環境例項,然後使用新的投影環境重新初始化任何系統服務或資源。

使用眼鏡的麥克風錄製音訊

你可以透過兩種不同的方法,錄製眼鏡的音訊:

  • 使用預測情境
  • 使用藍牙免持聽筒設定檔 (HFP)。

選擇錄製方法

選擇的方法取決於您是否需要高保真度、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)。

  • 緩衝區大小沒有固定要求,但建議取得最小緩衝區大小,盡量縮短感知延遲時間。

使用後請自行清潔

當應用程式不再需要麥克風,或活動停止時,請在 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 use case 繫結至眼鏡的相機,並為應用程式使用正確的內容:

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 的影片擷取說明文件

下表列出根據應用程式用途建議的解析度和影格率:

用途 解析度 影格速率
視訊通訊 1280 x 720 15 FPS
電腦視覺 640 x 480 10 FPS
AI 影片串流 640 x 480 1 FPS

從投影活動存取手機硬體

投影活動也可以使用 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
    }
}

在混合式應用程式 (同時包含行動裝置和智慧眼鏡體驗的應用程式) 中存取主機裝置 (手機) 專屬的硬體或資源時,您必須明確選取正確的環境,確保應用程式可以存取正確的硬體: