2026 में CameraX: कैमरा सेशन लीक किए बिना Preview और ImageAnalysis को बाइंड करना
Android पर CameraX की व्यावहारिक गाइड: Preview और ImageAnalysis को lifecycle से बाइंड करना, सही backpressure रणनीति, और रोटेशन से होने वाली क्रैश।
मेरी दो ऐप्स कैमरे को किसी चीज़ पर पॉइंट करती हैं और एक सेकंड से भी कम समय में जवाब की उम्मीद करती हैं: Stocky बारकोड स्कैन करते हुए, Subly बिल पढ़ते हुए। दोनों के पीछे के विज़न मॉडल — ML Kit का बारकोड स्कैनर और उसका टेक्स्ट रिकग्नाइज़र — कहीं और अपने खुद के लेखों में कवर हो चुके हैं। यह लेख दोनों के नीचे की परत के बारे में है: CameraX, वह हिस्सा जिसे तब तक बिल्कुल उतना ही खुला रहना चाहिए जब तक प्रीव्यू दिखाने वाली स्क्रीन जीवित है, एक फ्रेम भी ज़्यादा नहीं।
CameraX को चलाना आसान है और इसे सूक्ष्म रूप से गलत करना भी उतना ही आसान है। डेमो कोड कुछ ही लाइनों में एक प्रीव्यू और एक एनालाइज़र को बाइंड कर देता है और पहली बार चलाने पर काम भी करता है। बग बाद में सामने आते हैं — रोटेशन पर, स्क्रीनों के बीच तेज़ी से आगे-पीछे होने पर, ऐसे फोन पर जो स्कैन के बीच में ऐप को पॉज़ कर देता है। यहाँ वह है जो वास्तव में सही होना चाहिए।
एक activity को नहीं, एक lifecycle को बाइंड करना
CameraX को कच्चे Camera2 पर इस्तेमाल करने की वजह इसका API सरफेस नहीं, बल्कि bindToLifecycle है। अपने use case के साथ इसे एक LifecycleOwner दें, और CameraX कैमरा तभी शुरू करता है जब वह owner STARTED तक पहुँचता है, और जब नहीं पहुँचता तो उसे बंद कर देता है — कोई मैन्युअल onPause/onResume कैमरा वायरिंग नहीं जो गलत हो सके:
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
cameraProvider.unbindAll()
val preview = Preview.Builder().build().also {
it.surfaceProvider = previewView.surfaceProvider
}
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(analysisExecutor, ::analyzeFrame) }
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis,
)
}, ContextCompat.getMainExecutor(context))
बाइंड करने से पहले वह unbindAll() कोई सुरक्षा के लिए लिखा गया अतिरिक्त कोड नहीं है — यह सबसे आम CameraX क्रैश का समाधान है। ProcessCameraProvider.getInstance() एक ListenableFuture लौटाता है, इसलिए बाइंडिंग असिंक्रोनस रूप से, एक लिसनर पर होती है। अगर पुराना सेशन साफ़ तरीके से रिलीज़ होने से पहले स्क्रीन फिर से बन जाती है (रोटेशन, एक कॉन्फ़िग बदलाव, पहले से बाइंड हो चुकी स्कैन स्क्रीन पर वापस नेविगेट करना), तो आपको एक ही कैमरे के लिए लड़ते हुए दो use case सेट मिल जाते हैं, या एक IllegalStateException मिलती है जो बताती है कि एक use case पहले से किसी और lifecycle से बाइंड है। पहले unbindAll() कॉल करना हर बाइंड को इडमपोटेंट बना देता है: इससे फर्क नहीं पड़ता कि आप onResume से कितनी बार गुज़रे हैं।
प्रीव्यू और एनालिसिस, एक ही फ्रेम्स के दो उपभोक्ता हैं
Preview और ImageAnalysis किसी पाइपलाइन के चरण नहीं हैं — ये दो स्वतंत्र use case हैं जिन्हें कैमरा एक साथ फीड करता है। प्रीव्यू यूज़र को एक PreviewView पर लाइव फीड दिखाता है; एनालाइज़र को हर फ्रेम पर अपना खुद का ImageProxy मिलता है जिस पर इन्फरेंस चलानी होती है। दोनों को साथ बाइंड करना ही वह चीज़ है जो “लाइव स्कैनिंग ओवरले वाला कैमरा” को दो कैमरों की बजाय एक ही कॉल बना देता है। इसका यह भी मतलब है कि एनालाइज़र की स्पीड स्क्रीन पर यूज़र जो देखता है उसे प्रभावित नहीं करती — एक धीमा मॉडल प्रीव्यू को लैगी नहीं बनाता, और यह बात जितनी मामूली लगती है, स्कैनर के “रिस्पॉन्सिव” महसूस होने के लिए उससे कहीं ज़्यादा मायने रखती है।
backpressure रणनीति कोई मामूली सेटिंग नहीं है
नए CameraX रिलीज़ में ImageAnalysis डिफ़ॉल्ट रूप से STRATEGY_KEEP_ONLY_LATEST इस्तेमाल करता है, लेकिन इसे स्पष्ट रूप से सेट करना फायदेमंद है, क्योंकि विकल्प — STRATEGY_BLOCK_PRODUCER — जब आपका एनालाइज़र तेज़ी से काम नहीं कर पाता तो फ्रेम्स को कतार में डाल देता है, और एक बारकोड या OCR मॉडल शायद ही कभी 30fps वाले कैमरे के साथ बना रह पाता है। KEEP_ONLY_LATEST एनालाइज़र के व्यस्त रहने के दौरान सबसे नए फ्रेम को छोड़कर बाकी सभी को हटा देता है, जो बिल्कुल वही व्यवहार है जो आप चाहते हैं: यूज़र को लाइव प्रीव्यू दिखता है और उसे उस फ्रेम से नतीजा मिलता है जिसे मॉडल देखने के लिए फ्री था, न कि पाइपलाइन के पीछे रह जाने और एक सेकंड पुराने फ्रेम्स को प्रोसेस करने से।
हर ImageProxy को बंद करें, वरना फीड चुपचाप रुक जाती है
टेस्टिंग में इस बग को पहचानना सबसे मुश्किल है क्योंकि यह क्रैश नहीं होता — यह बस रुक जाता है। ImageAnalysis, KEEP_ONLY_LATEST के तहत भी, तब तक आपके एनालाइज़र को नया फ्रेम नहीं देगा जब तक पिछला ImageProxy बंद नहीं हो जाता। किसी null इमेज पर एक जल्दी return, आपके finally से पहले फेंका गया एक exception, या ऐसा लुकअप जो असिंक्रोनस रूप से चलता है और proxy को केवल अपने कॉलबैक में बंद करता है — इनमें से कोई भी एक ImageProxy को हमेशा के लिए खुला छोड़ सकता है, और उस फ्रेम के बाद से, एनालाइज़र बस चुप हो जाता है। कोई एरर नहीं, कोई लॉग नहीं, बस एक स्कैनर जिसने स्कैन करना बंद कर दिया:
fun analyzeFrame(image: ImageProxy) {
try {
val media = image.image ?: return
val input = InputImage.fromMediaImage(media, image.imageInfo.rotationDegrees)
scanner.process(input).addOnSuccessListener { /* handle result */ }
} finally {
image.close()
}
}
क्लोज़ को happy path के आख़िर में नहीं, एक finally में रखें। पूरे सेटअप में यही एक लाइन है जिसे छोड़ना आसान है और बाद में डीबग करना महंगा।
बाइंड करने से पहले परमिशन चेक करें, कॉलबैक के अंदर नहीं
CameraX आपकी ओर से कैमरा परमिशन नहीं माँगता, और बिना परमिशन के बाइंड करने पर यह शालीनता से फेल भी नहीं होता — अगर CAMERA अभी तक ग्रांट नहीं हुई है तो bindToLifecycle एक SecurityException फेंकता है जो स्क्रीन को क्रैश कर देगी। समाधान क्रम का है: ContextCompat.checkSelfPermission चेक करें और ProcessCameraProvider का लिसनर चलने से पहले ActivityResultContracts.RequestPermission से गुज़रें, उसके अंदर नहीं। एक परमिशन चेक जो सिर्फ यूज़र के एक बार पहले ही मना कर देने के बाद चलता है, या जो असिंक्रोनस प्रोवाइडर फ्यूचर के साथ रेस करता है, यही वजह है कि आपको ऐसा क्रैश रिपोर्ट मिलता है जो सिर्फ एक नई इंस्टॉल पर ही दोबारा होता है।
निष्कर्ष
CameraX कच्चे Camera2 पर अपनी जगह ठीक एक ही तरीके से कमाता है: यह “कैमरा lifecycle मैनेजमेंट” को एक सुलझी हुई समस्या बना देता है, बशर्ते आप वाकई इसे उस lifecycle का मालिक बनने दें। एक LifecycleOwner के ज़रिए बाइंड करें, हर बाइंड से पहले unbindAll() कॉल करें ताकि रोटेशन और नेविगेशन कभी किसी use case को दो बार बाइंड न कर पाएँ, डिफ़ॉल्ट पर भरोसा करने की बजाय backpressure रणनीति स्पष्ट रूप से सेट करें, और हर ImageProxy को एक finally में बंद करें। इन चार चीज़ों को सही कर लें और कैमरा उबाऊ बन जाता है — और एक ऐसे फ़ीचर के लिए जिसे हर बार काम करना है जब यूज़र अपना फोन किसी बारकोड या बिल की तरफ़ पॉइंट करता है, यही ठीक वह चीज़ है जो आप चाहते हैं।
// संबंधित पठन
जर्नल से और भी
2026 में androidx.startup: ContentProvider के ढेर के बिना लाइब्रेरी इनिशियलाइज़ेशन का क्रम तय करना
Android की App Startup लाइब्रेरी के लिए एक व्यावहारिक गाइड — initializer को एक ContentProvider में समेटना, dependencies घोषित करना, और वो lazy-init मामले जिनकी जगह यह नहीं ले सकती।
2026 में Android का इन-ऐप अपडेट API: फ़्लेक्सिबल बनाम इमीडिएट, और कब किसे इस्तेमाल करें
Google के इन-ऐप अपडेट API की व्यावहारिक गाइड — फ़्लेक्सिबल बनाम इमीडिएट फ़्लो, अपडेट प्रायोरिटी, स्टेलनेस डेज़, और वह रिज़्यूम चेक जिसे हर कोई भूल जाता है।
2026 में Android Photo Picker API: पूरी गैलरी की अनुमति दिए बिना फ़ोटो अटैच करना
Android के Photo Picker API की व्यावहारिक गाइड — सिंगल और मल्टीपल सिलेक्शन, MIME फ़िल्टरिंग, Android 13 से पहले के लिए बैकपोर्ट, और यह READ_MEDIA_IMAGES से बेहतर क्यों है।