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 âm thanh và
kính hiển thị

Sau khi bạn 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 âm thanh hoặc kính hiển thị. Để 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), bạn cần sử dụng ngữ cảnh được chiếu.

Có hai cách chính để lấy một ngữ cảnh được chiếu, tuỳ thuộc vào vị trí mà mã của bạn đang thực thi:

Nhận một ngữ cảnh được chiếu nếu mã của bạn đang chạy trong một hoạt động được chiếu

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 riêng của mã đó đã là một ngữ cảnh được chiếu. Trong trường hợp này, các lệnh 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.

Nhận một ngữ cảnh được chiếu cho mã đang chạy trong 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 ngữ 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:

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

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 ngữ cảnh được chiếu vẫn hợp lệ đối với thiết bị thông minh đã kết nối và hoạt động hoặc dịch vụ ứng dụng điện thoại của bạn (chẳng hạn như CameraManager) có thể truy cập vào phần cứng kính AI.

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

Ngữ cảnh được chiếu gắn liền với vòng đời của thiết bị thông minh, vì vậy, ngữ cảnh này sẽ bị huỷ 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 phải 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 của bạn đã tạo bằng ngữ 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 phiên bản ngữ cảnh được chiếu khác bằng cách sử dụng createProjectedDeviceContext, sau đó khởi động lại mọi dịch vụ hệ thống hoặc tài nguyên bằng ngữ 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 theo 2 cách riêng biệt:

Chọn một phương pháp ghi

Phương thức bạn chọn phụ thuộc vào việc bạn cần xử lý âm thanh có độ trung thực cao, dành riêng cho XR hay đầu vào âm thanh Bluetooth tiêu chuẩn.

Phương pháp ghi Quyền truy cập micrô Trường hợp sử dụng phổ biến

Bối cảnh dự án

Nhiều micrô

Tính năng ghi bằng ngữ cảnh được chiếu cho phép ứng dụng của bạn truy cập vào nhiều micrô trên kính và các tính năng phần cứng chuyên dụng của kính, chẳng hạn như:

  • Tính năng tạo không gian lập thể dành riêng cho XR.
  • Khử nhiễu nâng cao.
  • Tính năng tách giọng nói giúp phân biệt giọng nói của người đeo và giọng nói của người xung quanh.
  • Duy trì quyền truy cập ghi âm trong môi trường nhiều thiết bị ngay cả khi kính không phải là thiết bị Bluetooth đang hoạt động.

Bluetooth HFP

Một micrô

Dựa vào Cấu hình rảnh tay (HFP) của Bluetooth để có khả năng tương thích ngay khi sử dụng. Ở chế độ này, kính kết nối với điện thoại bằng cách sử dụng các cấu hình chuẩn của Tai nghe và Cấu hình phân phối âm thanh nâng cao (A2DP), 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 thanh từ kính mà không cần tích hợp bất kỳ chức năng nào dành riêng cho XR.

Ghi âm bằng ngữ cảnh được chiếu

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

Yêu cầu quyền khi bắt đầu chạy

Để truy cập vào nhiều micrô trên kính, bạn phải yêu cầu quyền truy cập âm thanh dành riêng cho thiết bị được chiếu. Quyền RECORD_AUDIO tiêu chuẩn, có phạm vi trên điện thoại mà người dùng đã cấp cho ứng dụng của bạn trên thiết bị di động là không đủ.

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

  1. Khai báo quyền RECORD_AUDIO trong tệp kê khai của ứng dụng.
  2. Yêu cầu các quyền có phạm vi thiết bị được chiếu theo một trong những cách sau, tuỳ thuộc vào vị trí thực thi mã của bạn:

Khởi động AudioRecord bằng một ngữ cảnh được chiếu

Để đảm bảo rằng âm thanh được ghi lại từ kính chứ không phải điện thoại lưu trữ, bạn phải liên kết đối tượng AudioRecord với ngữ cảnh thiết bị được chiếu.

Đoạn mã sau đây sử dụng AudioRecord.Builder và truyền projectedDeviceContext đến phương thức 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()

Các điểm chính về mã
  • Bạn có thể đặt nguồn âm thanh thành CAMCORDER, VOICE_RECOGNITION, VOICE_COMMUNICATION hoặc UNPROCESSED để điều chỉnh quy trình xử lý âm thanh cho trường hợp sử dụng cụ thể của bạn.

    Ví dụ: hãy sử dụng VOICE_COMMUNICATION nếu trường hợp sử dụng của bạn cần tính năng giảm tiếng ồn tự động. VOICE_RECOGNITION được xử lý bằng tính năng khử tiếng vọng âm thanh (AEC). Nếu bạn cần âm thanh thô, chưa chỉnh sửa, hãy chọn UNPROCESSED hoặc CAMCORDER.

  • Để đảm bảo khả năng tương thích với kính, đối tượng audioFormat phải xác định tốc độ lấy mẫu là 16 kHz và cấu hình kênh là đơn âm hoặc âm thanh nổi (bằng cách sử dụng CHANNEL_IN_MONO hoặc CHANNEL_IN_STEREO).

  • Mặc dù không có yêu cầu cố định về dung lượng bộ nhớ đệm, nhưng bạn nên lấy dung lượng bộ nhớ đệm tối thiểu để giảm thiểu độ trễ cảm nhận được.

Dọn dẹp sau khi sử dụng

Khi ứng dụng của bạn không cần dùng micrô nữa hoặc khi hoạt động dừng, hãy gọi stoprelease trên đối tượng AudioRecord.

Kiểm tra quyền khi bắt đầu chạy trước khi ghi

Trước khi gọi startRecording, hãy xác minh rằng người dùng đã cấp quyền truy cập micrô cho kính bằng ngữ cảnh được chiếu.

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 cần thiết khi chạy, 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 khác có liên quan đều do điện thoại kiểm soát chứ không phải thiết bị thông minh được kết nối (chẳng hạn như kính âm thanh hoặc kính hiển thị).

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_AUDIOBLUETOOTH_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 trong thời gian chạy, hãy 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 HFP Bluetooth. Thao tác này chỉ 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 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 ngữ cảnh phù hợp cho ứng dụng của bạn:

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

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 ngữ cảnh thiết bị được chiếu.
  • Trong phạm vi ngữ cảnh được chiếu, camera chính hướng ra ngoài của kính sẽ ánh xạ đến DEFAULT_BACK_CAMERA khi chọn camera.
  • Một quy trình kiểm tra trước khi liên kết sẽ sử dụng cameraProvider.hasCamera(cameraSelector) để xác minh rằng camera đã chọn có 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. Điều này có thể hữu ích cho các bước kiểm tra nâng cao về độ phân giải được hỗ trợ.
  • Một ResolutionSelector tuỳ chỉnh được tạo để 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).

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 của CameraX để tìm hiểu về cách sử dụng takePicture để chụp ảnh.

Quay video bằng camera của 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 liên quan đến 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 quá trình ghi bằng phương thức ghi 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
Truyền thông 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 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 ngữ 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 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
    }
}

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 chứa cả trải nghiệm trên thiết bị di động và kính), bạn phải chọn rõ ngữ cảnh phù hợp để đảm bảo ứng dụng của bạn có thể truy cập vào phần cứng phù hợp:

  • Sử dụng ngữ cảnh Activity từ Activity điện thoại hoặc ProjectedContext.createHostDeviceContext để lấy ngữ cảnh của điện thoại.
  • Không 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 một thành phần được chiếu là thành phần được khởi chạy gần đây nhất.