Sử dụng bối cảnh được chiếu để truy cập vào phần cứng trên kính âm thanh và kính hiển thị

Các thiết bị XR được hỗ trợ
Hướng dẫn này giúp bạn xây dựng các trải nghiệm cho những loại thiết bị XR sau.
Kính có âm thanh
và màn hình

Sau khi yêu cầu và được cấp các quyền cần thiết, ứng dụng của bạn có thể truy cập vào phần cứng trên kính phát âm thanh hoặc kính có màn hình. Chìa khoá để truy cập vào phần cứng của kính (thay vì phần cứng của điện thoại) là sử dụng bối cảnh dự kiến.

Có hai cách chính để lấy bối cảnh dự kiến, tuỳ thuộc vào nơi mã của bạn đang thực thi:

Nhận bối cảnh dự kiến nếu mã của bạn đang chạy trong một hoạt động dự kiến

Nếu mã của ứng dụng đang chạy trong hoạt động được chiếu, thì ngữ cảnh hoạt động của chính ứng dụng đó đã là ngữ cảnh được chiếu. Trong trường hợp này, các cuộc gọi được thực hiện trong hoạt động đó đã có thể truy cập vào phần cứng của kính.

Lấy bối cảnh dự kiến cho mã đang chạy trong một thành phần ứng dụng điện thoại

Nếu một phần của ứng dụng nằm ngoài hoạt động được chiếu (chẳng hạn như hoạt động trên điện thoại hoặc dịch vụ) cần truy cập vào phần cứng của kính, thì phần đó phải lấy được bối cảnh được chiếu một cách rõ ràng. Để thực hiện việc này, hãy sử dụng phương thức 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...")
                }
            }
    }
}

Kiểm tra tính hợp lệ

Gói lệnh gọi createProjectedDeviceContext trong ProjectedContext.isProjectedDeviceConnected. Mặc dù phương thức này trả về true, nhưng bối cảnh được chiếu vẫn hợp lệ với thiết bị thông minh được kết nối và hoạt động hoặc dịch vụ của ứng dụng điện thoại (chẳng hạn như CameraManager) có thể truy cập vào phần cứng của kính AI.

Dọn dẹp khi ngắt kết nối

Bối cảnh dự kiến được liên kết với vòng đời của thiết bị thông minh, vì vậy, bối cảnh này sẽ bị hủy bỏ khi thiết bị ngắt kết nối. Khi thiết bị ngắt kết nối, ProjectedContext.isProjectedDeviceConnected sẽ trả về false. Ứng dụng của bạn nên theo dõi thay đổi này và dọn dẹp mọi dịch vụ hệ thống (chẳng hạn như CameraManager) hoặc tài nguyên mà ứng dụng đã tạo bằng bối cảnh được chiếu đó.

Khởi động lại khi kết nối lại

Khi kính kết nối lại, ứng dụng của bạn có thể lấy một thực thể bối cảnh được chiếu khác bằng createProjectedDeviceContext, sau đó khởi chạy lại mọi dịch vụ hoặc tài nguyên hệ thống bằng bối cảnh được chiếu mới.

Ghi âm bằng micrô của kính

Bạn có thể ghi âm bằng kính thông qua Cấu hình rảnh tay (HFP) của Bluetooth. Ở chế độ này, kính kết nối với điện thoại bằng cấu hình Tai nghe và Cấu hình phân phối âm thanh nâng cao (A2DP) tiêu chuẩn, hoạt động như một thiết bị ngoại vi Bluetooth thông thường. Nếu ứng dụng của bạn đã được thiết kế để ghi âm qua Bluetooth tiêu chuẩn, thì bạn có thể dùng phương thức này để ghi âm từ kính mà không cần phát triển thêm.

Ghi âm bằng Bluetooth HFP

Để ghi âm bằng Bluetooth HFP, trước tiên, hãy yêu cầu các quyền khi bắt đầu chạy cần thiết, sau đó ghi âm bằng API AudioManager, như mô tả trong các phần sau.

Yêu cầu cấp quyền

Giống như mọi thiết bị âm thanh Bluetooth tiêu chuẩn, RECORD_AUDIO, BLUETOOTH_CONNECT và các quyền liên quan khác đều do điện thoại kiểm soát chứ không phải thiết bị thông minh (chẳng hạn như kính phát âm thanh hoặc kính có màn hình).

Hãy làm theo các bước sau để yêu cầu cấp quyền:

  1. Khai báo các quyền sau trong tệp kê khai của ứng dụng:

  2. Yêu cầu cả quyền RECORD_AUDIO và BLUETOOTH_CONNECT khi bắt đầu chạy bằng quy trình cấp quyền tiêu chuẩn của Android.

Sử dụng AudioManager để định tuyến âm thanh

