Ses gözlüklerindeki ve ekran gözlüklerindeki donanıma erişmek için yansıtılan bağlamı kullanma

Uygun XR cihazlar
Bu kılavuz, bu tür XR cihazlar için deneyimler geliştirmenize yardımcı olur.
Ses ve
Görüntü Arabirimli Gözlük

Gerekli izinleri isteyip aldıktan sonra uygulamanız, ses arabirimli gözlük veya görüntü arabirimli gözlükteki donanıma erişebilir. Telefonun donanımı yerine gözlüğün donanımına erişmek için yansıtılan bağlam kullanmanız gerekir.

Kodunuzun nerede yürütüldüğüne bağlı olarak, yansıtılan bağlamı elde etmenin iki temel yolu vardır:

Kodunuz yansıtılan bir etkinlikte çalışıyorsa yansıtılan bağlamı alma

Uygulamanızın kodu yansıtılan etkinliğinizin içinden çalışıyorsa kendi etkinlik bağlamı zaten yansıtılan bağlamdır. Bu senaryoda, bu etkinlik içinde yapılan çağrılar gözlüğün donanımına zaten erişebilir.

Telefon uygulaması bileşeninde çalışan kod için öngörülen bağlamı alma

Uygulamanızın, yansıtılan etkinliğinizin dışında kalan bir bölümünün (ör. telefon etkinliği veya hizmet) gözlüğün donanımına erişmesi gerekiyorsa açıkça yansıtılan bir bağlam elde etmesi gerekir. Bunun için createProjectedDeviceContext yöntemini kullanın:

@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...")
                }
            }
    }
}

Geçerlilik kontrolü

createProjectedDeviceContext çağrısını ProjectedContext.isProjectedDeviceConnected içine alın. Bu yöntem true döndürürken yansıtılan bağlam, bağlı cihaz için geçerli kalır ve telefon uygulaması etkinliğiniz veya hizmetiniz (ör. CameraManager) gözlük donanımına erişebilir.

Bağlantı kesildiğinde temizle

Yansıtılan bağlam, bağlı cihazın yaşam döngüsüne bağlıdır. Bu nedenle, cihazın bağlantısı kesildiğinde yok edilir. Cihazın bağlantısı kesildiğinde ProjectedContext.isProjectedDeviceConnected, false değerini döndürür. Uygulamanız bu değişikliği dinlemeli ve sistem hizmetlerini (ör. CameraManager) veya uygulamanızın bu yansıtılan bağlamı kullanarak oluşturduğu kaynakları temizlemelidir.

Yeniden bağlanıldığında yeniden başlat

Gözlük yeniden bağlandığında uygulamanız, createProjectedDeviceContext kullanarak başka bir yansıtılan bağlam örneği alabilir ve ardından yeni yansıtılan bağlamı kullanarak tüm sistem hizmetlerini veya kaynaklarını yeniden başlatabilir.

Gözlüğün mikrofonuyla ses kaydetme

Bluetooth Eller Serbest Profili'ni (HFP) kullanarak gözlükten ses kaydedebilirsiniz. Bu modda gözlük, standart mikrofonlu kulaklık ve Gelişmiş Ses Dağıtım Profili (A2DP) profillerini kullanarak telefona bağlanır ve tipik bir Bluetooth çevre birimi gibi çalışır. Uygulamanız standart Bluetooth kaydı için tasarlanmışsa gözlükten ses kaydetmek için bu yöntemi kullanabilirsiniz. Ek bir geliştirme yapmanız gerekmez.

Bluetooth HFP kullanarak ses kaydetme

Bluetooth HFP kullanarak ses kaydetmek için önce gerekli çalışma zamanında istenen izinleri isteyin, ardından aşağıdaki bölümlerde açıklandığı gibi AudioManager API'yi kullanarak sesi kaydedin.

İzin iste

Tüm standart Bluetooth ses sistemlerinde olduğu gibi RECORD_AUDIO, BLUETOOTH_CONNECT ve diğer ilgili izinler, bağlı cihaz (ör. sesli gözlük veya görüntülü gözlük) tarafından değil, telefon tarafından kontrol edilir.

İzinleri istemek için aşağıdaki adımları uygulayın:

  1. Uygulamanızın manifest dosyasında aşağıdaki izinleri beyan edin:

  2. Standart Android izin akışını kullanarak çalışma zamanında hem RECORD_AUDIO hem de BLUETOOTH_CONNECT izinlerini isteyin.

