ใช้บริบทที่คาดการณ์ไว้เพื่อเข้าถึงฮาร์ดแวร์ในแว่นตาเสียงและแว่นตาแสดงผล

อุปกรณ์ XR ที่รองรับ
คำแนะนำนี้จะช่วยคุณสร้างประสบการณ์การใช้งานสำหรับอุปกรณ์ XR ประเภทต่างๆ
แว่นตาสำหรับเสียงและ
แว่นตาแสดงผล

หลังจากขอและได้รับสิทธิ์ที่จำเป็นแล้ว แอปของคุณ จะเข้าถึงฮาร์ดแวร์ในแว่นตาสำหรับเสียงหรือแว่นตาแสดงผลได้ เคล็ดลับในการ เข้าถึงฮาร์ดแวร์ของแว่นตา (แทนที่จะเป็นฮาร์ดแวร์ของโทรศัพท์) คือการใช้ บริบทที่คาดการณ์ไว้

มี 2 วิธีหลักๆ ในการรับบริบทที่คาดการณ์ไว้ ซึ่งขึ้นอยู่กับตำแหน่งที่โค้ดของคุณทำงาน

รับบริบทที่คาดการณ์ไว้หากโค้ดของคุณทำงานในกิจกรรมที่คาดการณ์ไว้

หากโค้ดของแอปทำงานจากภายในกิจกรรมที่คาดการณ์ไว้ บริบทของกิจกรรมเอง จะเป็นบริบทที่คาดการณ์ไว้แล้ว ในสถานการณ์นี้ การเรียกใช้ภายในกิจกรรมนั้นจะเข้าถึงฮาร์ดแวร์ของแว่นตาได้แล้ว

รับบริบทที่คาดการณ์ไว้สำหรับโค้ดที่ทำงานในคอมโพเนนต์แอปโทรศัพท์

หากส่วนหนึ่งของแอปที่อยู่นอกกิจกรรมที่คาดการณ์ไว้ (เช่น กิจกรรมของโทรศัพท์หรือบริการ) ต้องเข้าถึงฮาร์ดแวร์ของแว่นตา ส่วนนั้นจะต้องได้รับบริบทที่คาดการณ์ไว้อย่างชัดเจน โดยใช้เมธอด 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 จากนั้นจึง เริ่มต้นบริการระบบหรือทรัพยากรใหม่โดยใช้บริบทที่คาดการณ์ไว้ใหม่

บันทึกเสียงด้วยไมโครโฟนของแว่นตา

คุณสามารถบันทึกเสียงจากแว่นตาได้ 2 วิธีที่แตกต่างกัน ดังนี้

เลือกวิธีการบันทึก

วิธีการที่คุณเลือกจะขึ้นอยู่กับว่าคุณต้องการการประมวลผลเสียงที่มีความเที่ยงตรงสูงและเฉพาะเจาะจงสำหรับ XR หรืออินพุตเสียงบลูทูธมาตรฐาน

วิธีการบันทึก สิทธิ์เข้าถึงไมโครโฟน กรณีการใช้งานทั่วไป

บริบทที่คาดการณ์ไว้

ไมโครโฟนหลายตัว

การบันทึกโดยใช้บริบทที่คาดการณ์ไว้จะช่วยให้แอปของคุณเข้าถึงไมโครโฟนหลายตัวจากแว่นตาและฟีเจอร์ฮาร์ดแวร์เฉพาะทางของแว่นตาได้ เช่น

  • การสร้างเสียง 3 มิติเฉพาะเจาะจงสำหรับ XR
  • การลดเสียงรบกวนขั้นสูง
  • การแยกเสียงที่แยกเสียงของผู้สวมใส่ออกมา
  • รักษาการเข้าถึงการบันทึกในสภาพแวดล้อมแบบหลายอุปกรณ์ แม้ว่าแว่นตาจะไม่ใช่อุปกรณ์บลูทูธที่ใช้งานอยู่

HFP ของบลูทูธ

ไมโครโฟนตัวเดียว

ใช้โปรไฟล์แฮนด์ฟรี (HFP) ของบลูทูธเพื่อความเข้ากันได้ทันทีโดยไม่ต้องตั้งค่า ในโหมดนี้ แว่นตาจะเชื่อมต่อกับโทรศัพท์โดยใช้โปรไฟล์ชุดหูฟังมาตรฐานและโปรไฟล์การกระจายเสียงขั้นสูง (A2DP) โปรไฟล์ ซึ่งทำงานเหมือนอุปกรณ์ต่อพ่วงบลูทูธทั่วไป

หากแอปของคุณได้รับการออกแบบมาสำหรับการบันทึกบลูทูธมาตรฐานอยู่แล้ว คุณสามารถใช้วิธีนี้เพื่อบันทึกเสียงจากแว่นตาโดยไม่ต้องผสานรวมความสามารถเฉพาะเจาะจงสำหรับ XR

บันทึกเสียงโดยใช้บริบทที่คาดการณ์ไว้

หากต้องการบันทึกเสียงโดยใช้บริบทที่คาดการณ์ไว้ ให้ขอสิทธิ์รันไทม์ที่จำเป็น ก่อน จากนั้นบันทึกเสียงโดยใช้ AudioRecord API ตามที่ อธิบายไว้ในส่วนต่อไปนี้

ขอสิทธิ์รันไทม์