Sau khi người dùng cấp cho ứng dụng của bạn các quyền cần thiết khi bắt đầu chạy, hãy sử dụng API AudioManager để đặt thiết bị liên lạc thành TYPE_BLUETOOTH_SCO nhằm định tuyến âm thanh qua Bluetooth HFP. Thao tác này sẽ hướng dẫn hệ thống truy xuất âm thanh từ thiết bị ngoại vi 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()

Chụp ảnh bằng camera của mắt kính

Để chụp ảnh bằng camera của kính, hãy thiết lập và liên kết trường hợp sử dụng ImageCapture của CameraX với camera của kính bằng cách sử dụng bối cảnh chính xác cho ứng dụng của bạn:

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

Các điểm chính về mã

  • Lấy một thực thể của ProcessCameraProvider bằng cách sử dụng bối cảnh thiết bị được chiếu.
  • Trong phạm vi của bối cảnh được chiếu, camera chính hướng ra ngoài của kính sẽ liên kết với DEFAULT_BACK_CAMERA khi chọn camera.
  • Quá trình kiểm tra trước khi liên kết sử dụng cameraProvider.hasCamera(cameraSelector) để xác minh rằng camera đã chọn có sẵn trên thiết bị trước khi tiếp tục.
  • Sử dụng Camera2 Interop với Camera2CameraInfo để đọc CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP cơ bản, có thể hữu ích cho việc kiểm tra nâng cao về độ phân giải được hỗ trợ.
  • ResolutionSelector tuỳ chỉnh được xây dựng để kiểm soát chính xác độ phân giải hình ảnh đầu ra cho ImageCapture.
  • Tạo một trường hợp sử dụng ImageCapture được định cấu hình bằng ResolutionSelector tuỳ chỉnh.
  • Liên kết trường hợp sử dụng ImageCapture với vòng đời của hoạt động. Thao tác này sẽ tự động quản lý việc mở và đóng camera dựa trên trạng thái của hoạt động (ví dụ: dừng camera khi hoạt động bị tạm dừng).
  • Định cấu hình cho phần cứng một luồng: Quy trình camera của kính bị giới hạn ở một luồng hoạt động tại một thời điểm. Khi ImageCapture được liên kết riêng, CameraX sẽ đính kèm một luồng lặp lại nội bộ (MeteringRepeating) để lấy nét và đo sáng theo mặc định, khiến quy trình một luồng bị đình trệ. Để tự liên kết ImageCapture mà không cần luồng xem trước, hãy tắt luồng lặp lại bắt buộc bằng cách đặt setRepeatingStreamForced(false) trên CameraXConfig.Builder (ví dụ: bằng cách triển khai CameraXConfig.Provider trong lớp Application hoặc định cấu hình ProcessCameraProvider trước khi khởi chạy).

Sau khi thiết lập camera của kính, bạn có thể chụp ảnh bằng lớp ImageCapture của CameraX. Tham khảo tài liệu về CameraX để tìm hiểu cách sử dụng takePicture để chụp ảnh.

Quay video bằng camera của mắt kính

Để quay video thay vì chụp ảnh bằng camera của kính, hãy thay thế các thành phần ImageCapture bằng các thành phần VideoCapture tương ứng và sửa đổi logic thực thi chụp.

Những thay đổi chính bao gồm việc sử dụng một trường hợp sử dụng khác, tạo một tệp đầu ra khác và bắt đầu quay bằng phương thức quay video thích hợp. Để biết thêm thông tin về API VideoCapture và cách sử dụng API này, hãy xem tài liệu về tính năng quay video của CameraX.

Bảng sau đây cho thấy độ phân giải và tốc độ khung hình được đề xuất tuỳ thuộc vào trường hợp sử dụng của ứng dụng:

Trường hợp sử dụng Độ phân giải Tốc độ khung hình
Giao tiếp qua video 1280 x 720 15 khung hình/giây
Thị giác máy tính 640 x 480 10 khung hình/giây
Phát trực tuyến video bằng AI 640 x 480 1 khung hình/giây

Truy cập vào phần cứng của điện thoại từ một hoạt động được chiếu

Hoạt động được chiếu cũng có thể truy cập vào phần cứng của điện thoại (chẳng hạn như camera hoặc micrô) bằng cách sử dụng createHostDeviceContext(context) để lấy bối cảnh của thiết bị lưu trữ (điện thoại):

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

Khi truy cập vào phần cứng hoặc tài nguyên dành riêng cho thiết bị lưu trữ (điện thoại) trong một ứng dụng kết hợp (ứng dụng có cả trải nghiệm trên thiết bị di động và kính), bạn phải chọn rõ ràng bối cảnh chính xác để đảm bảo ứng dụng có thể truy cập vào đúng phần cứng:

  • Sử dụng bối cảnh Activity từ điện thoại Activity hoặc ProjectedContext.createHostDeviceContext để lấy bối cảnh của điện thoại.
  • Đừng sử dụng getApplicationContext vì ngữ cảnh ứng dụng có thể trả về không chính xác ngữ cảnh của kính nếu hoạt động được chiếu là thành phần được khởi chạy gần đây nhất.