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

อุปกรณ์ 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 จากนั้นจึงเริ่มต้นบริการหรือทรัพยากรระบบอีกครั้งโดยใช้บริบทที่ฉายใหม่

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

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

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

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

ขอสิทธิ์

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

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

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

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

ใช้ AudioManager เพื่อกำหนดเส้นทางเสียง

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

หลังจากตั้งค่ากล้องของแว่นตาแล้ว คุณจะจับภาพด้วยคลาส ImageCapture ของ CameraX ได้ ดูข้อมูลเกี่ยวกับการใช้ takePicture เพื่อจับภาพได้ในเอกสารประกอบของ CameraX

จับภาพวิดีโอด้วยกล้องของแว่นตา

หากต้องการจับภาพวิดีโอแทนรูปภาพด้วยกล้องของแว่นตา ให้แทนที่คอมโพเนนต์ ImageCapture ด้วยคอมโพเนนต์ VideoCapture ที่เกี่ยวข้อง และแก้ไขตรรกะการดำเนินการจับภาพ

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