react-native-rectangle-doc-scanner 3.193.0 → 3.195.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,300 +1,384 @@
1
1
  package com.reactnativerectangledocscanner
2
2
 
3
+ import android.Manifest
3
4
  import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
4
8
  import android.graphics.ImageFormat
9
+ import android.graphics.Matrix
10
+ import android.graphics.Rect
11
+ import android.graphics.SurfaceTexture
12
+ import android.graphics.YuvImage
13
+ import android.hardware.camera2.CameraCaptureSession
14
+ import android.hardware.camera2.CameraCharacteristics
15
+ import android.hardware.camera2.CameraDevice
16
+ import android.hardware.camera2.CameraManager
17
+ import android.hardware.camera2.CaptureRequest
18
+ import android.media.Image
19
+ import android.media.ImageReader
20
+ import android.os.Handler
21
+ import android.os.HandlerThread
5
22
  import android.util.Log
6
23
  import android.util.Size
7
24
  import android.view.Surface
8
- import androidx.camera.core.*
9
- import androidx.camera.lifecycle.ProcessCameraProvider
10
- import androidx.camera.view.PreviewView
25
+ import android.view.TextureView
11
26
  import androidx.core.content.ContextCompat
12
- import androidx.lifecycle.Lifecycle
13
27
  import androidx.lifecycle.LifecycleOwner
14
- import androidx.lifecycle.LiveData
15
- import androidx.lifecycle.Observer
28
+ import java.io.ByteArrayOutputStream
16
29
  import java.io.File
17
- import java.util.concurrent.ExecutorService
18
- import java.util.concurrent.Executors
30
+ import java.io.FileOutputStream
31
+ import java.util.concurrent.atomic.AtomicBoolean
19
32
 
20
33
  class CameraController(
21
34
  private val context: Context,
22
35
  private val lifecycleOwner: LifecycleOwner,
23
- private val previewView: PreviewView
36
+ private val previewView: TextureView
24
37
  ) {
25
- private var camera: Camera? = null
26
- private var cameraProvider: ProcessCameraProvider? = null
27
- private var imageCapture: ImageCapture? = null
28
- private var imageAnalysis: ImageAnalysis? = null
29
- private val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
30
-
38
+ private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
39
+ private var cameraDevice: CameraDevice? = null
40
+ private var captureSession: CameraCaptureSession? = null
41
+ private var previewRequestBuilder: CaptureRequest.Builder? = null
42
+ private var imageReader: ImageReader? = null
43
+ private var backgroundThread: HandlerThread? = null
44
+ private var backgroundHandler: Handler? = null
45
+
46
+ private var cameraId: String? = null
47
+ private var sensorOrientation: Int = 0
48
+ private var previewSize: Size? = null
49
+ private var analysisSize: Size? = null
31
50
  private var useFrontCamera = false
32
51
  private var torchEnabled = false
33
52
  private var detectionEnabled = true
34
- private var isCaptureSession = false
35
- private var hasFallbackAttempted = false
36
- private var cameraStateLiveData: LiveData<CameraState>? = null
37
- private var cameraStateObserver: Observer<CameraState>? = null
53
+ private var hasStarted = false
54
+
55
+ private val isOpening = AtomicBoolean(false)
56
+ private val lastFrameLock = Any()
57
+ private var lastFrame: LastFrame? = null
38
58
 
39
59
  var onFrameAnalyzed: ((Rectangle?, Int, Int) -> Unit)? = null
40
60
 
41
61
  companion object {
42
62
  private const val TAG = "CameraController"
63
+ private const val MAX_PREVIEW_WIDTH = 1280
64
+ private const val MAX_PREVIEW_HEIGHT = 720
65
+ }
66
+
67
+ private data class LastFrame(
68
+ val nv21: ByteArray,
69
+ val width: Int,
70
+ val height: Int,
71
+ val rotationDegrees: Int,
72
+ val isFront: Boolean
73
+ )
74
+
75
+ private val textureListener = object : TextureView.SurfaceTextureListener {
76
+ override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
77
+ Log.d(TAG, "[CAMERA2] Texture available: ${width}x${height}")
78
+ createPreviewSession()
79
+ }
80
+
81
+ override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
82
+ Log.d(TAG, "[CAMERA2] Texture size changed: ${width}x${height}")
83
+ }
84
+
85
+ override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
86
+ Log.d(TAG, "[CAMERA2] Texture destroyed")
87
+ return true
88
+ }
89
+
90
+ override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit
43
91
  }
