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