react-native-rectangle-doc-scanner 3.194.0 → 3.196.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,298 +1,425 @@
|
|
|
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.RectF
|
|
12
|
+
import android.graphics.SurfaceTexture
|
|
13
|
+
import android.graphics.YuvImage
|
|
14
|
+
import android.hardware.camera2.CameraCaptureSession
|
|
15
|
+
import android.hardware.camera2.CameraCharacteristics
|
|
16
|
+
import android.hardware.camera2.CameraDevice
|
|
17
|
+
import android.hardware.camera2.CameraManager
|
|
18
|
+
import android.hardware.camera2.CaptureRequest
|
|
19
|
+
import android.media.Image
|
|
20
|
+
import android.media.ImageReader
|
|
21
|
+
import android.os.Handler
|
|
22
|
+
import android.os.HandlerThread
|
|
5
23
|
import android.util.Log
|
|
6
24
|
import android.util.Size
|
|
7
25
|
import android.view.Surface
|
|
8
|
-
import
|
|
9
|
-
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
10
|
-
import androidx.camera.view.PreviewView
|
|
26
|
+
import android.view.TextureView
|
|
11
27
|
import androidx.core.content.ContextCompat
|
|
12
|
-
import androidx.lifecycle.Lifecycle
|
|
13
28
|
import androidx.lifecycle.LifecycleOwner
|
|
14
|
-
import
|
|
15
|
-
import androidx.lifecycle.Observer
|
|
29
|
+
import java.io.ByteArrayOutputStream
|
|
16
30
|
import java.io.File
|
|
17
|
-
import java.
|
|
18
|
-
import java.util.concurrent.
|
|
31
|
+
import java.io.FileOutputStream
|
|
32
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
19
33
|
|
|
20
34
|
class CameraController(
|
|
21
35
|
private val context: Context,
|
|
22
36
|
private val lifecycleOwner: LifecycleOwner,
|
|
23
|
-
private val previewView:
|
|
37
|
+
private val previewView: TextureView
|
|
24
38
|
) {
|
|
25
|
-
private
|
|
26
|
-
private var
|
|
27
|
-
private var
|
|
28
|
-
private var
|
|
29
|
-
private
|
|
30
|
-
|
|
39
|
+
private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
|
40
|
+
private var cameraDevice: CameraDevice? = null
|
|
41
|
+
private var captureSession: CameraCaptureSession? = null
|
|
42
|
+
private var previewRequestBuilder: CaptureRequest.Builder? = null
|
|
43
|
+
private var imageReader: ImageReader? = null
|
|
44
|
+
private var backgroundThread: HandlerThread? = null
|
|
45
|
+
private var backgroundHandler: Handler? = null
|
|
46
|
+
|
|
47
|
+
private var cameraId: String? = null
|
|
48
|
+
private var sensorOrientation: Int = 0
|
|
49
|
+
private var previewSize: Size? = null
|
|
50
|
+
private var analysisSize: Size? = null
|
|
31
51
|
private var useFrontCamera = false
|
|
32
52
|
private var torchEnabled = false
|
|
33
53
|
private var detectionEnabled = true
|
|
34
|
-
private var
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
private
|
|
54
|
+
private var hasStarted = false
|
|
55
|
+
|
|
56
|
+
private val isOpening = AtomicBoolean(false)
|
|
57
|
+
private val lastFrameLock = Any()
|
|
58
|
+
private var lastFrame: LastFrame? = null
|
|
38
59
|
|
|
39
60
|
var onFrameAnalyzed: ((Rectangle?, Int, Int) -> Unit)? = null
|
|
40
61
|
|
|
41
62
|
companion object {
|
|
42
63
|
private const val TAG = "CameraController"
|
|
64
|
+
private const val MAX_PREVIEW_WIDTH = 1280
|
|
65
|
+
private const val MAX_PREVIEW_HEIGHT = 720
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private data class LastFrame(
|
|
69
|
+
val nv21: ByteArray,
|
|
70
|
+
val width: Int,
|
|
71
|
+
val height: Int,
|
|
72
|
+
val rotationDegrees: Int,
|
|
73
|
+
val isFront: Boolean
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
private val textureListener = object : TextureView.SurfaceTextureListener {
|
|
77
|
+
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
|
78
|
+
Log.d(TAG, "[CAMERA2] Texture available: ${width}x${height}")
|
|
79
|
+
createPreviewSession()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
|
83
|
+
Log.d(TAG, "[CAMERA2] Texture size changed: ${width}x${height}")
|
|
84
|
+
updatePreviewTransform()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
|
88
|
+
Log.d(TAG, "[CAMERA2] Texture destroyed")
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit
|
|
43
93
|
}
|
|
44
94
|
|
|
45
|
-
/**
|
|
46
|
-
* Start camera with preview and analysis
|
|
47
|
-
*/
|
|
48
95
|
fun startCamera(
|
|
49
96
|
useFrontCam: Boolean = false,
|
|
50
97
|
enableDetection: Boolean = true
|
|
51
98
|
) {
|
|
52
99
|
Log.d(TAG, "========================================")
|
|
53
|
-
Log.d(TAG, "[
|
|
54
|
-
Log.d(TAG, "[
|
|
55
|
-
Log.d(TAG, "[
|
|
56
|
-
Log.d(TAG, "[
|
|
57
|
-
Log.d(TAG, "[CAMERA_CONTROLLER] lifecycleOwner.lifecycle.currentState: ${lifecycleOwner.lifecycle.currentState}")
|
|
100
|
+
Log.d(TAG, "[CAMERA2] startCamera called")
|
|
101
|
+
Log.d(TAG, "[CAMERA2] useFrontCam: $useFrontCam")
|
|
102
|
+
Log.d(TAG, "[CAMERA2] enableDetection: $enableDetection")
|
|
103
|
+
Log.d(TAG, "[CAMERA2] lifecycleOwner: $lifecycleOwner")
|
|
58
104
|
Log.d(TAG, "========================================")
|
|
59
105
|
|
|
60
106
|
this.useFrontCamera = useFrontCam
|
|
61
107
|
this.detectionEnabled = enableDetection
|
|
62
108
|
|
|
63
|
-
|
|
64
|
-
|
|
109
|
+
if (hasStarted) {
|
|
110
|
+
Log.d(TAG, "[CAMERA2] Already started, skipping")
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
hasStarted = true
|
|
114
|
+
|
|
115
|
+
if (!hasCameraPermission()) {
|
|
116
|
+
Log.e(TAG, "[CAMERA2] Camera permission not granted")
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
startBackgroundThread()
|
|
121
|
+
chooseCamera()
|
|
122
|
+
|
|
123
|
+
if (previewView.isAvailable) {
|
|
124
|
+
openCamera()
|
|
125
|
+
} else {
|
|
126
|
+
previewView.surfaceTextureListener = textureListener
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fun stopCamera() {
|
|
131
|
+
Log.d(TAG, "[CAMERA2] stopCamera called")
|
|
132
|
+
try {
|
|
133
|
+
captureSession?.close()
|
|
134
|
+
captureSession = null
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
Log.w(TAG, "[CAMERA2] Failed to close session", e)
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
cameraDevice?.close()
|
|
140
|
+
cameraDevice = null
|
|
141
|
+
} catch (e: Exception) {
|
|
142
|
+
Log.w(TAG, "[CAMERA2] Failed to close camera device", e)
|
|
143
|
+
}
|
|
144
|
+
imageReader?.close()
|
|
145
|
+
imageReader = null
|
|
146
|
+
stopBackgroundThread()
|
|
147
|
+
hasStarted = false
|
|
148
|
+
}
|
|
65
149
|
|
|
66
|
-
|
|
150
|
+
fun capturePhoto(
|
|
151
|
+
outputDirectory: File,
|
|
152
|
+
onImageCaptured: (File) -> Unit,
|
|
153
|
+
onError: (Exception) -> Unit
|
|
154
|
+
) {
|
|
155
|
+
val frame = synchronized(lastFrameLock) { lastFrame }
|
|
156
|
+
if (frame == null) {
|
|
157
|
+
onError(Exception("No frame available for capture"))
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
backgroundHandler?.post {
|
|
67
162
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
163
|
+
val photoFile = File(
|
|
164
|
+
outputDirectory,
|
|
165
|
+
"doc_scan_${System.currentTimeMillis()}.jpg"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
val jpegBytes = nv21ToJpeg(frame.nv21, frame.width, frame.height, 95)
|
|
169
|
+
val bitmap = BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
|
170
|
+
?: throw IllegalStateException("Failed to decode JPEG")
|
|
171
|
+
|
|
172
|
+
val rotated = rotateAndMirror(bitmap, frame.rotationDegrees, frame.isFront)
|
|
173
|
+
FileOutputStream(photoFile).use { out ->
|
|
174
|
+
rotated.compress(Bitmap.CompressFormat.JPEG, 95, out)
|
|
175
|
+
}
|
|
176
|
+
if (rotated != bitmap) {
|
|
177
|
+
rotated.recycle()
|
|
178
|
+
}
|
|
179
|
+
bitmap.recycle()
|
|
180
|
+
|
|
181
|
+
Log.d(TAG, "[CAMERA2] Photo capture succeeded: ${photoFile.absolutePath}")
|
|
182
|
+
onImageCaptured(photoFile)
|
|
75
183
|
} catch (e: Exception) {
|
|
76
|
-
Log.e(TAG, "[
|
|
77
|
-
e
|
|
184
|
+
Log.e(TAG, "[CAMERA2] Photo capture failed", e)
|
|
185
|
+
onError(e)
|
|
78
186
|
}
|
|
79
|
-
}
|
|
187
|
+
}
|
|
80
188
|
}
|
|
81
189
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
190
|
+
fun setTorchEnabled(enabled: Boolean) {
|
|
191
|
+
torchEnabled = enabled
|
|
192
|
+
val builder = previewRequestBuilder ?: return
|
|
193
|
+
builder.set(CaptureRequest.FLASH_MODE, if (enabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
|
|
194
|
+
try {
|
|
195
|
+
captureSession?.setRepeatingRequest(builder.build(), null, backgroundHandler)
|
|
196
|
+
} catch (e: Exception) {
|
|
197
|
+
Log.w(TAG, "[CAMERA2] Failed to update torch", e)
|
|
198
|
+
}
|
|
88
199
|
}
|
|
89
200
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
val
|
|
99
|
-
|
|
100
|
-
|
|
201
|
+
fun switchCamera() {
|
|
202
|
+
useFrontCamera = !useFrontCamera
|
|
203
|
+
stopCamera()
|
|
204
|
+
startCamera(useFrontCamera, detectionEnabled)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fun isTorchAvailable(): Boolean {
|
|
208
|
+
val id = cameraId ?: return false
|
|
209
|
+
val characteristics = cameraManager.getCameraCharacteristics(id)
|
|
210
|
+
return characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) == true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fun focusAt(x: Float, y: Float) {
|
|
214
|
+
// No-op for now. Camera2 focus metering can be added if needed.
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fun shutdown() {
|
|
218
|
+
stopCamera()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fun chooseCamera() {
|
|
222
|
+
val lensFacing = if (useFrontCamera) {
|
|
223
|
+
CameraCharacteristics.LENS_FACING_FRONT
|
|
224
|
+
} else {
|
|
225
|
+
CameraCharacteristics.LENS_FACING_BACK
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
val ids = cameraManager.cameraIdList
|
|
229
|
+
val selected = ids.firstOrNull { id ->
|
|
230
|
+
val characteristics = cameraManager.getCameraCharacteristics(id)
|
|
231
|
+
characteristics.get(CameraCharacteristics.LENS_FACING) == lensFacing
|
|
232
|
+
} ?: ids.firstOrNull()
|
|
233
|
+
|
|
234
|
+
if (selected == null) {
|
|
235
|
+
Log.e(TAG, "[CAMERA2] No camera available")
|
|
101
236
|
return
|
|
102
237
|
}
|
|
103
238
|
|
|
104
|
-
|
|
105
|
-
val
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
239
|
+
cameraId = selected
|
|
240
|
+
val characteristics = cameraManager.getCameraCharacteristics(selected)
|
|
241
|
+
sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
|
|
242
|
+
|
|
243
|
+
val streamConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
|
|
244
|
+
val previewChoices = streamConfig?.getOutputSizes(SurfaceTexture::class.java) ?: emptyArray()
|
|
245
|
+
val analysisChoices = streamConfig?.getOutputSizes(ImageFormat.YUV_420_888) ?: emptyArray()
|
|
246
|
+
|
|
247
|
+
previewSize = chooseSize(previewChoices, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
|
|
248
|
+
analysisSize = chooseSize(analysisChoices, MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
|
|
249
|
+
Log.d(TAG, "[CAMERA2] Selected sizes - preview: $previewSize, analysis: $analysisSize")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun openCamera() {
|
|
253
|
+
val id = cameraId ?: run {
|
|
254
|
+
Log.e(TAG, "[CAMERA2] Camera id not set")
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (isOpening.getAndSet(true)) {
|
|
109
258
|
return
|
|
110
259
|
}
|
|
111
260
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
val targetRotation = previewView.display?.rotation ?: Surface.ROTATION_0
|
|
121
|
-
|
|
122
|
-
// Preview use case (avoid forcing a size to let CameraX pick a compatible stream)
|
|
123
|
-
Log.d(TAG, "[BIND] Creating Preview use case...")
|
|
124
|
-
val preview = Preview.Builder()
|
|
125
|
-
.setTargetRotation(targetRotation)
|
|
126
|
-
.build()
|
|
127
|
-
Log.d(TAG, "[BIND] Preview created: $preview")
|
|
128
|
-
|
|
129
|
-
// Image capture use case (bound only when capture is requested)
|
|
130
|
-
if (useImageCapture) {
|
|
131
|
-
Log.d(TAG, "[BIND] Creating ImageCapture use case...")
|
|
132
|
-
imageCapture = ImageCapture.Builder()
|
|
133
|
-
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
|
|
134
|
-
// Cap resolution to avoid camera session timeouts on lower-end devices.
|
|
135
|
-
.setTargetResolution(Size(960, 720))
|
|
136
|
-
.setTargetRotation(targetRotation)
|
|
137
|
-
.setFlashMode(ImageCapture.FLASH_MODE_AUTO)
|
|
138
|
-
.build()
|
|
139
|
-
Log.d(TAG, "[BIND] ImageCapture created: $imageCapture")
|
|
140
|
-
} else {
|
|
141
|
-
imageCapture = null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Image analysis use case for rectangle detection
|
|
145
|
-
imageAnalysis = if (enableDetection) {
|
|
146
|
-
Log.d(TAG, "[BIND] Creating ImageAnalysis use case...")
|
|
147
|
-
ImageAnalysis.Builder()
|
|
148
|
-
// Keep analysis lightweight to prevent session configuration timeouts.
|
|
149
|
-
.setTargetResolution(Size(960, 720))
|
|
150
|
-
.setTargetRotation(targetRotation)
|
|
151
|
-
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
152
|
-
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
153
|
-
.build()
|
|
154
|
-
.also { analysis ->
|
|
155
|
-
analysis.setAnalyzer(cameraExecutor) { imageProxy ->
|
|
156
|
-
analyzeFrame(imageProxy)
|
|
157
|
-
}
|
|
158
|
-
Log.d(TAG, "[BIND] ImageAnalysis created and analyzer set: $analysis")
|
|
261
|
+
try {
|
|
262
|
+
cameraManager.openCamera(id, object : CameraDevice.StateCallback() {
|
|
263
|
+
override fun onOpened(device: CameraDevice) {
|
|
264
|
+
Log.d(TAG, "[CAMERA2] Camera opened")
|
|
265
|
+
isOpening.set(false)
|
|
266
|
+
cameraDevice = device
|
|
267
|
+
createPreviewSession()
|
|
159
268
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
269
|
+
|
|
270
|
+
override fun onDisconnected(device: CameraDevice) {
|
|
271
|
+
Log.w(TAG, "[CAMERA2] Camera disconnected")
|
|
272
|
+
isOpening.set(false)
|
|
273
|
+
device.close()
|
|
274
|
+
cameraDevice = null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override fun onError(device: CameraDevice, error: Int) {
|
|
278
|
+
Log.e(TAG, "[CAMERA2] Camera error: $error")
|
|
279
|
+
isOpening.set(false)
|
|
280
|
+
device.close()
|
|
281
|
+
cameraDevice = null
|
|
282
|
+
}
|
|
283
|
+
}, backgroundHandler)
|
|
284
|
+
} catch (e: SecurityException) {
|
|
285
|
+
isOpening.set(false)
|
|
286
|
+
Log.e(TAG, "[CAMERA2] Camera permission missing", e)
|
|
287
|
+
} catch (e: Exception) {
|
|
288
|
+
isOpening.set(false)
|
|
289
|
+
Log.e(TAG, "[CAMERA2] Failed to open camera", e)
|
|
163
290
|
}
|
|
291
|
+
}
|
|
164
292
|
|
|
293
|
+
private fun createPreviewSession() {
|
|
294
|
+
val device = cameraDevice ?: return
|
|
295
|
+
val texture = previewView.surfaceTexture ?: return
|
|
296
|
+
val previewSize = previewSize ?: return
|
|
297
|
+
val analysisSize = analysisSize ?: previewSize
|
|
298
|
+
|
|
299
|
+
texture.setDefaultBufferSize(previewSize.width, previewSize.height)
|
|
300
|
+
val previewSurface = Surface(texture)
|
|
301
|
+
|
|
302
|
+
imageReader?.close()
|
|
303
|
+
imageReader = ImageReader.newInstance(
|
|
304
|
+
analysisSize.width,
|
|
305
|
+
analysisSize.height,
|
|
306
|
+
ImageFormat.YUV_420_888,
|
|
307
|
+
2
|
|
308
|
+
).apply {
|
|
309
|
+
setOnImageAvailableListener({ reader ->
|
|
310
|
+
val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
|
|
311
|
+
handleImage(image)
|
|
312
|
+
}, backgroundHandler)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
val surfaces = listOf(previewSurface, imageReader!!.surface)
|
|
165
316
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
Log.d(TAG, "[BIND] Total use cases to bind: ${useCases.size}")
|
|
317
|
+
device.createCaptureSession(
|
|
318
|
+
surfaces,
|
|
319
|
+
object : CameraCaptureSession.StateCallback() {
|
|
320
|
+
override fun onConfigured(session: CameraCaptureSession) {
|
|
321
|
+
if (cameraDevice == null) {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
captureSession = session
|
|
325
|
+
previewRequestBuilder = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
|
|
326
|
+
addTarget(previewSurface)
|
|
327
|
+
addTarget(imageReader!!.surface)
|
|
328
|
+
set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO)
|
|
329
|
+
set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
|
|
330
|
+
set(CaptureRequest.FLASH_MODE, if (torchEnabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
session.setRepeatingRequest(previewRequestBuilder!!.build(), null, backgroundHandler)
|
|
334
|
+
Log.d(TAG, "[CAMERA2] Preview session started")
|
|
335
|
+
updatePreviewTransform()
|
|
336
|
+
} catch (e: Exception) {
|
|
337
|
+
Log.e(TAG, "[CAMERA2] Failed to start preview", e)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
190
340
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
341
|
+
override fun onConfigureFailed(session: CameraCaptureSession) {
|
|
342
|
+
Log.e(TAG, "[CAMERA2] Preview session configure failed")
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
backgroundHandler
|
|
196
346
|
)
|
|
197
|
-
Log.d(TAG, "[BIND] Bound to lifecycle successfully, camera: $camera")
|
|
198
|
-
registerCameraStateObserver(camera)
|
|
199
|
-
|
|
200
|
-
// Restore torch state if it was enabled
|
|
201
|
-
if (torchEnabled) {
|
|
202
|
-
Log.d(TAG, "[BIND] Restoring torch state...")
|
|
203
|
-
setTorchEnabled(true)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
Log.d(TAG, "[BIND] ========================================")
|
|
207
|
-
Log.d(TAG, "[BIND] Camera started successfully!")
|
|
208
|
-
Log.d(TAG, "[BIND] hasFlashUnit: ${camera?.cameraInfo?.hasFlashUnit()}")
|
|
209
|
-
Log.d(TAG, "[BIND] ========================================")
|
|
210
|
-
isCaptureSession = useImageCapture
|
|
211
347
|
} catch (e: Exception) {
|
|
212
|
-
Log.e(TAG, "[
|
|
213
|
-
e.printStackTrace()
|
|
348
|
+
Log.e(TAG, "[CAMERA2] Failed to create preview session", e)
|
|
214
349
|
}
|
|
215
350
|
}
|
|
216
351
|
|
|
217
|
-
private fun
|
|
218
|
-
val
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Log.e(TAG, "[STATE] Fallback bind failed", e)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
352
|
+
private fun updatePreviewTransform() {
|
|
353
|
+
val previewSize = previewSize ?: return
|
|
354
|
+
val viewWidth = previewView.width
|
|
355
|
+
val viewHeight = previewView.height
|
|
356
|
+
if (viewWidth == 0 || viewHeight == 0) {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
val rotation = previewView.display?.rotation ?: Surface.ROTATION_0
|
|
361
|
+
val matrix = Matrix()
|
|
362
|
+
val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
|
|
363
|
+
val bufferRect = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
|
364
|
+
RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat())
|
|
365
|
+
} else {
|
|
366
|
+
RectF(0f, 0f, previewSize.width.toFloat(), previewSize.height.toFloat())
|
|
235
367
|
}
|
|
368
|
+
val centerX = viewRect.centerX()
|
|
369
|
+
val centerY = viewRect.centerY()
|
|
370
|
+
|
|
371
|
+
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
|
|
372
|
+
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
|
|
236
373
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
374
|
+
val scale = if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
|
375
|
+
maxOf(viewHeight.toFloat() / previewSize.height, viewWidth.toFloat() / previewSize.width)
|
|
376
|
+
} else {
|
|
377
|
+
maxOf(viewWidth.toFloat() / previewSize.width, viewHeight.toFloat() / previewSize.height)
|
|
378
|
+
}
|
|
379
|
+
matrix.postScale(scale, scale, centerX, centerY)
|
|
380
|
+
|
|
381
|
+
when (rotation) {
|
|
382
|
+
Surface.ROTATION_90 -> matrix.postRotate(90f, centerX, centerY)
|
|
383
|
+
Surface.ROTATION_180 -> matrix.postRotate(180f, centerX, centerY)
|
|
384
|
+
Surface.ROTATION_270 -> matrix.postRotate(270f, centerX, centerY)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
previewView.setTransform(matrix)
|
|
240
388
|
}
|
|
241
389
|
|
|
242
|
-
|
|
243
|
-
* Analyze frame for rectangle detection
|
|
244
|
-
*/
|
|
245
|
-
private fun analyzeFrame(imageProxy: ImageProxy) {
|
|
390
|
+
private fun handleImage(image: Image) {
|
|
246
391
|
try {
|
|
247
|
-
val rotationDegrees =
|
|
248
|
-
val
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
imageProxy.width
|
|
252
|
-
}
|
|
253
|
-
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
254
|
-
imageProxy.width
|
|
255
|
-
} else {
|
|
256
|
-
imageProxy.height
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (imageProxy.format != ImageFormat.YUV_420_888 || imageProxy.planes.size < 3) {
|
|
260
|
-
onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
|
|
261
|
-
return
|
|
262
|
-
}
|
|
392
|
+
val rotationDegrees = getRotationDegrees()
|
|
393
|
+
val width = image.width
|
|
394
|
+
val height = image.height
|
|
395
|
+
val nv21 = imageToNV21(image)
|
|
263
396
|
|
|
264
|
-
val
|
|
265
|
-
val
|
|
266
|
-
nv21,
|
|
267
|
-
imageProxy.width,
|
|
268
|
-
imageProxy.height,
|
|
269
|
-
rotationDegrees
|
|
270
|
-
)
|
|
397
|
+
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) height else width
|
|
398
|
+
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) width else height
|
|
271
399
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Log.e(TAG, "Error analyzing frame", e)
|
|
275
|
-
val rotationDegrees = imageProxy.imageInfo.rotationDegrees
|
|
276
|
-
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) {
|
|
277
|
-
imageProxy.height
|
|
278
|
-
} else {
|
|
279
|
-
imageProxy.width
|
|
400
|
+
synchronized(lastFrameLock) {
|
|
401
|
+
lastFrame = LastFrame(nv21, width, height, rotationDegrees, useFrontCamera)
|
|
280
402
|
}
|
|
281
|
-
|
|
282
|
-
|
|
403
|
+
|
|
404
|
+
if (detectionEnabled) {
|
|
405
|
+
val rectangle = DocumentDetector.detectRectangleInYUV(
|
|
406
|
+
nv21,
|
|
407
|
+
width,
|
|
408
|
+
height,
|
|
409
|
+
rotationDegrees
|
|
410
|
+
)
|
|
411
|
+
onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
|
|
283
412
|
} else {
|
|
284
|
-
|
|
413
|
+
onFrameAnalyzed?.invoke(null, frameWidth, frameHeight)
|
|
285
414
|
}
|
|
286
|
-
|
|
415
|
+
} catch (e: Exception) {
|
|
416
|
+
Log.e(TAG, "[CAMERA2] Error analyzing frame", e)
|
|
287
417
|
} finally {
|
|
288
|
-
|
|
418
|
+
image.close()
|
|
289
419
|
}
|
|
290
420
|
}
|
|
291
421
|
|
|
292
|
-
|
|
293
|
-
* Convert ImageProxy (YUV_420_888) to NV21 byte array
|
|
294
|
-
*/
|
|
295
|
-
private fun imageProxyToNV21(image: ImageProxy): ByteArray {
|
|
422
|
+
private fun imageToNV21(image: Image): ByteArray {
|
|
296
423
|
val width = image.width
|
|
297
424
|
val height = image.height
|
|
298
425
|
|
|
@@ -336,114 +463,76 @@ class CameraController(
|
|
|
336
463
|
return nv21
|
|
337
464
|
}
|
|
338
465
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
onError: (Exception) -> Unit
|
|
346
|
-
) {
|
|
347
|
-
if (!isCaptureSession) {
|
|
348
|
-
val provider = cameraProvider ?: run {
|
|
349
|
-
onError(Exception("Camera provider not initialized"))
|
|
350
|
-
return
|
|
351
|
-
}
|
|
352
|
-
ContextCompat.getMainExecutor(context).execute {
|
|
353
|
-
try {
|
|
354
|
-
// Rebind with ImageCapture only for the capture to avoid stream timeouts.
|
|
355
|
-
provider.unbindAll()
|
|
356
|
-
bindCameraUseCases(enableDetection = false, useImageCapture = true)
|
|
357
|
-
capturePhoto(outputDirectory, onImageCaptured, onError)
|
|
358
|
-
} catch (e: Exception) {
|
|
359
|
-
onError(e)
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return
|
|
363
|
-
}
|
|
466
|
+
private fun nv21ToJpeg(nv21: ByteArray, width: Int, height: Int, quality: Int): ByteArray {
|
|
467
|
+
val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
|
|
468
|
+
val out = ByteArrayOutputStream()
|
|
469
|
+
yuv.compressToJpeg(Rect(0, 0, width, height), quality, out)
|
|
470
|
+
return out.toByteArray()
|
|
471
|
+
}
|
|
364
472
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
return
|
|
473
|
+
private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
|
|
474
|
+
if (rotationDegrees == 0 && !mirror) {
|
|
475
|
+
return bitmap
|
|
368
476
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
imageCapture.takePicture(
|
|
378
|
-
outputOptions,
|
|
379
|
-
ContextCompat.getMainExecutor(context),
|
|
380
|
-
object : ImageCapture.OnImageSavedCallback {
|
|
381
|
-
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
|
382
|
-
Log.d(TAG, "Photo capture succeeded: ${photoFile.absolutePath}")
|
|
383
|
-
onImageCaptured(photoFile)
|
|
384
|
-
if (detectionEnabled) {
|
|
385
|
-
ContextCompat.getMainExecutor(context).execute {
|
|
386
|
-
bindCameraUseCases(enableDetection = true, useImageCapture = false)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
override fun onError(exception: ImageCaptureException) {
|
|
392
|
-
Log.e(TAG, "Photo capture failed", exception)
|
|
393
|
-
if (exception.imageCaptureError == ImageCapture.ERROR_CAMERA_CLOSED) {
|
|
394
|
-
Log.w(TAG, "Camera was closed during capture, attempting restart")
|
|
395
|
-
stopCamera()
|
|
396
|
-
startCamera(useFrontCamera, detectionEnabled)
|
|
397
|
-
}
|
|
398
|
-
if (detectionEnabled) {
|
|
399
|
-
ContextCompat.getMainExecutor(context).execute {
|
|
400
|
-
bindCameraUseCases(enableDetection = true, useImageCapture = false)
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
onError(exception)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
)
|
|
477
|
+
val matrix = Matrix()
|
|
478
|
+
if (mirror) {
|
|
479
|
+
matrix.postScale(-1f, 1f, bitmap.width / 2f, bitmap.height / 2f)
|
|
480
|
+
}
|
|
481
|
+
if (rotationDegrees != 0) {
|
|
482
|
+
matrix.postRotate(rotationDegrees.toFloat(), bitmap.width / 2f, bitmap.height / 2f)
|
|
483
|
+
}
|
|
484
|
+
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
407
485
|
}
|
|
408
486
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
487
|
+
private fun getRotationDegrees(): Int {
|
|
488
|
+
val displayRotation = previewView.display?.rotation ?: Surface.ROTATION_0
|
|
489
|
+
val displayDegrees = when (displayRotation) {
|
|
490
|
+
Surface.ROTATION_0 -> 0
|
|
491
|
+
Surface.ROTATION_90 -> 90
|
|
492
|
+
Surface.ROTATION_180 -> 180
|
|
493
|
+
Surface.ROTATION_270 -> 270
|
|
494
|
+
else -> 0
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return if (useFrontCamera) {
|
|
498
|
+
(sensorOrientation + displayDegrees) % 360
|
|
499
|
+
} else {
|
|
500
|
+
(sensorOrientation - displayDegrees + 360) % 360
|
|
501
|
+
}
|
|
415
502
|
}
|
|
416
503
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
504
|
+
private fun chooseSize(choices: Array<Size>, maxWidth: Int, maxHeight: Int): Size? {
|
|
505
|
+
if (choices.isEmpty()) {
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
val filtered = choices.filter { it.width <= maxWidth && it.height <= maxHeight }
|
|
509
|
+
val candidates = if (filtered.isNotEmpty()) filtered else choices.toList()
|
|
510
|
+
return candidates.sortedBy { it.width * it.height }.last()
|
|
423
511
|
}
|
|
424
512
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
513
|
+
private fun startBackgroundThread() {
|
|
514
|
+
if (backgroundThread != null) {
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
backgroundThread = HandlerThread("Camera2Background").also {
|
|
518
|
+
it.start()
|
|
519
|
+
backgroundHandler = Handler(it.looper)
|
|
520
|
+
}
|
|
430
521
|
}
|
|
431
522
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
523
|
+
private fun stopBackgroundThread() {
|
|
524
|
+
try {
|
|
525
|
+
backgroundThread?.quitSafely()
|
|
526
|
+
backgroundThread?.join()
|
|
527
|
+
} catch (e: InterruptedException) {
|
|
528
|
+
Log.w(TAG, "[CAMERA2] Background thread shutdown interrupted", e)
|
|
529
|
+
} finally {
|
|
530
|
+
backgroundThread = null
|
|
531
|
+
backgroundHandler = null
|
|
532
|
+
}
|
|
440
533
|
}
|
|
441
534
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
*/
|
|
445
|
-
fun shutdown() {
|
|
446
|
-
cameraExecutor.shutdown()
|
|
447
|
-
stopCamera()
|
|
535
|
+
private fun hasCameraPermission(): Boolean {
|
|
536
|
+
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
|
448
537
|
}
|
|
449
538
|
}
|
|
@@ -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:
|
|
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
|
|
77
|
-
previewView =
|
|
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 COMPATIBLE (TextureView) to avoid SurfaceView black frames on some devices.
|
|
81
|
-
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
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]
|
|
91
|
-
Log.d(TAG, "[INIT]
|
|
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
|
|
90
|
+
Log.d(TAG, "[INIT] Adding TextureView to parent...")
|
|
95
91
|
addView(previewView)
|
|
96
|
-
Log.d(TAG, "[INIT]
|
|
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