44
92
 
45
- /**
46
- * Start camera with preview and analysis
47
- */
48
93
  fun startCamera(
49
94
  useFrontCam: Boolean = false,
50
95
  enableDetection: Boolean = true
51
96
  ) {
52
97
  Log.d(TAG, "========================================")
53
- Log.d(TAG, "[CAMERA_CONTROLLER] startCamera called")
54
- Log.d(TAG, "[CAMERA_CONTROLLER] useFrontCam: $useFrontCam")
55
- Log.d(TAG, "[CAMERA_CONTROLLER] enableDetection: $enableDetection")
56
- Log.d(TAG, "[CAMERA_CONTROLLER] lifecycleOwner: $lifecycleOwner")
57
- Log.d(TAG, "[CAMERA_CONTROLLER] lifecycleOwner.lifecycle.currentState: ${lifecycleOwner.lifecycle.currentState}")
98
+ Log.d(TAG, "[CAMERA2] startCamera called")
99
+ Log.d(TAG, "[CAMERA2] useFrontCam: $useFrontCam")
100
+ Log.d(TAG, "[CAMERA2] enableDetection: $enableDetection")
101
+ Log.d(TAG, "[CAMERA2] lifecycleOwner: $lifecycleOwner")
58
102
  Log.d(TAG, "========================================")
59
103
 
60
104
  this.useFrontCamera = useFrontCam
61
105
  this.detectionEnabled = enableDetection
62
106
 
63
- Log.d(TAG, "[CAMERA_CONTROLLER] Getting ProcessCameraProvider instance...")
64
- val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
107
+ if (hasStarted) {
108
+ Log.d(TAG, "[CAMERA2] Already started, skipping")
109
+ return
110
+ }
111
+ hasStarted = true
65
112
 
66
- cameraProviderFuture.addListener({
67
- try {
68
- Log.d(TAG, "[CAMERA_CONTROLLER] ProcessCameraProvider future resolved")
69
- cameraProvider = cameraProviderFuture.get()
70
- Log.d(TAG, "[CAMERA_CONTROLLER] Got cameraProvider: $cameraProvider")
71
- Log.d(TAG, "[CAMERA_CONTROLLER] Calling bindCameraUseCases...")
72
- // Bind preview + analysis only. ImageCapture is bound lazily during capture
73
- // to avoid stream configuration timeouts on some devices.
74
- bindCameraUseCases(enableDetection, useImageCapture = false)
75
- } catch (e: Exception) {
76
- Log.e(TAG, "[CAMERA_CONTROLLER] Failed to start camera", e)
77
- e.printStackTrace()
78
- }
79
- }, ContextCompat.getMainExecutor(context))
113
+ if (!hasCameraPermission()) {
114
+ Log.e(TAG, "[CAMERA2] Camera permission not granted")
115
+ return
116
+ }
117
+
118
+ startBackgroundThread()
119
+ chooseCamera()
120
+
121
+ if (previewView.isAvailable) {
122
+ openCamera()
123
+ } else {
124
+ previewView.surfaceTextureListener = textureListener
125
+ }
80
126
  }
81
127
 
82
- /**
83
- * Stop camera and release resources
84
- */
85
128
  fun stopCamera() {
86
- cameraProvider?.unbindAll()
87
- camera = null
129
+ Log.d(TAG, "[CAMERA2] stopCamera called")
130
+ try {
131
+ captureSession?.close()
132
+ captureSession = null
133
+ } catch (e: Exception) {
134
+ Log.w(TAG, "[CAMERA2] Failed to close session", e)
135
+ }
136
+ try {
137
+ cameraDevice?.close()
138
+ cameraDevice = null
139
+ } catch (e: Exception) {
140
+ Log.w(TAG, "[CAMERA2] Failed to close camera device", e)
141
+ }
142
+ imageReader?.close()
143
+ imageReader = null
144
+ stopBackgroundThread()
145
+ hasStarted = false
88
146
  }
89
147
 