Sesi yönlendirmek için AudioManager'ı kullanma

Kullanıcı, uygulamanıza gerekli çalışma zamanında istenen izinleri verdikten sonra sesi Bluetooth HFP üzerinden yönlendirmek için AudioManager API'sini kullanarak iletişim cihazını TYPE_BLUETOOTH_SCO olarak ayarlayın. Bu, sisteme Bluetooth çevre biriminden ses alması talimatını verir.

val audioManager = context.getSystemService(AudioManager::class.java) ?: return
val hfpDevice = audioManager.availableCommunicationDevices
    .firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } ?: return

// HFP input is limited to 16 kHz, with mono or stereo channel configurations.
val sampleRate = 16_000
val channelMask = AudioFormat.CHANNEL_IN_MONO
val encoding = AudioFormat.ENCODING_PCM_16BIT

// Use a small multiple of the minimum buffer size to keep chunks short for low latency.
val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, encoding)
if (minBufferSize <= 0) return

val audioRecord = AudioRecord.Builder()
    .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
    .setAudioFormat(
        AudioFormat.Builder()
            .setSampleRate(sampleRate)
            .setChannelMask(channelMask)
            .setEncoding(encoding)
            .build()
    )
    .setBufferSizeInBytes(minBufferSize * 2)
    .build()

try {
    // Route voice communication audio through the Bluetooth device.
    if (!audioManager.setCommunicationDevice(hfpDevice)) return
    audioRecord.startRecording()
    // Read audio with audioRecord.read() on a background thread.
} finally {
    // Stop and release when done.
    if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) audioRecord.stop()
    audioRecord.release()
    audioManager.clearCommunicationDevice()
}

Akıllı gözlüğün kamerasıyla görüntü yakalama

Gözlüğün kamerasıyla görüntü yakalamak için CameraX'in ImageCapture kullanım alanını ayarlayın ve uygulamanız için doğru bağlamı kullanarak gözlüğün kamerasına bağlayın:

private fun setUpGlassesCamera(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 dynamically using Camera2 Interop.
                        val cameraInfo = cameraProvider.getCameraInfo(cameraSelector)
                        val map = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(
                            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
                        )
                        val supportedResolutions = map?.getOutputSizes(ImageFormat.JPEG)?.toList() ?: emptyList()
                        Log.i(TAG, "Supported resolutions for JPEG: ${supportedResolutions.joinToString()}")

                        // 4. Dynamically select the best target resolution from the queried list.
                        val preferredResolution = Size(1920, 1080)
                        val dynamicTargetResolution = when {
                            // Case A: The preferred resolution is explicitly supported by the glasses.
                            supportedResolutions.contains(preferredResolution) -> preferredResolution

                            // Case B: Find the highest supported 16:9 resolution as a fallback.
                            else -> {
                                val targetAspectRatio = 16.0 / 9.0
                                val tolerance = 0.01
                                supportedResolutions
                                    .filter { abs((it.width.toDouble() / it.height) - targetAspectRatio) < tolerance }
                                    .maxByOrNull { it.width * it.height }
                                    // Case C: Absolute fallback if no 16:9 resolutions are found.
                                    ?: supportedResolutions.firstOrNull()
                                    ?: preferredResolution
                            }
                        }
                        Log.i(TAG, "Selected target resolution: $dynamicTargetResolution")

                        val resolutionStrategy = ResolutionStrategy(
                            dynamicTargetResolution,
                            ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
                        )
                        val resolutionSelector = ResolutionSelector.Builder()
                            .setResolutionStrategy(resolutionStrategy)
                            .build()

                        // 5. 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))
                }
            }
    }
}