หากต้องการเข้าถึงไมโครโฟนหลายตัวในแว่นตา คุณต้องขอสิทธิ์เสียงสำหรับอุปกรณ์ที่คาดการณ์ไว้โดยเฉพาะ สิทธิ์ RECORD_AUDIO ที่มีขอบเขตเป็นโทรศัพท์ซึ่งผู้ใช้ให้ไว้สำหรับแอปในอุปกรณ์เคลื่อนที่นั้นไม่เพียงพอ

ทำตามขั้นตอนต่อไปนี้เพื่อขอสิทธิ์

  1. ประกาศสิทธิ์ RECORD_AUDIO permission ในไฟล์ Manifest ของแอป
  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()

ประเด็นสำคัญเกี่ยวกับโค้ด
  • คุณสามารถตั้งค่าแหล่งที่มาของเสียงเป็น 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 มิลลิวินาที) แทนที่จะรอให้บัฟเฟอร์เต็ม

ล้างข้อมูลหลังการใช้งาน

เมื่อแอปไม่ต้องการใช้ไมโครโฟนอีกต่อไปหรือเมื่อกิจกรรมหยุดลง ให้เรียกใช้ stop และ release ในออบเจ็กต์ AudioRecord

ตรวจสอบสิทธิ์รันไทม์ก่อนบันทึก

ก่อนเรียกใช้ startRecording, ตรวจสอบว่าผู้ใช้ได้ให้สิทธิ์เข้าถึงไมโครโฟนสำหรับแว่นตาโดยใช้บริบทที่คาดการณ์ไว้

บันทึกเสียงโดยใช้ HFP ของบลูทูธ

หากต้องการบันทึกเสียงโดยใช้ HFP ของบลูทูธ ให้ขอสิทธิ์รันไทม์ที่จำเป็น ก่อน จากนั้นบันทึกเสียงโดยใช้ AudioManager API ตามที่ อธิบายไว้ในส่วนต่อไปนี้

ขอสิทธิ์

เช่นเดียวกับอุปกรณ์เสียงบลูทูธมาตรฐาน สิทธิ์ RECORD_AUDIO, BLUETOOTH_CONNECT และสิทธิ์อื่นๆ ที่เกี่ยวข้องจะควบคุมโดย โทรศัพท์ ไม่ใช่อุปกรณ์ที่เชื่อมต่อ (เช่น แว่นตาที่มีระบบเสียง หรือแว่นตาที่มีจอแสดงผล)

ทำตามขั้นตอนต่อไปนี้เพื่อขอสิทธิ์

  1. ประกาศสิทธิ์ต่อไปนี้ในไฟล์ Manifest ของแอป

  2. ขอสิทธิ์ RECORD_AUDIO และ BLUETOOTH_CONNECT ทั้ง 2 รายการในเวลาที่ รันไทม์โดยใช้ โฟลว์สิทธิ์มาตรฐานของ Android

ใช้ 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()

ถ่ายภาพด้วยกล้องของแว่นตา

หากต้องการถ่ายภาพด้วยกล้องของแว่นตา ให้ตั้งค่าและผูกกรณีการใช้งาน ImageCapture ของ CameraX กับกล้องของแว่นตาโดยใช้บริบทที่ถูกต้อง สำหรับแอป

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 ได้อย่างแม่นยำ
  • สร้างกรณีการใช้งาน ImageCapture ที่กำหนดค่าด้วย ResolutionSelector ที่กำหนดเอง
  • ผูกกรณีการใช้งาน ImageCapture กับวงจรชีวิตของกิจกรรม ซึ่งจะจัดการการเปิดและปิดกล้องโดยอัตโนมัติตามสถานะของกิจกรรม (เช่น หยุดกล้องเมื่อกิจกรรมหยุดชั่วคราว)
  • การกำหนดค่าสำหรับฮาร์ดแวร์แบบสตรีมเดียว: ไปป์ไลน์ของกล้องในแว่นตาจะจำกัดไว้ที่สตรีมเดียวที่ใช้งานอยู่พร้อมกัน เมื่อผูก ImageCapture เพียงอย่างเดียว CameraX จะแนบสตรีมที่ทำซ้ำภายใน (MeteringRepeating) สำหรับการโฟกัสและการวัดแสงโดยค่าเริ่มต้น ซึ่งทำให้ไปป์ไลน์แบบสตรีมเดียวหยุดทำงาน หากต้องการผูก ImageCapture ด้วยตัวเองโดยไม่มี สตรีมการแสดงตัวอย่าง ให้ปิดใช้สตรีมที่ทำซ้ำแบบบังคับโดยตั้งค่า setRepeatingStreamForced(false) ใน CameraXConfig.Builder (เช่น โดยการใช้ CameraXConfig.Provider ในคลาส Application หรือกำหนดค่า ProcessCameraProvider ก่อนการเริ่มต้น)

หลังจากตั้งค่ากล้องของแว่นตาแล้ว คุณจะถ่ายภาพด้วยคลาส ImageCapture ของ CameraX ได้ โปรดดูเอกสารประกอบของ 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 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 เนื่องจากบริบทของแอปพลิเคชัน อาจแสดงผลบริบทของแว่นตาอย่างไม่ถูกต้องหากกิจกรรมที่คาดการณ์ไว้เป็น คอมโพเนนต์ที่เปิดตัวล่าสุด