از بافت پیش‌بینی‌شده برای دسترسی به سخت‌افزار عینک‌های صوتی و عینک‌های نمایشگر استفاده کنید

دستگاه‌های XR واجدشرایط
این راهنمایی به شما کمک می‌کند برای این نوع دستگاه‌های واقعیت گسترده تجربه بسازید.
عینک
نمایشگردار و صوتی

پس‌از اینکه اجازه‌های لازم را درخواست کردید و به شما اعطا شد، برنامه‌تان می‌تواند به سخت‌افزار عینک صوتی یا عینک نمایشگردار دسترسی پیدا کند. کلید دسترسی به سخت‌افزار عینک (به‌جای سخت‌افزار تلفن) استفاده از بافت پیش‌بینی‌شده است.

دو روش اصلی برای دریافت بافت پیش‌بینی‌شده وجود دارد که به محل اجرای کد شما بستگی دارد:

اگر کدتان در فعالیت پیش‌بینی‌شده‌ای اجرا می‌شود، بافت پیش‌بینی‌شده‌ای دریافت کنید

اگر کد برنامه‌تان از داخل فعالیت فرافکنی‌شده شما اجرا می‌شود، بافت فعالیت خودش ازقبل بافت فرافکنی‌شده است. در این سناریو، تماس‌هایی که در آن فعالیت برقرار می‌شود ازقبل می‌توانند به سخت‌افزار عینک دسترسی داشته باشند.

دریافت بافت پیش‌بینی‌شده برای کد درحال اجرا در مؤلفه برنامه تلفن

اگر بخشی از برنامه‌تان خارج از فعالیت فرافکنی‌شده شما (مثل فعالیت تلفن یا سرویس) نیاز به دسترسی به سخت‌افزار عینک داشته باشد، باید به‌طور صریح بافت فرافکنی‌شده را دریافت کند. برای انجام این کار، از روش 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) می‌تواند به سخت‌افزار عینک دسترسی داشته باشد.

پاک‌سازی هنگام قطع اتصال

بافت پیش‌بینی‌شده به چرخه حیات دستگاه متصل‌شده گره خورده است، بنابراین وقتی اتصال دستگاه قطع می‌شود، ازبین می‌رود. وقتی اتصال دستگاه قطع می‌شود، ProjectedContext.isProjectedDeviceConnected false را برمی‌گرداند. برنامه شما باید به این تغییر گوش دهد و هرگونه سرویس سیستم (مانند CameraManager) یا منابعی را که برنامه شما بااستفاده از آن زمینه پیش‌بینی‌شده ایجاد کرده است پاک کند.

مقداردهی اولیه مجدد هنگام اتصال مجدد

وقتی عینک دوباره متصل می‌شود، برنامه‌تان می‌تواند بااستفاده از createProjectedDeviceContext نمونه زمینه پیش‌بینی‌شده دیگری را دریافت کند و سپس هرگونه سرویس یا منبع سیستم را بااستفاده از زمینه پیش‌بینی‌شده جدید دوباره مقداردهی اولیه کند.

ضبط کردن صدا با میکروفون عینک

می‌توانید بااستفاده از «نمایه دست‌آزاد» (HFP) بلوتوث، از عینک صدا ضبط کنید. در این حالت، عینک بااستفاده از نمایه‌های استاندارد «هدست» و «نمایه توزیع پیشرفته صدا» (A2DP) به تلفن متصل می‌شود و مانند یک دستگاه جانبی بلوتوث معمولی عمل می‌کند. اگر برنامه‌تان ازقبل برای ضبط استاندارد بلوتوث طراحی شده است، می‌توانید از این روش برای ضبط صدا از عینک بدون هیچ‌گونه توسعه اضافی استفاده کنید.

ضبط صدا بااستفاده از Bluetooth HFP

برای ضبط صدا بااستفاده از Bluetooth HFP، ابتدا اجازه‌های زمان اجرای لازم را درخواست کنید، سپس صدا را بااستفاده از میانای برنامه‌سازی کاربردی AudioManager ضبط کنید، همان‌طور که در بخش‌های زیر شرح داده شده است.