Kodla ilgili önemli noktalar

  • Yansıtılan cihaz bağlamını kullanarak ProcessCameraProvider örneği alır.
  • Yansıtılan bağlamın kapsamında, kamera seçerken gözlüğün birincil, dışa bakan kamerası DEFAULT_BACK_CAMERA ile eşleşir.
  • Bağlama öncesi kontrol, devam etmeden önce seçilen kameranın cihazda kullanılabilir olduğunu doğrulamak için cameraProvider.hasCamera(cameraSelector) kullanır.
  • Temel CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP'yi okumak için Camera2CameraInfo ile Camera2 Interop'u kullanır. Bu, desteklenen çözünürlüklerle ilgili gelişmiş kontroller için yararlı olabilir.
  • ImageCapture için çıkış görüntüsü çözünürlüğünü hassas bir şekilde kontrol etmek amacıyla özel bir ResolutionSelector oluşturulur.
  • Özel ResolutionSelector ile yapılandırılmış bir ImageCapture kullanım alanı oluşturur.
  • ImageCapture kullanım alanını etkinliğin yaşam döngüsüne bağlar. Bu, etkinliğin durumuna göre kameranın açılıp kapanmasını otomatik olarak yönetir (örneğin, etkinlik duraklatıldığında kamerayı durdurur).
  • Tek akışlı donanım için yapılandırma: Gözlük kamera ardışık düzeni, aynı anda tek bir etkin akışla sınırlıdır. ImageCapture tek başına bağlandığında CameraX, varsayılan olarak odaklama ve ölçüm için dahili bir yinelenen akış (MeteringRepeating) ekler. Bu da tek akışlı ardışık düzenin durmasına neden olur. ImageCapture öğesini önizleme akışı olmadan tek başına bağlamak için CameraXConfig.Builder üzerinde setRepeatingStreamForced(false) ayarını yaparak zorunlu yinelenen akışı devre dışı bırakın (örneğin, Application sınıfınızda CameraXConfig.Provider uygulayarak veya başlatmadan önce ProcessCameraProvider öğesini yapılandırarak).

Akıllı gözlüğün kamerası ayarlandıktan sonra CameraX'in ImageCapture sınıfıyla görüntü yakalayabilirsiniz. Görüntü yakalamak için takePicture kullanma hakkında bilgi edinmek üzere CameraX belgelerine bakın.

Akıllı gözlüğün kamerasıyla yayın yapma

Resim yerine akışları yakalamak için resim yakalama örneğindeki ImageCapture bileşenini VideoCapture veya ImageAnalysis bileşenleriyle değiştirin ve yakalama mantığını değiştirin.

Bu API'ler ve bunların nasıl kullanılacağı hakkında daha fazla bilgi için CameraX video yakalama ve CameraX görüntü analizi belgelerine bakın.

Çözünürlük ve kare hızı seçme

Desteklenen çözünürlükleri ve bunlara karşılık gelen maksimum kare hızlarını bulmak için cihazı sorgulamanız gerekir. Aşağıdaki tabloda, uygulamanızın kullanım alanına bağlı olarak görebileceğiniz değer örnekleri listelenmiştir. Pil gücünden tasarruf etmek ve gözlüğün ısınmasını azaltmak için kullanım alanınıza uygun en düşük çözünürlüğü ve kare hızını seçin.

Kullanım alanı Çözünürlük Kare hızı
Metin tanıma (OCR) veya dünya algılama 1920 x 1080 1 FPS
Yapay zeka video akışı (çok formatlı asistanlar) 1024 x 768 1 FPS
Yerelleştirme ve görsel konumlandırma (VPS) 640 x 360 veya 640 x 480 10 FPS
Görüntülü görüşme 640 x 360 veya 640 x 480 24 FPS

Her çözünürlük için maksimum kare hızını sorgulama

CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES, cihazın genel kare hızı sınırlarını bildirir. Bu sınırlar, belirli bir çözünürlüğün desteklediği akış hızından daha yüksek olabilir. Sesli ve görüntülü gözlüklerde her çözünürlük için gerçek maksimum değeri elde etmek istiyorsanız minimum kare süresini okuyun ve StreamConfigurationMap.getOutputMinFrameDuration(format, size) ile FPS'ye dönüştürün:

@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
fun getMaxFpsBySize(cameraInfo: CameraInfo, format: Int): Map<Size, Int> {
    val map = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(
        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
    ) ?: return emptyMap()

    return map.getOutputSizes(format).associateWith { size ->
        val minFrameDurationNs = map.getOutputMinFrameDuration(format, size)
        if (minFrameDurationNs > 0) (1_000_000_000L / minFrameDurationNs).toInt() else 0
    }
}

Ardından, hedef kare hızı aralığınızın minFps ve maxFps sınırlarını hesaplamak için bu değeri kullanın. Alt sınırı (minFps), getOutputMinFrameDuration değerinden hesaplanan çözünürlüğün maksimum akış kare hızına ayarlayın ve daha yüksek bir üst sınır (maxFps) belirleyin:

val maxFpsBySize = getMaxFpsBySize(cameraInfo, ImageFormat.YUV_420_888)
val maxSupportedFps = maxFpsBySize[Size(640, 480)] ?: return ImageAnalysis.Builder().build()

// minFps is the target streaming frame rate and should be at most maxSupportedFps.
val minFps = minOf(desiredFps, maxSupportedFps)
// Set the upper bound to 2x minFps so the stream can catch up if frames are delayed.
val maxFps = minFps * 2

Kodla ilgili önemli noktalar

  • Kullanım alanınıza uygun biçimi kullanın. ImageAnalysis için bu tutar YUV_420_888.
  • VideoCapture için bunun yerine Class aşırı yüklemelerini kullanın: getOutputSizes(MediaRecorder::class.java) ve getOutputMinFrameDuration(MediaRecorder::class.java, size).
  • Her iki sınırı da Range(minFps, minFps) gibi aynı değere ayarlamak yerine maxFps değerinin minFps değerinden büyük olduğu bir aralık (ör. minFps*2) belirtin. Alt sınır (minFps), hedef yayın kare hızını belirler ve en fazla getOutputMinFrameDuration değerinden hesaplanan değer kadar olmalıdır. Üst sınır (maxFps) ise gecikmeli kareler için bir telafi penceresi görevi görerek kalıcı gecikmeyi önler. Örneğin, 1 FPS'lik bir akış için [1,2] veya [1,3] ve 24 FPS'lik bir akış için [24,30] veya [24,48].

Kare hızını yapılandırma

Kare hızı aralığını CameraX kullanım alanınıza göre yapılandırmak için:

// Configure Resolution Strategy for the stream
val resolutionStrategy = ResolutionStrategy(
    size,
    ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER
)
val resolutionSelector = ResolutionSelector.Builder()
    .setResolutionStrategy(resolutionStrategy)
    .build()

// Create and return the ImageAnalysis builder with proper frame rate ranges
// ImageAnalysis: OCR, AI streaming, and VPS
val analysisBuilder = ImageAnalysis.Builder()
    .setResolutionSelector(resolutionSelector)

Camera2Interop.Extender(analysisBuilder)
    .setCaptureRequestOption(
        CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
        Range(minFps, maxFps)
    )
return analysisBuilder.build()

Bu örnekte hem VideoCapture hem de ImageAnalysis için yapılandırma gösterilmektedir ancak uygulamanızın desteklediği kullanım alanlarını uygulayabilirsiniz.

Akışın nasıl davrandığını anlama

Yakalama akışlarıyla çalışırken aşağıdaki sistem davranışlarını göz önünde bulundurun:

  • Desteklenmeyen çözünürlükler reddedilir: Listede olmayan bir boyut isterseniz Camera2, akış açılmadan önce yapılandırmayı reddeder.
  • Maksimum değerin üzerinde kare hızı isteme: Çözünürlüğünüz için desteklenen maksimum değerin üzerinde bir minimum kare hızı isterseniz oturum yine de açılır ancak gözlükten gelen hedef kare hızı, desteklenen maksimum değerle sınırlanır ve üst sınır, yakalama hızı sıklık sınırlaması olarak kullanılmaya devam eder. Örneğin, 640 x 480 çözünürlükte [30,60] isteği, hedef kare hızını 24 FPS'ye sabitler ([24,60] gibi davranır).
  • Termal kısıtlama: Gözlük ısındığında kare hızı düşer ancak çözünürlük aynı kalır. Uygulamanızı, daha düşük veya düzensiz kare hızlarını işleyecek şekilde oluşturun. Örneğin, sabit bir hız varsaymak yerine kare zaman damgalarını kontrol edin.

Yansıtılan bir etkinlikten telefonun donanımına erişme

Yansıtılan etkinlik, ana makine cihazın (telefon) bağlamını almak için createHostDeviceContext(context) kullanarak telefonun donanımına (ör. kamera veya mikrofon) da erişebilir:

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

Karma bir uygulamada (hem mobil hem de gözlük deneyimleri içeren bir uygulama) ana cihaza (telefon) özgü donanım veya kaynaklara erişirken uygulamanızın doğru donanıma erişebilmesi için doğru bağlamı açıkça seçmeniz gerekir: