Setelah Anda meminta dan diberi izin yang diperlukan, aplikasi Anda dapat mengakses hardware di kacamata pintar audio atau kacamata pintar dengan layar. Kunci untuk mengakses hardware kacamata (bukan hardware ponsel) adalah menggunakan konteks yang diproyeksikan.
Ada dua cara utama untuk mendapatkan konteks yang diproyeksikan, bergantung pada tempat kode Anda dieksekusi:
Mendapatkan konteks yang diproyeksikan jika kode Anda berjalan dalam aktivitas yang diproyeksikan
Jika kode aplikasi Anda berjalan dari dalam aktivitas yang diproyeksikan, konteks aktivitasnya sudah menjadi konteks yang diproyeksikan. Dalam skenario ini, panggilan yang dilakukan dalam aktivitas tersebut sudah dapat mengakses hardware kacamata.
Mendapatkan konteks yang diproyeksikan untuk kode yang berjalan di komponen aplikasi Telepon
Jika bagian dari aplikasi Anda di luar aktivitas yang diproyeksikan (seperti aktivitas
ponsel atau layanan) perlu mengakses hardware kacamata, bagian tersebut harus secara eksplisit
mendapatkan konteks yang diproyeksikan. Untuk melakukannya, gunakan
metode 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...") } } } }
Memeriksa validitas
Gabungkan panggilan createProjectedDeviceContext dalam
ProjectedContext.isProjectedDeviceConnected. Meskipun metode ini menampilkan
true, konteks yang diproyeksikan tetap valid untuk perangkat terhubung, dan aktivitas atau layanan aplikasi Telepon Anda (seperti CameraManager) dapat mengakses hardware kacamata.
Pembersihan saat koneksi terputus
Konteks yang diproyeksikan terkait dengan siklus proses perangkat terhubung, sehingga konteks tersebut akan dihancurkan saat perangkat terputus. Saat perangkat terputus,
ProjectedContext.isProjectedDeviceConnected akan menampilkan false. Aplikasi Anda
harus memproses perubahan ini dan membersihkan layanan sistem (seperti
CameraManager) atau resource yang dibuat aplikasi Anda menggunakan konteks
yang diproyeksikan tersebut.
Lakukan inisialisasi ulang saat terhubung kembali
Saat kacamata terhubung kembali, aplikasi Anda dapat memperoleh instance konteks yang diproyeksikan
lainnya menggunakan createProjectedDeviceContext, lalu melakukan inisialisasi ulang
layanan atau resource sistem apa pun menggunakan konteks yang diproyeksikan baru.
Merekam audio dengan mikrofon kacamata
Anda dapat merekam audio dari kacamata menggunakan Profil Handsfree (HFP) Bluetooth. Dalam mode ini, kacamata terhubung ke ponsel menggunakan profil Headset dan Advanced Audio Distribution Profile (A2DP) standar, yang berfungsi seperti periferal Bluetooth biasa. Jika aplikasi Anda sudah dirancang untuk perekaman Bluetooth standar, Anda dapat menggunakan metode ini untuk merekam audio dari kacamata tanpa pengembangan tambahan.
Merekam audio menggunakan HFP Bluetooth
Untuk merekam audio menggunakan Bluetooth HFP, pertama-tama minta izin runtime
yang diperlukan, lalu rekam audio menggunakan AudioManager API, seperti
yang dijelaskan di bagian berikut.
Meminta izin
Seperti perangkat audio Bluetooth standar, RECORD_AUDIO, BLUETOOTH_CONNECT, dan izin terkait lainnya dikontrol oleh ponsel, bukan perangkat terhubung (seperti kacamata pintar audio atau kacamata pintar dengan layar).
Ikuti langkah-langkah berikut untuk meminta izin:
Deklarasikan izin berikut dalam file manifes aplikasi Anda:
Minta izin
RECORD_AUDIOdanBLUETOOTH_CONNECTsaat runtime menggunakan alur izin Android standar.
Menggunakan AudioManager untuk mengarahkan audio
Setelah pengguna memberikan izin runtime yang diperlukan ke aplikasi Anda, gunakan
API AudioManager untuk menyetel perangkat komunikasi ke
TYPE_BLUETOOTH_SCO guna merutekan audio melalui Bluetooth HFP. Tindakan ini
mengarahkan sistem untuk mengambil audio dari periferal Bluetooth.
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() }
Mengambil gambar dengan kamera kacamata pintar
Untuk mengambil gambar dengan kamera kacamata, siapkan dan ikat ImageCapture kasus penggunaan CameraX ke kamera kacamata menggunakan konteks yang benar untuk aplikasi Anda:
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)) } } } }
Poin penting tentang kode
- Mendapatkan instance
ProcessCameraProvidermenggunakan konteks perangkat yang diproyeksikan. - Dalam cakupan konteks yang diproyeksikan, kamera utama kacamata yang mengarah ke luar
dipetakan ke
DEFAULT_BACK_CAMERAsaat memilih kamera. - Pemeriksaan pra-pengikatan menggunakan
cameraProvider.hasCamera(cameraSelector)untuk memverifikasi bahwa kamera yang dipilih tersedia di perangkat sebelum melanjutkan. - Menggunakan Camera2 Interop dengan
Camera2CameraInfountuk membacaCameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAPyang mendasarinya, yang dapat berguna untuk pemeriksaan lanjutan pada resolusi yang didukung. ResolutionSelectorkustom dibuat untuk mengontrol resolusi gambar output secara tepat untukImageCapture.- Membuat kasus penggunaan
ImageCaptureyang dikonfigurasi denganResolutionSelectorkustom. - Mengikat kasus penggunaan
ImageCaptureke siklus proses aktivitas. Tindakan ini akan otomatis mengelola pembukaan dan penutupan kamera berdasarkan status aktivitas (misalnya, menghentikan kamera saat aktivitas dijeda). - Mengonfigurasi untuk hardware streaming tunggal: Pipeline kamera kacamata
dibatasi untuk satu streaming aktif dalam satu waktu. Saat
ImageCaptureterikat sendiri, CameraX akan melampirkan streaming berulang internal (MeteringRepeating) untuk fokus dan pengukuran secara default, yang menyebabkan pipeline streaming tunggal terhenti. Untuk mengikatImageCapturesendiri tanpa streaming pratinjau, nonaktifkan streaming pengulangan paksa dengan menetapkansetRepeatingStreamForced(false)diCameraXConfig.Builder(misalnya, dengan menerapkanCameraXConfig.Providerdi classApplicationatau mengonfigurasiProcessCameraProvidersebelum inisialisasi).
Setelah kamera kacamata disiapkan, Anda dapat mengambil gambar dengan class
ImageCapture CameraX. Lihat dokumentasi CameraX untuk mempelajari cara menggunakan
takePicture untuk mengambil gambar.
Merekam streaming dengan kamera kacamata pintar
Untuk mengambil streaming, bukan gambar, ganti komponen ImageCapture
dari contoh pengambilan gambar dengan komponen VideoCapture atau
ImageAnalysis dan ubah logika pengambilan.
Untuk mengetahui informasi selengkapnya tentang API ini dan cara menggunakannya, lihat dokumentasi perekaman video CameraX dan analisis gambar CameraX.
Memilih resolusi dan kecepatan frame
Anda harus mengkueri perangkat untuk menemukan resolusi yang didukung dan kecepatan frame maksimum yang sesuai. Tabel berikut mencantumkan contoh nilai yang mungkin Anda lihat, bergantung pada kasus penggunaan aplikasi Anda. Untuk menghemat daya baterai dan mengurangi panas pada kacamata, pilih resolusi dan kecepatan frame terendah yang sesuai untuk kasus penggunaan Anda.
| Kasus penggunaan | Resolusi | Kecepatan frame |
|---|---|---|
| Pengenalan teks (OCR) atau sensor dunia | 1920 x 1080 | 1 FPS |
| Streaming video AI (asisten multimodal) | 1024 x 768 | 1 FPS |
| Pelokalan dan pemosisian visual (VPS) | 640x360 atau 640x480 | 10 FPS |
| Panggilan video | 640x360 atau 640x480 | 24 FPS |
Membuat kueri kecepatan frame maksimum untuk setiap resolusi
CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES melaporkan batas kecepatan frame perangkat secara keseluruhan, yang dapat lebih tinggi daripada kecepatan streaming yang didukung resolusi tertentu. Untuk mendapatkan nilai maksimum sebenarnya untuk setiap resolusi pada kacamata pintar dengan layar dan audio, baca durasi frame minimumnya dan konversikan ke FPS dengan StreamConfigurationMap.getOutputMinFrameDuration(format, size):
@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 } }
Selanjutnya, gunakan nilai tersebut untuk menghitung batas minFps dan maxFps untuk
rentang kecepatan frame target Anda. Batasi batas bawah (minFps) pada kecepatan frame streaming maksimum resolusi yang dihitung dari getOutputMinFrameDuration, dan tetapkan batas atas yang lebih tinggi (maxFps) sebagai jendela catch-up:
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
Poin penting tentang kode
- Gunakan format yang sesuai dengan kasus penggunaan Anda. Untuk
ImageAnalysis, harganyaYUV_420_888. - Untuk
VideoCapture, gunakan overloadClasssebagai gantinya:getOutputSizes(MediaRecorder::class.java)dangetOutputMinFrameDuration(MediaRecorder::class.java, size). - Tentukan rentang dengan
maxFpslebih besar dariminFps(sepertiminFps*2), bukan menetapkan kedua batas ke nilai yang sama sepertiRange(minFps, minFps). Batas bawah (minFps) menetapkan target kecepatan frame streaming dan harus maksimal sebesar nilai yang dihitung darigetOutputMinFrameDuration, sementara batas atas (maxFps) bertindak sebagai jendela catch-up untuk frame yang tertunda guna mencegah latensi persisten. Misalnya, [1,2] atau [1,3] untuk streaming 1 FPS, dan [24,30] atau [24,48] untuk streaming 24 FPS.
Mengonfigurasi kecepatan frame
Untuk mengonfigurasi rentang kecepatan frame berdasarkan kasus penggunaan CameraX Anda:
// 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()
Contoh ini menunjukkan konfigurasi untuk VideoCapture dan ImageAnalysis,
tetapi Anda dapat menerapkan kasus penggunaan apa pun yang didukung aplikasi Anda.
Memahami perilaku streaming
Perhatikan perilaku sistem berikut saat menggunakan streaming perekaman:
- Resolusi yang tidak didukung akan ditolak: Jika Anda meminta ukuran yang tidak ada dalam daftar, Camera2 akan menolak konfigurasi sebelum streaming dibuka.
- Meminta kecepatan frame di atas batas maksimum: Jika Anda meminta kecepatan frame minimum di atas batas maksimum yang didukung untuk resolusi Anda, sesi akan tetap terbuka, tetapi kecepatan frame target dari kacamata akan dibatasi ke batas maksimum yang didukung, sementara batas atas akan terus bertindak sebagai batas kecepatan catch-up. Misalnya, meminta [30,60] pada 640 x 480 akan membatasi kecepatan frame target ke 24 FPS (berperilaku seperti [24,60]).
- Throttling termal: Saat kacamata pintar menjadi hangat, kecepatan frame akan turun, tetapi resolusinya tetap sama. Bangun aplikasi Anda untuk menangani kecepatan frame yang lebih rendah atau tidak merata, misalnya dengan memeriksa stempel waktu frame, bukan mengasumsikan kecepatan tetap.
Mengakses hardware ponsel dari aktivitas yang diproyeksikan
Aktivitas yang diproyeksikan juga dapat mengakses hardware ponsel (seperti
kamera atau mikrofon) menggunakan createHostDeviceContext(context) untuk mendapatkan
konteks perangkat host (ponsel):
@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 } }
Saat mengakses hardware atau resource yang khusus untuk perangkat host (ponsel) di aplikasi hybrid (aplikasi yang berisi pengalaman seluler dan kacamata), Anda harus secara eksplisit memilih konteks yang benar untuk memastikan aplikasi dapat mengakses hardware yang benar:
- Gunakan konteks
Activitydari ponselActivityatauProjectedContext.createHostDeviceContextuntuk mendapatkan konteks ponsel. - Jangan gunakan
getApplicationContextkarena konteks aplikasi dapat menampilkan konteks kacamata secara tidak benar jika aktivitas yang diproyeksikan adalah komponen yang paling baru diluncurkan.