90
- /**
91
- * Bind camera use cases (preview, capture, analysis)
92
- */
93
- private fun bindCameraUseCases(enableDetection: Boolean, useImageCapture: Boolean) {
94
- Log.d(TAG, "[BIND] bindCameraUseCases called")
95
- Log.d(TAG, "[BIND] enableDetection: $enableDetection")
96
- Log.d(TAG, "[BIND] useImageCapture: $useImageCapture")
97
-
98
- val cameraProvider = cameraProvider
99
- if (cameraProvider == null) {
100
- Log.e(TAG, "[BIND] cameraProvider is null, returning")
148
+ fun capturePhoto(
149
+ outputDirectory: File,
150
+ onImageCaptured: (File) -> Unit,
151
+ onError: (Exception) -> Unit
152
+ ) {
153
+ val frame = synchronized(lastFrameLock) { lastFrame }
154
+ if (frame == null) {
155
+ onError(Exception("No frame available for capture"))
101
156
  return
102
157
  }
103
158
 
104
- // Check lifecycle state
105
- val lifecycle = lifecycleOwner.lifecycle
106
- Log.d(TAG, "[BIND] Lifecycle current state: ${lifecycle.currentState}")
107
- if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
108
- Log.e(TAG, "[BIND] Cannot bind camera - lifecycle is destroyed")
109
- return
159
+ backgroundHandler?.post {
160
+ try {
161
+ val photoFile = File(
162
+ outputDirectory,
163
+ "doc_scan_${System.currentTimeMillis()}.jpg"
164
+ )
165
+
166
+ val jpegBytes = nv21ToJpeg(frame.nv21, frame.width, frame.height, 95)
167
+ val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
168
+ ?: throw IllegalStateException("Failed to decode JPEG")
169
+
170
+ val rotated = rotateAndMirror(bitmap, frame.rotationDegrees, frame.isFront)
171
+ FileOutputStream(photoFile).use { out ->
172
+ rotated.compress(Bitmap.CompressFormat.JPEG, 95, out)
173
+ }
174
+ if (rotated != bitmap) {
175
+ rotated.recycle()
176
+ }
177
+ bitmap.recycle()
178
+
179
+ Log.d(TAG, "[CAMERA2] Photo capture succeeded: ${photoFile.absolutePath}")
180
+ onImageCaptured(photoFile)
181
+ } catch (e: Exception) {
182
+ Log.e(TAG, "[CAMERA2] Photo capture failed", e)
183
+ onError(e)
184
+ }
110
185
  }
186
+ }
111
187
 
112
- // Select camera
113
- val cameraSelector = if (useFrontCamera) {
114
- CameraSelector.DEFAULT_FRONT_CAMERA
115
- } else {
116
- CameraSelector.DEFAULT_BACK_CAMERA
188
+ fun setTorchEnabled(enabled: Boolean) {
189
+ torchEnabled = enabled
190
+ val builder = previewRequestBuilder ?: return
191
+ builder.set(CaptureRequest.FLASH_MODE, if (enabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
192
+ try {
193
+ captureSession?.setRepeatingRequest(builder.build(), null, backgroundHandler)
194
+ } catch (e: Exception) {
195
+ Log.w(TAG, "[CAMERA2] Failed to update torch", e)
117
196
  }
118
- Log.d(TAG, "[BIND] Camera selector: ${if (useFrontCamera) "FRONT" else "BACK"}")
119
-
120
- val targetRotation = previewView.display?.rotation ?: Surface.ROTATION_0
121
-
122
- // Preview use case
123
- Log.d(TAG, "[BIND] Creating Preview use case...")
124
- val preview = Preview.Builder()
125
- // Use a modest fixed size to avoid stream configuration timeouts on some devices.
126
- .setTargetResolution(Size(960, 720))
127
- .setTargetRotation(targetRotation)
128
- .build()
129
- Log.d(TAG, "[BIND] Preview created: $preview")
130
-
131
- // Image capture use case (bound only when capture is requested)
132
- if (useImageCapture) {
133
- Log.d(TAG, "[BIND] Creating ImageCapture use case...")
134
- imageCapture = ImageCapture.Builder()
135
- .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
136
- // Cap resolution to avoid camera session timeouts on lower-end devices.
137
- .setTargetResolution(Size(960, 720))
138
- .setTargetRotation(targetRotation)
139
- .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
140
- .build()
141
- Log.d(TAG, "[BIND] ImageCapture created: $imageCapture")
197
+ }
198
+
199
+ fun switchCamera() {
200
+ useFrontCamera = !useFrontCamera
201
+ stopCamera()
202
+ startCamera(useFrontCamera, detectionEnabled)
203
+ }
204
+
205
+ fun isTorchAvailable(): Boolean {
206
+ val id = cameraId ?: return false
207
+ val characteristics = cameraManager.getCameraCharacteristics(id)
208
+ return characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
209
+ }
210
+
211
+ fun focusAt(x: Float, y: Float) {
212
+ // No-op for now. Camera2 focus metering can be added if needed.
213
+ }
214
+
215
+ fun shutdown() {
216
+ stopCamera()
217
+ }
218
+
219
+ private fun chooseCamera() {
220
+ val lensFacing = if (useFrontCamera) {
221
+ CameraCharacteristics.LENS_FACING_FRONT
142
222
  } else {
143
- imageCapture = null
223
+ CameraCharacteristics.LENS_FACING_BACK
144
224
  }
145
225
 
146
- // Image analysis use case for rectangle detection
147
- imageAnalysis = if (enableDetection) {
148
- Log.d(TAG, "[BIND] Creating ImageAnalysis use case...")
149
- ImageAnalysis.Builder()
150
- // Keep analysis lightweight to prevent session configuration timeouts.
151
- .setTargetResolution(Size(960, 720))
152
- .setTargetRotation(targetRotation)
153
- .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
154
- .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
155
- .build()
156
- .also { analysis ->
157
- analysis.setAnalyzer(cameraExecutor) { imageProxy ->
158
- analyzeFrame(imageProxy)
159
- }
160
- Log.d(TAG, "[BIND] ImageAnalysis created and analyzer set: $analysis")
161
- }
162
- } else {
163
- Log.d(TAG, "[BIND] ImageAnalysis disabled")
164
- null
226
+ val ids = cameraManager.cameraIdList
227
+ val selected = ids.firstOrNull { id ->
228
+ val characteristics = cameraManager.getCameraCharacteristics(id)
229
+ characteristics.get(CameraCharacteristics.LENS_FACING) == lensFacing
230
+ } ?: ids.firstOrNull()
231
+
232
+ if (selected == null) {
233
+ Log.e(TAG, "[CAMERA2] No camera available")
234
+ return
165
235
  }
166
236
 
167
- try {
168
- Log.d(TAG, "[BIND] PreviewView: $previewView")
169
- Log.d(TAG, "[BIND] PreviewView.surfaceProvider: ${previewView.surfaceProvider}")
170
- Log.d(TAG, "[BIND] PreviewView attached to window: ${previewView.isAttachedToWindow}")
171
- Log.d(TAG, "[BIND] PreviewView size: ${previewView.width}x${previewView.height}")
172
- Log.d(TAG, "[BIND] PreviewView implementationMode: ${previewView.implementationMode}")
173
-
174
- // Set surface provider FIRST, before binding - this is critical
175
- Log.d(TAG, "[BIND] Setting surface provider BEFORE binding...")
176
- preview.setSurfaceProvider(previewView.surfaceProvider)
177
- Log.d(TAG, "[BIND] Surface provider set successfully")
178
-
179
- // Unbind all use cases before rebinding
180
- Log.d(TAG, "[BIND] Unbinding all existing use cases...")
181
- cameraProvider.unbindAll()
182
-
183
- // Bind use cases to camera
184
- val useCases = mutableListOf<UseCase>(preview)
185
- if (imageCapture != null) {
186
- useCases.add(imageCapture!!)
187
- }
188
- if (imageAnalysis != null) {
189
- useCases.add(imageAnalysis!!)
190
- }
191
- Log.d(TAG, "[BIND] Total use cases to bind: ${useCases.size}")
237
+ cameraId = selected
238
+ val characteristics = cameraManager.getCameraCharacteristics(selected)
239
+ sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
192
240
 
193
- Log.d(TAG, "[BIND] Binding to lifecycle...")
194
- camera = cameraProvider.bindToLifecycle(
195
- lifecycleOwner,
196
- cameraSelector,
197
- *useCases.toTypedArray()
198
- )
199
- Log.d(TAG, "[BIND] Bound to lifecycle successfully, camera: $camera")
200
- registerCameraStateObserver(camera)
241
+ val streamConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
242
+ val previewChoices = streamConfig?.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
243
+ val analysisChoices = streamConfig?.getOutputSizes(ImageFormat.YUV_420_888) ?: emptyArray()
201
244
 
202
- // Restore torch state if it was enabled
203
- if (torchEnabled) {
204
- Log.d(TAG, "[BIND] Restoring torch state...")
205
- setTorchEnabled(true)
206
- }
245
+ previewSize = chooseSize(previewChoices, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
246
+ analysisSize = chooseSize(analysisChoices, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
247
+ Log.d(TAG, "[CAMERA2] Selected sizes - preview: $previewSize, analysis: $analysisSize")
248
+ }
249
+
250
+ private fun openCamera() {
251
+ val id = cameraId ?: run {
252
+ Log.e(TAG, "[CAMERA2] Camera id not set")
253
+ return
254
+ }
255
+ if (isOpening.getAndSet(true)) {
256
+ return
257
+ }
258
+
259
+ try {
260
+ cameraManager.openCamera(id, object : CameraDevice.StateCallback() {
261
+ override fun onOpened(device: CameraDevice) {
262
+ Log.d(TAG, "[CAMERA2] Camera opened")
263
+ isOpening.set(false)
264
+ cameraDevice = device
265
+ createPreviewSession()
266
+ }
207
267
 
208
- Log.d(TAG, "[BIND] ========================================")
209
- Log.d(TAG, "[BIND] Camera started successfully!")
210
- Log.d(TAG, "[BIND] hasFlashUnit: ${camera?.cameraInfo?.hasFlashUnit()}")
211
- Log.d(TAG, "[BIND] ========================================")
212
- isCaptureSession = useImageCapture
268
+ override fun onDisconnected(device: CameraDevice) {
269
+ Log.w(TAG, "[CAMERA2] Camera disconnected")
270
+ isOpening.set(false)
271
+ device.close()
272
+ cameraDevice = null
273
+ }
274
+
275
+ override fun onError(device: CameraDevice, error: Int) {
276
+ Log.e(TAG, "[CAMERA2] Camera error: $error")
277
+ isOpening.set(false)
278
+ device.close()
279
+ cameraDevice = null
280
+ }
281
+ }, backgroundHandler)
282
+ } catch (e: SecurityException) {
283
+ isOpening.set(false)
284
+ Log.e(TAG, "[CAMERA2] Camera permission missing", e)
213
285
  } catch (e: Exception) {
214
- Log.e(TAG, "[BIND] Failed to bind camera use cases", e)
215
- e.printStackTrace()
286
+ isOpening.set(false)
287
+ Log.e(TAG, "[CAMERA2] Failed to open camera", e)
216
288
  }
217
289
  }
218
290
 
219
- private fun registerCameraStateObserver(camera: Camera?) {
220
- val cam = camera ?: return
221
- cameraStateLiveData?.let { liveData ->
222
- cameraStateObserver?.let { liveData.removeObserver(it) }
291
+ private fun createPreviewSession() {
292
+ val device = cameraDevice ?: return
293
+ val texture = previewView.surfaceTexture ?: return
294
+ val previewSize = previewSize ?: return
295
+ val analysisSize = analysisSize ?: previewSize
296
+
297
+ texture.setDefaultBufferSize(previewSize.width, previewSize.height)
298
+ val previewSurface = Surface(texture)
299
+
300
+ imageReader?.close()
301
+ imageReader = ImageReader.newInstance(
302
+ analysisSize.width,
303
+ analysisSize.height,
304
+ ImageFormat.YUV_420_888,
305
+ 2
306
+ ).apply {
307
+ setOnImageAvailableListener({ reader ->
308
+ val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
309
+ handleImage(image)
310
+ }, backgroundHandler)
223
311
  }
224
312
 
225
- val observer = Observer<CameraState> { state ->
226
- val error = state.error
227
- if (error != null && !hasFallbackAttempted && !isCaptureSession) {
228
- hasFallbackAttempted = true
229
- Log.e(TAG, "[STATE] Camera error detected (${error.code}), falling back to preview-only")
230
- try {
231
- cameraProvider?.unbindAll()
232
- bindCameraUseCases(enableDetection = false, useImageCapture = false)
233
- } catch (e: Exception) {
234
- Log.e(TAG, "[STATE] Fallback bind failed", e)
235
- }
236
- }
237
- }
313
+ val surfaces = listOf(previewSurface, imageReader!!.surface)
314
+ try {
315
+ device.createCaptureSession(
316
+ surfaces,
317
+ object : CameraCaptureSession.StateCallback() {
318
+ override fun onConfigured(session: CameraCaptureSession) {
319
+ if (cameraDevice == null) {
320
+ return
321
+ }
322
+ captureSession = session
323
+ previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
324
+ addTarget(previewSurface)
325
+ addTarget(imageReader!!.surface)
326
+ set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
327
+ set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
328
+ set(CaptureRequest.FLASH_MODE, if (torchEnabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
329
+ }
330
+ try {
331
+ session.setRepeatingRequest(previewRequestBuilder!!.build(), null, backgroundHandler)
332
+ Log.d(TAG, "[CAMERA2] Preview session started")
333
+ } catch (e: Exception) {
334
+ Log.e(TAG, "[CAMERA2] Failed to start preview", e)
335
+ }
336
+ }
238
337
 
239
- cameraStateObserver = observer
240
- cameraStateLiveData = cam.cameraInfo.cameraState
241
- cam.cameraInfo.cameraState.observe(lifecycleOwner, observer)
338
+ override fun onConfigureFailed(session: CameraCaptureSession) {
339
+ Log.e(TAG, "[CAMERA2] Preview session configure failed")
340
+ }
341
+ },
342
+ backgroundHandler
343
+ )
344
+ } catch (e: Exception) {
345
+ Log.e(TAG, "[CAMERA2] Failed to create preview session", e)
346
+ }
242
347
  }
243
348
 
244
- /**
245
- * Analyze frame for rectangle detection
246
- */
247
- private fun analyzeFrame(imageProxy: ImageProxy) {
349
+ private fun handleImage(image: Image) {
248
350
  try {
249
- val rotationDegrees = imageProxy.imageInfo.rotationDegrees
250
- val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
251
- imageProxy.height
252
- } else {
253
- imageProxy.width
254
- }
255
- val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
256
- imageProxy.width
257
- } else {
258
- imageProxy.height
259
- }
351
+ val rotationDegrees = getRotationDegrees()
352
+ val width = image.width
353
+ val height = image.height
354
+ val nv21 = imageToNV21(image)
260
355
 
261
- if (imageProxy.format != ImageFormat.YUV_420_888 || imageProxy.planes.size < 3) {
262
- onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
263
- return
264
- }
356
+ val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) height else width
357
+ val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) width else height
265
358
 
266
- val nv21 = imageProxyToNV21(imageProxy)
267
- val rectangle = DocumentDetector.detectRectangleInYUV(
268
- nv21,
269
- imageProxy.width,
270
- imageProxy.height,
271
- rotationDegrees
272
- )
273
-
274
- onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
275
- } catch (e: Exception) {
276
- Log.e(TAG, "Error analyzing frame", e)
277
- val rotationDegrees = imageProxy.imageInfo.rotationDegrees
278
- val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
279
- imageProxy.height
280
- } else {
281
- imageProxy.width
359
+ synchronized(lastFrameLock) {
360
+ lastFrame = LastFrame(nv21, width, height, rotationDegrees, useFrontCamera)
282
361
  }
283
- val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
284
- imageProxy.width
362
+
363
+ if (detectionEnabled) {
364
+ val rectangle = DocumentDetector.detectRectangleInYUV(
365
+ nv21,
366
+ width,
367
+ height,
368
+ rotationDegrees
369
+ )
370
+ onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
285
371
  } else {
286
- imageProxy.height
372
+ onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
287
373
  }
288
- onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
374
+ } catch (e: Exception) {
375
+ Log.e(TAG, "[CAMERA2] Error analyzing frame", e)
289
376
  } finally {
290
- imageProxy.close()
377
+ image.close()
291
378
  }
292
379
  }
293
380
 
294
- /**
295
- * Convert ImageProxy (YUV_420_888) to NV21 byte array
296
- */
297
- private fun imageProxyToNV21(image: ImageProxy): ByteArray {
381
+ private fun imageToNV21(image: Image): ByteArray {
298
382
  val width = image.width
299
383
  val height = image.height
300
384
 
@@ -338,114 +422,76 @@ class CameraController(
338
422
  return nv21
339
423
  }
340
424
 
341
- /**
342
- * Capture photo
343
- */
344
- fun capturePhoto(
345
- outputDirectory: File,
346
- onImageCaptured: (File) -> Unit,
347
- onError: (Exception) -> Unit
348
- ) {
349
- if (!isCaptureSession) {
350
- val provider = cameraProvider ?: run {
351
- onError(Exception("Camera provider not initialized"))
352
- return
353
- }
354
- ContextCompat.getMainExecutor(context).execute {
355
- try {
356
- // Rebind with ImageCapture only for the capture to avoid stream timeouts.
357
- provider.unbindAll()
358
- bindCameraUseCases(enableDetection = false, useImageCapture = true)
359
- capturePhoto(outputDirectory, onImageCaptured, onError)
360
- } catch (e: Exception) {
361
- onError(e)
362
- }
363
- }
364
- return
365
- }
425
+ private fun nv21ToJpeg(nv21: ByteArray, width: Int, height: Int, quality: Int): ByteArray {
426
+ val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
427
+ val out = ByteArrayOutputStream()
428
+ yuv.compressToJpeg(Rect(0, 0, width, height), quality, out)
429
+ return out.toByteArray()
430
+ }
366
431
 
367
- val imageCapture = imageCapture ?: run {
368
- onError(Exception("Image capture not initialized"))
369
- return
432
+ private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
433
+ if (rotationDegrees == 0 && !mirror) {
434
+ return bitmap
370
435
  }
371
-
372
- val photoFile = File(
373
- outputDirectory,
374
- "doc_scan_${System.currentTimeMillis()}.jpg"
375
- )
376
-
377
- val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
378
-
379
- imageCapture.takePicture(
380
- outputOptions,
381
- ContextCompat.getMainExecutor(context),
382
- object : ImageCapture.OnImageSavedCallback {
383
- override fun onImageSaved(output: ImageCapture.OutputFileResults) {
384
- Log.d(TAG, "Photo capture succeeded: ${photoFile.absolutePath}")
385
- onImageCaptured(photoFile)
386
- if (detectionEnabled) {
387
- ContextCompat.getMainExecutor(context).execute {
388
- bindCameraUseCases(enableDetection = true, useImageCapture = false)
389
- }
390
- }
391
- }
392
-
393
- override fun onError(exception: ImageCaptureException) {
394
- Log.e(TAG, "Photo capture failed", exception)
395
- if (exception.imageCaptureError == ImageCapture.ERROR_CAMERA_CLOSED) {
396
- Log.w(TAG, "Camera was closed during capture, attempting restart")
397
- stopCamera()
398
- startCamera(useFrontCamera, detectionEnabled)
399
- }
400
- if (detectionEnabled) {
401
- ContextCompat.getMainExecutor(context).execute {
402
- bindCameraUseCases(enableDetection = true, useImageCapture = false)
403
- }
404
- }
405
- onError(exception)
406
- }
407
- }
408
- )
436
+ val matrix = Matrix()
437
+ if (mirror) {
438
+ matrix.postScale(-1f, 1f, bitmap.width / 2f, bitmap.height / 2f)
439
+ }
440
+ if (rotationDegrees != 0) {
441
+ matrix.postRotate(rotationDegrees.toFloat(), bitmap.width / 2f, bitmap.height / 2f)
442
+ }
443
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
409
444
  }
410
445
 
411
- /**
412
- * Enable or disable torch (flashlight)
413
- */
414
- fun setTorchEnabled(enabled: Boolean) {
415
- torchEnabled = enabled
416
- camera?.cameraControl?.enableTorch(enabled)
446
+ private fun getRotationDegrees(): Int {
447
+ val displayRotation = previewView.display?.rotation ?: Surface.ROTATION_0
448
+ val displayDegrees = when (displayRotation) {
449
+ Surface.ROTATION_0 -> 0
450
+ Surface.ROTATION_90 -> 90
451
+ Surface.ROTATION_180 -> 180
452
+ Surface.ROTATION_270 -> 270
453
+ else -> 0
454
+ }
455
+
456
+ return if (useFrontCamera) {
457
+ (sensorOrientation + displayDegrees) % 360
458
+ } else {
459
+ (sensorOrientation - displayDegrees + 360) % 360
460
+ }
417
461
  }
418
462
 
419
- /**
420
- * Switch between front and back camera
421
- */
422
- fun switchCamera() {
423
- useFrontCamera = !useFrontCamera
424
- startCamera(useFrontCamera)
463
+ private fun chooseSize(choices: Array<Size>, maxWidth: Int, maxHeight: Int): Size? {
464
+ if (choices.isEmpty()) {
465
+ return null
466
+ }
467
+ val filtered = choices.filter { it.width <= maxWidth && it.height <= maxHeight }
468
+ val candidates = if (filtered.isNotEmpty()) filtered else choices.toList()
469
+ return candidates.sortedBy { it.width * it.height }.last()
425
470
  }
426
471
 
427
- /**
428
- * Check if torch is available
429
- */
430
- fun isTorchAvailable(): Boolean {
431
- return camera?.cameraInfo?.hasFlashUnit() == true
472
+ private fun startBackgroundThread() {
473
+ if (backgroundThread != null) {
474
+ return
475
+ }
476
+ backgroundThread = HandlerThread("Camera2Background").also {
477
+ it.start()
478
+ backgroundHandler = Handler(it.looper)
479
+ }
432
480
  }
433
481
 
434
- /**
435
- * Focus at specific point
436
- */
437
- fun focusAt(x: Float, y: Float) {
438
- val factory = previewView.meteringPointFactory
439
- val point = factory.createPoint(x, y)
440
- val action = FocusMeteringAction.Builder(point).build()
441
- camera?.cameraControl?.startFocusAndMetering(action)
482
+ private fun stopBackgroundThread() {
483
+ try {
484
+ backgroundThread?.quitSafely()
485
+ backgroundThread?.join()
486
+ } catch (e: InterruptedException) {
487
+ Log.w(TAG, "[CAMERA2] Background thread shutdown interrupted", e)
488
+ } finally {
489
+ backgroundThread = null
490
+ backgroundHandler = null
491
+ }
442
492
  }
443
493
 
444
- /**
445
- * Cleanup resources
446
- */
447
- fun shutdown() {
448
- cameraExecutor.shutdown()
449
- stopCamera()
494
+ private fun hasCameraPermission(): Boolean {
495
+ return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
450
496
  }
451
497
  }
@@ -8,9 +8,9 @@ import android.graphics.Paint
8
8
  import android.graphics.PorterDuff
9
9
  import android.graphics.PorterDuffXfermode
10
10
  import android.util.Log
11
+ import android.view.TextureView
11
12
  import android.view.View
12
13
  import android.widget.FrameLayout
13
- import androidx.camera.view.PreviewView
14
14
  import androidx.lifecycle.Lifecycle
15
15
  import androidx.lifecycle.LifecycleOwner
16
16
  import androidx.lifecycle.LifecycleRegistry
@@ -25,7 +25,7 @@ import kotlin.math.min
25
25
 
26
26
  class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), LifecycleOwner {
27
27
  private val themedContext = context
28
- private val previewView: PreviewView
28
+ private val previewView: TextureView
29
29
  private val overlayView: OverlayView
30
30
  private var cameraController: CameraController? = null
31
31
  private val lifecycleRegistry = LifecycleRegistry(this)
@@ -73,12 +73,9 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
73
73
  Log.d(TAG, "[INIT] Lifecycle state: ${lifecycleRegistry.currentState}")
74
74
 
75
75
  // Create preview view
76
- Log.d(TAG, "[INIT] Creating PreviewView...")
77
- previewView = PreviewView(context).apply {
76
+ Log.d(TAG, "[INIT] Creating TextureView...")
77
+ previewView = TextureView(context).apply {
78
78
  layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
79
- scaleType = PreviewView.ScaleType.FILL_CENTER
80
- // Use PERFORMANCE (SurfaceView) - more reliable for camera preview
81
- implementationMode = PreviewView.ImplementationMode.PERFORMANCE
82
79
  visibility = View.VISIBLE
83
80
  keepScreenOn = true
84
81
  // Force view to be drawn
@@ -87,13 +84,12 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
87
84
  bringToFront()
88
85
  requestLayout()
89
86
  }
90
- Log.d(TAG, "[INIT] PreviewView created: $previewView")
91
- Log.d(TAG, "[INIT] PreviewView implementationMode: ${previewView.implementationMode}")
92
- Log.d(TAG, "[INIT] PreviewView visibility: ${previewView.visibility}")
87
+ Log.d(TAG, "[INIT] TextureView created: $previewView")
88
+ Log.d(TAG, "[INIT] TextureView visibility: ${previewView.visibility}")
93
89
 
94
- Log.d(TAG, "[INIT] Adding PreviewView to parent...")
90
+ Log.d(TAG, "[INIT] Adding TextureView to parent...")
95
91
  addView(previewView)
96
- Log.d(TAG, "[INIT] PreviewView added, childCount: $childCount")
92
+ Log.d(TAG, "[INIT] TextureView added, childCount: $childCount")
97
93
 
98
94
  // Create overlay view for drawing rectangle
99
95
  Log.d(TAG, "[INIT] Creating OverlayView...")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.193.0",
3
+ "version": "3.195.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",