استخدام سياق مُسقَط للوصول إلى الأجهزة على نظّارة الصوت والنظّارة المزودة بشاشة عرض

أجهزة XR المشمولة
تساعدك هذه الإرشادات في إنشاء تجارب لهذه الأنواع من أجهزة 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)

اختيار طريقة تسجيل

تعتمد الطريقة التي تختارها على ما إذا كنت بحاجة إلى معالجة صوتية عالية الدقة خاصة بالواقع المُمتد أو إدخال صوتي عادي عبر البلوتوث.

طريقة التسجيل الوصول إلى الميكروفون حالة الاستخدام الشائعة

السياق المُسقط

ميكروفونات متعددة

يتيح لك التسجيل باستخدام سياق مُسقط وصول تطبيقك إلى ميكروفونات متعددة من النظارات وميزات الأجهزة المتخصصة، مثل:

  • التوزيع المكاني الخاص بالواقع المُمتد
  • إلغاء الضجيج المتقدّم
  • فصل الصوت الذي يعزل صوت الشخص الذي يرتدي النظارات
  • الحفاظ على إذن الوصول إلى التسجيل في بيئات متعددة الأجهزة حتى عندما لا تكون النظارات هي جهاز البلوتوث النشط

ملف تعريف البلوتوث بدون لمس الجهاز

ميكروفون واحد

يعتمد على ملف تعريف البلوتوث بدون لمس الجهاز (HFP) لتحقيق التوافق الفوري الجاهز للاستخدام. في هذا الوضع، تتصل النظارات بالهاتف باستخدام ملفات تعريف سماعة الرأس القياسية وملف تعريف توزيع الصوت المتقدّم (A2DP) profiles، وتعمل مثل جهاز طرفي عادي يتضمّن بلوتوث.

إذا كان تطبيقك مصمّمًا أصلاً للتسجيل العادي عبر البلوتوث، يمكنك استخدام هذه الطريقة لتسجيل الصوت من النظارات بدون دمج أي إمكانات خاصة بالواقع المُمتد.

تسجيل الصوت باستخدام سياق مُسقط

لتسجيل الصوت باستخدام سياق مُسقط، اطلب أولاً أذونات وقت التشغيل المطلوبة ، ثم سجِّل الصوت باستخدام واجهة برمجة التطبيقات AudioRecord، كما هو موضّح في الأقسام التالية.

طلب أذونات وقت التشغيل

للوصول إلى ميكروفونات متعددة على النظارات، يجب طلب أذونات الصوت خصيصًا للجهاز المُسقط. لا يكفي الإذن العادي RECORD_AUDIO الذي منحه المستخدم لتطبيقك على جهازه الجوّال والذي ينطبق على الهاتف فقط.

اتّبِع الخطوات التالية لطلب الأذونات:

  1. أدرِج الإذن RECORD_AUDIO في ملف بيان تطبيقك.
  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 معدّل عيّنات يبلغ 16 كيلوهرتز وضبط قناة أحادية أو استريو (باستخدام CHANNEL_IN_MONO أو CHANNEL_IN_STEREO).

  • استخدِم AudioRecord.getMinBufferSize() لتحديد الحد الأدنى لحجم المخزن المؤقت لإنشاء عنصر AudioRecord. ومع ذلك، لمنع انقطاع الصوت من النظارات، عليك القراءة من هذا المخزن المؤقت في أجزاء قصيرة ومتكررة (مثالية 20 ملي ثانية) بدلاً من الانتظار حتى يتم ملء المخزن المؤقت بالكامل.

التنظيف بعد الاستخدام

عندما لا يعود تطبيقك بحاجة إلى الميكروفون أو عند إيقاف النشاط، استخدِم stop و release على عنصر AudioRecord.

التحقّق من أذونات وقت التشغيل قبل التسجيل

قبل استخدام startRecording، تأكَّد من أنّ المستخدم قد منح إذن الميكروفون للنظارات باستخدام السياق المُسقط.

تسجيل الصوت باستخدام ملف تعريف البلوتوث بدون لمس الجهاز

لتسجيل الصوت باستخدام ملف تعريف البلوتوث بدون لمس الجهاز، اطلب أولاً أذونات وقت التشغيل المطلوبة ، ثم سجِّل الصوت باستخدام واجهة برمجة التطبيقات AudioManager، كما هو موضّح في الأقسام التالية.

طلب الحصول على الأذونات

كما هو الحال مع أي جهاز صوتي عادي يتضمّن بلوتوث، يتحكّم الهاتف في RECORD_AUDIO و BLUETOOTH_CONNECT والأذونات الأخرى ذات الصلة، وليس الجهاز المتصل (مثل النظارات الصوتية أو نظارات بشاشة عرض).

اتّبِع الخطوات التالية لطلب الأذونات:

  1. أدرِج الأذونات التالية في ملف بيان تطبيقك:

  2. اطلب الإذنَين RECORD_AUDIO وBLUETOOTH_CONNECT في وقت التشغيل باستخدام مسار أذونات Android العادي.

استخدام AudioManager لتوجيه الصوت

بعد أن يمنح المستخدم تطبيقك أذونات وقت التشغيل اللازمة، استخدِم واجهة برمجة التطبيقات AudioManager لضبط جهاز الاتصال على TYPE_BLUETOOTH_SCO لتوجيه الصوت من خلال ملف تعريف البلوتوث بدون لمس الجهاز. يوجّه هذا الإجراء النظام إلى استرداد الصوت من الجهاز الطرفي الذي يتضمّن بلوتوث.

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

التقاط صورة باستخدام كاميرا النظارات

لالتقاط صورة باستخدام كاميرا النظارات، اضبط واربط حالة استخدام CameraX's ImageCapture بكاميرا النظارات باستخدام السياق الصحيح لتطبيقك:

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 وكيفية استخدامها، اطّلِع على مستندات التقاط الفيديو في CameraX.

يعرض الجدول التالي درجة الدقة وعدد اللقطات في الثانية المقترحتَين حسب حالة استخدام تطبيقك:

حالة الاستخدام درجة الدقة عدد اللقطات في الثانية
الاتصال عبر الفيديو 1280 × 720 15 لقطة في الثانية
الرؤية الحاسوبية 640 × 480 10 لقطات في الثانية
بث الفيديو من إنشاء الذكاء الاصطناعي 640 × 480 لقطة واحدة في الثانية

الوصول إلى أجهزة الهاتف من نشاط مُسقط

يمكن للنشاط المُسقط أيضًا الوصول إلى أجهزة الهاتف (مثل الكاميرا أو الميكروفون) باستخدام 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 لأنّ سياق التطبيق يمكن أن يعرض بشكل غير صحيح سياق النظارات إذا كان النشاط المُسقط هو الـ مكوِّن الذي تم إطلاقه مؤخرًا.