بعد طلب الأذونات اللازمة والحصول عليها، يمكن لتطبيقك الوصول إلى أجهزة نظارات الصوت أو نظارات بشاشة عرض. المفتاح للوصول إلى أجهزة النظارات (بدلاً من أجهزة الهاتف) هو استخدام سياق مُسقط.
هناك طريقتان أساسيتان للحصول على سياق مُسقط، حسب مكان تنفيذ الرمز البرمجي:
الحصول على سياق مُسقط إذا كان الرمز البرمجي قيد التشغيل في نشاط مُسقط
إذا كان الرمز البرمجي لتطبيقك قيد التشغيل من داخل نشاطك المُسقط، يكون سياق النشاط الخاص به سياقًا مُسقطًا. في هذا السيناريو، يمكن للمكالمات التي يتم إجراؤها داخل هذا النشاط الوصول إلى أجهزة النظارات الذكية.
الحصول على سياق مُسقط للرمز البرمجي قيد التشغيل في مكوِّن تطبيق هاتف
إذا كان جزء من تطبيقك خارج نشاطك المُسقط (مثل نشاط هاتف أو خدمة) بحاجة إلى الوصول إلى أجهزة النظارات، يجب أن يحصل صراحةً على سياق مُسقط. لإجراء ذلك، استخدِم طريقة
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 الذي منحه المستخدم لتطبيقك على جهازه الجوّال والذي ينطبق على الهاتف فقط.
اتّبِع الخطوات التالية لطلب الأذونات:
- أدرِج الإذن
RECORD_AUDIOفي ملف بيان تطبيقك. اطلب الأذونات التي تنطبق على الجهاز المُسقط بإحدى الطرق التالية، حسب مكان تنفيذ الرمز البرمجي:
- الرمز البرمجي قيد التشغيل من نشاط مُسقط: استخدِم
ActivityResultLauncherمعProjectedPermissionsResultContract. لمزيد من المعلومات حول استخدام هذه الطريقة، اطّلِع على قسم تسجيل مشغّل الأذونات والأقسام اللاحقة في دليل طلب أذونات الأجهزة. - الرمز البرمجي قيد التشغيل من نشاط هاتف مضيف: استخدِم
Activity#requestPermissions(permissions, requestCode, deviceId)وقدِّم رقم تعريف الجهاز الذي تم الحصول عليه منprojectedDeviceContext، كما هو موضّح في قسم فهم مسار المستخدم لطلب الحصول على إذن في دليل طلب أذونات الأجهزة.
- الرمز البرمجي قيد التشغيل من نشاط مُسقط: استخدِم
تهيئة 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 والأذونات الأخرى ذات الصلة، وليس الجهاز المتصل (مثل النظارات الصوتية أو نظارات بشاشة عرض).
اتّبِع الخطوات التالية لطلب الأذونات:
أدرِج الأذونات التالية في ملف بيان تطبيقك:
اطلب الإذنَين
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لأنّ سياق التطبيق يمكن أن يعرض بشكل غير صحيح سياق النظارات إذا كان النشاط المُسقط هو الـ مكوِّن الذي تم إطلاقه مؤخرًا.