درخواست اجازه‌ها

مانند هر دستگاه صوتی بلوتوث استاندارد، RECORD_AUDIO، BLUETOOTH_CONNECT، و سایر اجازه‌های مرتبط توسط تلفن کنترل می‌شوند و نه دستگاه متصل‌شده (مانند عینک صوتی یا عینک نمایشگردار).

برای درخواست اجازه‌ها، این مراحل را دنبال کنید:

  1. اجازه‌های زیر را در فایل مانیفست برنامه‌تان اعلام کنید:

  2. هر دو اجازه RECORD_AUDIO و BLUETOOTH_CONNECT را در زمان اجرا بااستفاده از جریان اجازه استاندارد Android درخواست کنید.

استفاده از AudioManager برای مسیریابی صدا

پس‌از اینکه کاربر اجازه‌های زمان اجرای لازم را به برنامه شما اعطا کرد، از میانای برنامه‌سازی کاربردی AudioManager برای تنظیم دستگاه ارتباطی روی TYPE_BLUETOOTH_SCO استفاده کنید تا صدا ازطریق Bluetooth HFP مسیریابی شود. این کار به سیستم دستور می‌دهد تا صدا را از دستگاه جانبی بلوتوث بازیابی کند.

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

با دوربین عینک عکس بگیرید

برای گرفتن تصویر با دوربین عینک، CameraX را راه‌اندازی و متصل کنید ImageCapture مورد استفاده به دوربین عینک بااستفاده از زمینه صحیح برای برنامه‌تان:

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

نکات کلیدی درباره کد

  • نمونه‌ای از 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 تصویر بگیرید. برای آشنایی با نحوه استفاده از takePicture برای گرفتن عکس، به اسناد CameraX مراجعه کنید.

ضبط کردن جاری‌سازی با دوربین عینک

برای ضبط کردن جاری‌سازی‌ها به‌جای تصویر، مؤلفه ImageCapture را از نمونه ضبط تصویر با مؤلفه‌های VideoCapture یا ImageAnalysis جایگزین کنید و منطق ضبط را تغییر دهید.

برای کسب اطلاعات بیشتر درباره این میاناهای برنامه‌سازی کاربردی و نحوه استفاده از آن‌ها، به مستندات ضبط ویدیو CameraX و تحلیل تصویر CameraX مراجعه کنید.

وضوح و نرخ فریم را انتخاب کنید

باید دستگاه را جستجو کنید تا وضوح‌های پشتیبانی‌شده و حداکثر نرخ فریم مربوط به آن‌ها را پیدا کنید. جدول زیر نمونه‌هایی از مقادیری را که ممکن است بسته به مورد استفاده برنامه‌تان ببینید فهرست می‌کند. برای صرفه‌جویی در مصرف باتری و کاهش گرمای عینک، پایین‌ترین وضوح و نرخ فریمی را که برای مورد استفاده شما مناسب است انتخاب کنید.

مورد استفاده برطرف کردن مشکل نرخ فریم
تشخیص نوشتار (OCR) یا حس کردن جهان ۱۰۸۰ × ۱۹۲۰ ‫۱ فریم در ثانیه
جاری‌سازی ویدیویی هوش مصنوعی (دستیارهای چندحالتی) ۱۰۲۴ × ۷۶۸ ‫۱ فریم در ثانیه
بومی‌سازی و موقعیت‌یابی بصری (VPS) ‫۶۴۰ × ۳۶۰ یا ۶۴۰ × ۴۸۰ ‫۱۰ فریم در ثانیه
تماس تصویری ‫۶۴۰ × ۳۶۰ یا ۶۴۰ × ۴۸۰ ‫۲۴ فریم در ثانیه

پرس‌وجو کردن حداکثر نرخ فریم برای هر وضوح

CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES محدوده نرخ فریم کلی دستگاه را گزارش می‌کند که می‌تواند بالاتر از نرخ جاری‌سازی‌ای باشد که وضوح خاصی پشتیبانی می‌کند. برای دریافت حداکثر واقعی برای هر وضوح در عینک صوتی و نمایشگردار، حداقل مدت فریم آن را بخوانید و آن را با StreamConfigurationMap.getOutputMinFrameDuration(format, size) به FPS تبدیل کنید:

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

سپس، از آن مقدار برای محاسبه حدود minFps و maxFps برای محدوده نرخ فریم هدف استفاده کنید. کران پایین (minFps) را در حداکثر نرخ فریم جاری‌سازی وضوح که از getOutputMinFrameDuration محاسبه شده است محدود کنید و کران بالای بالاتری (maxFps) را به‌عنوان پنجره جبران تنظیم کنید:

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

نکات کلیدی درباره کد

  • از قالبی استفاده کنید که با مورد استفاده شما مطابقت داشته باشد. برای ImageAnalysis، YUV_420_888 است.
  • برای VideoCapture، به‌جای آن از سربارهای Class استفاده کنید: getOutputSizes(MediaRecorder::class.java) و getOutputMinFrameDuration(MediaRecorder::class.java, size).
  • محدوده‌ای را مشخص کنید که در آن maxFps بزرگ‌تر از minFps باشد (مانند minFps*2) به‌جای اینکه هر دو حد را روی مقدار یکسانی مانند Range(minFps, minFps) تنظیم کنید. کران پایین (minFps) نرخ فریم جاری‌سازی هدف را تنظیم می‌کند و باید حداکثر مقدار محاسبه‌شده از getOutputMinFrameDuration باشد، درحالی‌که کران بالا (maxFps) به‌عنوان پنجره جبران برای فریم‌های تأخیری عمل می‌کند تا از تأخیر مداوم جلوگیری کند. برای مثال، [1,2] یا [1,3] برای جاری‌سازی ۱ فریم در ثانیه، و [24,30] یا [24,48] برای جاری‌سازی ۲۴ فریم در ثانیه.

پیکربندی نرخ فریم

برای پیکربندی محدوده نرخ فریم براساس مورد استفاده CameraX:

// 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()

این مثال پیکربندی را برای هر دو VideoCapture و ImageAnalysis نشان می‌دهد، اما می‌توانید هرکدام از موارد استفاده را که برنامه‌تان پشتیبانی می‌کند پیاده‌سازی کنید.

درک نحوه رفتار جاری‌سازی

هنگام کار با جریان‌های ضبط، رفتارهای سیستم زیر را به‌خاطر داشته باشید:

  • وضوح‌های پشتیبانی‌نشده رد می‌شوند: اگر اندازه‌ای را درخواست کنید که در فهرست نباشد، Camera2 پیکربندی را قبل‌از باز شدن جاری‌سازی رد می‌کند.
  • درخواست نرخ فریم بالاتر از حداکثر: اگر حداقل نرخ فریم را بالاتر از حداکثر نرخ فریم پشتیبانی‌شده برای وضوح خود درخواست کنید، جلسه همچنان باز می‌شود، اما نرخ فریم هدف از عینک به آن حداکثر پشتیبانی‌شده محدود می‌شود، درحالی‌که حد بالا همچنان به‌عنوان حد نرخ جبرانی عمل می‌کند. برای مثال، درخواست [۳۰،۶۰] در ۶۴۰ × ۴۸۰ نرخ فریم هدف را به ۲۴ فریم در ثانیه محدود می‌کند (مانند [۲۴،۶۰] رفتار می‌کند).
  • دریچه حرارتی: وقتی عینک گرم می‌شود، نرخ فریم کاهش می‌یابد اما وضوح ثابت می‌ماند. برنامه‌تان را طوری بسازید که بتواند نرخ فریم پایین‌تر یا نامنظم را مدیریت کند، برای مثال با بررسی مُهرهای زمان فریم به‌جای فرض کردن نرخ ثابت.

دسترسی به سخت‌افزار تلفن از فعالیت فرافکنی‌شده

فعالیت فرافکنی‌شده همچنین می‌تواند بااستفاده از 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 استفاده نکنید زیرا اگر فعالیت پیش‌بینی‌شده آخرین مؤلفه راه‌اندازی‌شده باشد، بافت برنامه می‌تواند بافت عینک را به‌اشتباه برگرداند.