Showing
17 changed files
with
635 additions
and
470 deletions
| @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner' | @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner' | ||
| 2 | version '1.0-SNAPSHOT' | 2 | version '1.0-SNAPSHOT' |
| 3 | 3 | ||
| 4 | buildscript { | 4 | buildscript { |
| 5 | - ext.kotlin_version = '1.7.20' | 5 | + ext.kotlin_version = '1.7.21' |
| 6 | repositories { | 6 | repositories { |
| 7 | google() | 7 | google() |
| 8 | mavenCentral() | 8 | mavenCentral() |
| 1 | -package dev.steenbakker.mobile_scanner | ||
| 2 | - | ||
| 3 | -import androidx.annotation.IntDef | ||
| 4 | - | ||
| 5 | -@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE) | ||
| 6 | -@Target(AnnotationTarget.FIELD) | ||
| 7 | -@Retention(AnnotationRetention.SOURCE) | ||
| 8 | -annotation class AnalyzeMode { | ||
| 9 | - companion object { | ||
| 10 | - const val NONE = 0 | ||
| 11 | - const val BARCODE = 1 | ||
| 12 | - } | ||
| 13 | -} |
| 1 | +package dev.steenbakker.mobile_scanner | ||
| 2 | + | ||
| 3 | +import android.os.Handler | ||
| 4 | +import android.os.Looper | ||
| 5 | +import io.flutter.embedding.engine.plugins.FlutterPlugin | ||
| 6 | +import io.flutter.plugin.common.EventChannel | ||
| 7 | + | ||
| 8 | +class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler { | ||
| 9 | + | ||
| 10 | + private var eventSink: EventChannel.EventSink? = null | ||
| 11 | + | ||
| 12 | + private val eventChannel = EventChannel( | ||
| 13 | + flutterPluginBinding.binaryMessenger, | ||
| 14 | + "dev.steenbakker.mobile_scanner/scanner/event" | ||
| 15 | + ) | ||
| 16 | + | ||
| 17 | + init { | ||
| 18 | + eventChannel.setStreamHandler(this) | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + fun publishEvent(event: Map<String, Any>) { | ||
| 22 | + Handler(Looper.getMainLooper()).post { | ||
| 23 | + eventSink?.success(event) | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) { | ||
| 28 | + this.eventSink = eventSink | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + override fun onCancel(event: Any?) { | ||
| 32 | + this.eventSink = null | ||
| 33 | + } | ||
| 34 | +} |
| @@ -3,16 +3,10 @@ package dev.steenbakker.mobile_scanner | @@ -3,16 +3,10 @@ package dev.steenbakker.mobile_scanner | ||
| 3 | import android.Manifest | 3 | import android.Manifest |
| 4 | import android.app.Activity | 4 | import android.app.Activity |
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | -import android.graphics.ImageFormat | ||
| 7 | -import android.graphics.Point | ||
| 8 | -import android.graphics.Rect | ||
| 9 | -import android.graphics.YuvImage | ||
| 10 | -import android.media.Image | ||
| 11 | import android.net.Uri | 6 | import android.net.Uri |
| 12 | -import android.util.Log | ||
| 13 | -import android.util.Size | 7 | +import android.os.Handler |
| 8 | +import android.os.Looper | ||
| 14 | import android.view.Surface | 9 | import android.view.Surface |
| 15 | -import androidx.annotation.NonNull | ||
| 16 | import androidx.camera.core.* | 10 | import androidx.camera.core.* |
| 17 | import androidx.camera.lifecycle.ProcessCameraProvider | 11 | import androidx.camera.lifecycle.ProcessCameraProvider |
| 18 | import androidx.core.app.ActivityCompat | 12 | import androidx.core.app.ActivityCompat |
| @@ -20,19 +14,32 @@ import androidx.core.content.ContextCompat | @@ -20,19 +14,32 @@ import androidx.core.content.ContextCompat | ||
| 20 | import androidx.lifecycle.LifecycleOwner | 14 | import androidx.lifecycle.LifecycleOwner |
| 21 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 15 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 22 | import com.google.mlkit.vision.barcode.BarcodeScanning | 16 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 23 | -import com.google.mlkit.vision.barcode.common.Barcode | ||
| 24 | import com.google.mlkit.vision.common.InputImage | 17 | import com.google.mlkit.vision.common.InputImage |
| 25 | -import io.flutter.plugin.common.EventChannel | ||
| 26 | -import io.flutter.plugin.common.MethodCall | ||
| 27 | -import io.flutter.plugin.common.MethodChannel | 18 | +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed |
| 19 | +import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters | ||
| 28 | import io.flutter.plugin.common.PluginRegistry | 20 | import io.flutter.plugin.common.PluginRegistry |
| 29 | import io.flutter.view.TextureRegistry | 21 | import io.flutter.view.TextureRegistry |
| 30 | -import java.io.ByteArrayOutputStream | ||
| 31 | -import java.io.File | ||
| 32 | 22 | ||
| 33 | - | ||
| 34 | -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) : | ||
| 35 | - MethodChannel.MethodCallHandler, EventChannel.StreamHandler, | 23 | +typealias PermissionCallback = (permissionGranted: Boolean) -> Unit |
| 24 | +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit | ||
| 25 | +typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit | ||
| 26 | +typealias MobileScannerErrorCallback = (error: String) -> Unit | ||
| 27 | +typealias TorchStateCallback = (state: Int) -> Unit | ||
| 28 | +typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit | ||
| 29 | + | ||
| 30 | +class NoCamera : Exception() | ||
| 31 | +class AlreadyStarted : Exception() | ||
| 32 | +class AlreadyStopped : Exception() | ||
| 33 | +class TorchError : Exception() | ||
| 34 | +class CameraError : Exception() | ||
| 35 | +class TorchWhenStopped : Exception() | ||
| 36 | + | ||
| 37 | +class MobileScanner( | ||
| 38 | + private val activity: Activity, | ||
| 39 | + private val textureRegistry: TextureRegistry, | ||
| 40 | + private val mobileScannerCallback: MobileScannerCallback, | ||
| 41 | + private val mobileScannerErrorCallback: MobileScannerErrorCallback | ||
| 42 | +) : | ||
| 36 | PluginRegistry.RequestPermissionsResultListener { | 43 | PluginRegistry.RequestPermissionsResultListener { |
| 37 | companion object { | 44 | companion object { |
| 38 | /** | 45 | /** |
| @@ -40,10 +47,8 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -40,10 +47,8 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 40 | * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode | 47 | * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode |
| 41 | */ | 48 | */ |
| 42 | private const val REQUEST_CODE = 0x0786 | 49 | private const val REQUEST_CODE = 0x0786 |
| 43 | - private val TAG = MobileScanner::class.java.simpleName | ||
| 44 | } | 50 | } |
| 45 | 51 | ||
| 46 | - private var sink: EventChannel.EventSink? = null | ||
| 47 | private var listener: PluginRegistry.RequestPermissionsResultListener? = null | 52 | private var listener: PluginRegistry.RequestPermissionsResultListener? = null |
| 48 | 53 | ||
| 49 | private var cameraProvider: ProcessCameraProvider? = null | 54 | private var cameraProvider: ProcessCameraProvider? = null |
| @@ -51,31 +56,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -51,31 +56,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 51 | private var preview: Preview? = null | 56 | private var preview: Preview? = null |
| 52 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 57 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 53 | 58 | ||
| 54 | -// @AnalyzeMode | ||
| 55 | -// private var analyzeMode: Int = AnalyzeMode.NONE | 59 | + private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES |
| 60 | + private var detectionTimeout: Long = 250 | ||
| 61 | + private var lastScanned: List<String?>? = null | ||
| 56 | 62 | ||
| 57 | - @ExperimentalGetImage | ||
| 58 | - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { | ||
| 59 | - when (call.method) { | ||
| 60 | - "state" -> checkPermission(result) | ||
| 61 | - "request" -> requestPermission(result) | ||
| 62 | - "start" -> start(call, result) | ||
| 63 | - "torch" -> toggleTorch(call, result) | ||
| 64 | -// "analyze" -> switchAnalyzeMode(call, result) | ||
| 65 | - "stop" -> stop(result) | ||
| 66 | - "analyzeImage" -> analyzeImage(call, result) | ||
| 67 | - else -> result.notImplemented() | ||
| 68 | - } | ||
| 69 | - } | 63 | + private var scannerTimeout = false |
| 64 | + | ||
| 65 | + private var returnImage = false | ||
| 66 | + | ||
| 67 | + private var scanner = BarcodeScanning.getClient() | ||
| 70 | 68 | ||
| 71 | - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { | ||
| 72 | - this.sink = events | 69 | + /** |
| 70 | + * Check if we already have camera permission. | ||
| 71 | + */ | ||
| 72 | + fun hasCameraPermission(): Int { | ||
| 73 | + // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized | ||
| 74 | + val hasPermission = ContextCompat.checkSelfPermission( | ||
| 75 | + activity, | ||
| 76 | + Manifest.permission.CAMERA | ||
| 77 | + ) == PackageManager.PERMISSION_GRANTED | ||
| 78 | + | ||
| 79 | + return if (hasPermission) { | ||
| 80 | + 1 | ||
| 81 | + } else { | ||
| 82 | + 0 | ||
| 83 | + } | ||
| 73 | } | 84 | } |
| 74 | 85 | ||
| 75 | - override fun onCancel(arguments: Any?) { | ||
| 76 | - sink = null | 86 | + /** |
| 87 | + * Request camera permissions. | ||
| 88 | + */ | ||
| 89 | + fun requestPermission(permissionCallback: PermissionCallback) { | ||
| 90 | + listener | ||
| 91 | + ?: PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults -> | ||
| 92 | + if (requestCode != REQUEST_CODE) { | ||
| 93 | + false | ||
| 94 | + } else { | ||
| 95 | + val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED | ||
| 96 | + permissionCallback(authorized) | ||
| 97 | + true | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + val permissions = arrayOf(Manifest.permission.CAMERA) | ||
| 101 | + ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) | ||
| 77 | } | 102 | } |
| 78 | 103 | ||
| 104 | + /** | ||
| 105 | + * Calls the callback after permissions are requested. | ||
| 106 | + */ | ||
| 79 | override fun onRequestPermissionsResult( | 107 | override fun onRequestPermissionsResult( |
| 80 | requestCode: Int, | 108 | requestCode: Int, |
| 81 | permissions: Array<out String>, | 109 | permissions: Array<out String>, |
| @@ -84,282 +112,161 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -84,282 +112,161 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 84 | return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false | 112 | return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false |
| 85 | } | 113 | } |
| 86 | 114 | ||
| 87 | - private fun checkPermission(result: MethodChannel.Result) { | ||
| 88 | - // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized | ||
| 89 | - val state = | ||
| 90 | - if (ContextCompat.checkSelfPermission( | ||
| 91 | - activity, | ||
| 92 | - Manifest.permission.CAMERA | ||
| 93 | - ) == PackageManager.PERMISSION_GRANTED | ||
| 94 | - ) 1 | ||
| 95 | - else 0 | ||
| 96 | - result.success(state) | ||
| 97 | - } | ||
| 98 | - | ||
| 99 | - private fun requestPermission(result: MethodChannel.Result) { | ||
| 100 | - listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults -> | ||
| 101 | - if (requestCode != REQUEST_CODE) { | ||
| 102 | - false | ||
| 103 | - } else { | ||
| 104 | - val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED | ||
| 105 | - result.success(authorized) | ||
| 106 | - listener = null | ||
| 107 | - true | ||
| 108 | - } | ||
| 109 | - } | ||
| 110 | - val permissions = arrayOf(Manifest.permission.CAMERA) | ||
| 111 | - ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) | ||
| 112 | - } | ||
| 113 | -// var lastScanned: List<Barcode>? = null | ||
| 114 | -// var isAnalyzing: Boolean = false | ||
| 115 | - | 115 | + /** |
| 116 | + * callback for the camera. Every frame is passed through this function. | ||
| 117 | + */ | ||
| 116 | @ExperimentalGetImage | 118 | @ExperimentalGetImage |
| 117 | - val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format | ||
| 118 | - | 119 | + val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format |
| 119 | val mediaImage = imageProxy.image ?: return@Analyzer | 120 | val mediaImage = imageProxy.image ?: return@Analyzer |
| 120 | val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | 121 | val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) |
| 121 | 122 | ||
| 123 | + if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) { | ||
| 124 | + imageProxy.close() | ||
| 125 | + return@Analyzer | ||
| 126 | + } else if (detectionSpeed == DetectionSpeed.NORMAL) { | ||
| 127 | + scannerTimeout = true | ||
| 128 | + } | ||
| 129 | + | ||
| 122 | scanner.process(inputImage) | 130 | scanner.process(inputImage) |
| 123 | .addOnSuccessListener { barcodes -> | 131 | .addOnSuccessListener { barcodes -> |
| 124 | -// if (isAnalyzing) { | ||
| 125 | -// Log.d("scanner", "SKIPPING" ) | ||
| 126 | -// return@addOnSuccessListener | ||
| 127 | -// } | ||
| 128 | -// isAnalyzing = true | 132 | + if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { |
| 133 | + val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue } | ||
| 134 | + if (newScannedBarcodes == lastScanned) { | ||
| 135 | + // New scanned is duplicate, returning | ||
| 136 | + return@addOnSuccessListener | ||
| 137 | + } | ||
| 138 | + lastScanned = newScannedBarcodes | ||
| 139 | + } | ||
| 140 | + | ||
| 129 | val barcodeMap = barcodes.map { barcode -> barcode.data } | 141 | val barcodeMap = barcodes.map { barcode -> barcode.data } |
| 142 | + | ||
| 130 | if (barcodeMap.isNotEmpty()) { | 143 | if (barcodeMap.isNotEmpty()) { |
| 131 | - sink?.success(mapOf( | ||
| 132 | - "name" to "barcode", | ||
| 133 | - "data" to barcodeMap, | ||
| 134 | - "image" to mediaImage.toByteArray() | ||
| 135 | - )) | 144 | + mobileScannerCallback( |
| 145 | + barcodeMap, | ||
| 146 | + if (returnImage) mediaImage.toByteArray() else null | ||
| 147 | + ) | ||
| 136 | } | 148 | } |
| 137 | -// for (barcode in barcodes) { | ||
| 138 | -//// if (lastScanned?.contains(barcodes.first) == true) continue; | ||
| 139 | -// if (lastScanned == null) { | ||
| 140 | -// lastScanned = barcodes | ||
| 141 | -// } else if (lastScanned!!.contains(barcode)) { | ||
| 142 | -// // Duplicate, don't send image | ||
| 143 | -// sink?.success(mapOf( | ||
| 144 | -// "name" to "barcode", | ||
| 145 | -// "data" to barcode.data, | ||
| 146 | -// )) | ||
| 147 | -// } else { | ||
| 148 | -// if (byteArray.isEmpty()) { | ||
| 149 | -// Log.d("scanner", "EMPTY" ) | ||
| 150 | -// return@addOnSuccessListener | ||
| 151 | -// } | ||
| 152 | -// | ||
| 153 | -// Log.d("scanner", "SCANNED IMAGE: $byteArray") | ||
| 154 | -// lastScanned = barcodes; | ||
| 155 | -// | ||
| 156 | -// | ||
| 157 | -// } | ||
| 158 | -// | ||
| 159 | -// } | ||
| 160 | -// isAnalyzing = false | ||
| 161 | } | 149 | } |
| 162 | - .addOnFailureListener { e -> sink?.success(mapOf( | ||
| 163 | - "name" to "error", | ||
| 164 | - "data" to e.localizedMessage | ||
| 165 | - )) } | 150 | + .addOnFailureListener { e -> |
| 151 | + mobileScannerErrorCallback( | ||
| 152 | + e.localizedMessage ?: e.toString() | ||
| 153 | + ) | ||
| 154 | + } | ||
| 166 | .addOnCompleteListener { imageProxy.close() } | 155 | .addOnCompleteListener { imageProxy.close() } |
| 167 | - } | ||
| 168 | - | ||
| 169 | - private fun Image.toByteArray(): ByteArray { | ||
| 170 | - val yBuffer = planes[0].buffer // Y | ||
| 171 | - val vuBuffer = planes[2].buffer // VU | ||
| 172 | - | ||
| 173 | - val ySize = yBuffer.remaining() | ||
| 174 | - val vuSize = vuBuffer.remaining() | ||
| 175 | - | ||
| 176 | - val nv21 = ByteArray(ySize + vuSize) | ||
| 177 | 156 | ||
| 178 | - yBuffer.get(nv21, 0, ySize) | ||
| 179 | - vuBuffer.get(nv21, ySize, vuSize) | ||
| 180 | - | ||
| 181 | - val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) | ||
| 182 | - val out = ByteArrayOutputStream() | ||
| 183 | - yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out) | ||
| 184 | - return out.toByteArray() | 157 | + if (detectionSpeed == DetectionSpeed.NORMAL) { |
| 158 | + // Set timer and continue | ||
| 159 | + Handler(Looper.getMainLooper()).postDelayed({ | ||
| 160 | + scannerTimeout = false | ||
| 161 | + }, detectionTimeout) | ||
| 162 | + } | ||
| 185 | } | 163 | } |
| 186 | 164 | ||
| 187 | - private var scanner = BarcodeScanning.getClient() | ||
| 188 | - | ||
| 189 | - | 165 | + /** |
| 166 | + * Start barcode scanning by initializing the camera and barcode scanner. | ||
| 167 | + */ | ||
| 190 | @ExperimentalGetImage | 168 | @ExperimentalGetImage |
| 191 | - private fun start(call: MethodCall, result: MethodChannel.Result) { | 169 | + fun start( |
| 170 | + barcodeScannerOptions: BarcodeScannerOptions?, | ||
| 171 | + returnImage: Boolean, | ||
| 172 | + cameraPosition: CameraSelector, | ||
| 173 | + torch: Boolean, | ||
| 174 | + detectionSpeed: DetectionSpeed, | ||
| 175 | + torchStateCallback: TorchStateCallback, | ||
| 176 | + mobileScannerStartedCallback: MobileScannerStartedCallback, | ||
| 177 | + detectionTimeout: Long | ||
| 178 | + ) { | ||
| 179 | + this.detectionSpeed = detectionSpeed | ||
| 180 | + this.detectionTimeout = detectionTimeout | ||
| 181 | + this.returnImage = returnImage | ||
| 182 | + | ||
| 192 | if (camera?.cameraInfo != null && preview != null && textureEntry != null) { | 183 | if (camera?.cameraInfo != null && preview != null && textureEntry != null) { |
| 193 | - val resolution = preview!!.resolutionInfo!!.resolution | ||
| 194 | - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 195 | - val width = resolution.width.toDouble() | ||
| 196 | - val height = resolution.height.toDouble() | ||
| 197 | - val size = if (portrait) mapOf( | ||
| 198 | - "width" to width, | ||
| 199 | - "height" to height | ||
| 200 | - ) else mapOf("width" to height, "height" to width) | ||
| 201 | - val answer = mapOf( | ||
| 202 | - "textureId" to textureEntry!!.id(), | ||
| 203 | - "size" to size, | ||
| 204 | - "torchable" to camera!!.cameraInfo.hasFlashUnit() | ||
| 205 | - ) | ||
| 206 | - result.success(answer) | 184 | + throw AlreadyStarted() |
| 185 | + } | ||
| 186 | + | ||
| 187 | + scanner = if (barcodeScannerOptions != null) { | ||
| 188 | + BarcodeScanning.getClient(barcodeScannerOptions) | ||
| 207 | } else { | 189 | } else { |
| 208 | - val facing: Int = call.argument<Int>("facing") ?: 0 | ||
| 209 | - val ratio: Int? = call.argument<Int>("ratio") | ||
| 210 | - val torch: Boolean = call.argument<Boolean>("torch") ?: false | ||
| 211 | - val formats: List<Int>? = call.argument<List<Int>>("formats") | ||
| 212 | -// val analyzerWidth = call.argument<Int>("ratio") | ||
| 213 | -// val analyzeRHEIG = call.argument<Int>("ratio") | ||
| 214 | - | ||
| 215 | - if (formats != null) { | ||
| 216 | - val formatsList: MutableList<Int> = mutableListOf() | ||
| 217 | - for (index in formats) { | ||
| 218 | - formatsList.add(BarcodeFormats.values()[index].intValue) | ||
| 219 | - } | ||
| 220 | - scanner = if (formatsList.size == 1) { | ||
| 221 | - BarcodeScanning.getClient( | ||
| 222 | - BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) | ||
| 223 | - .build() | ||
| 224 | - ) | ||
| 225 | - } else { | ||
| 226 | - BarcodeScanning.getClient( | ||
| 227 | - BarcodeScannerOptions.Builder().setBarcodeFormats( | ||
| 228 | - formatsList.first(), | ||
| 229 | - *formatsList.subList(1, formatsList.size).toIntArray() | ||
| 230 | - ).build() | ||
| 231 | - ) | ||
| 232 | - } | ||
| 233 | - } | 190 | + BarcodeScanning.getClient() |
| 191 | + } | ||
| 234 | 192 | ||
| 235 | - val future = ProcessCameraProvider.getInstance(activity) | ||
| 236 | - val executor = ContextCompat.getMainExecutor(activity) | 193 | + val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) |
| 194 | + val executor = ContextCompat.getMainExecutor(activity) | ||
| 237 | 195 | ||
| 238 | - future.addListener({ | ||
| 239 | - cameraProvider = future.get() | ||
| 240 | - if (cameraProvider == null) { | ||
| 241 | - result.error("cameraProvider", "cameraProvider is null", null) | ||
| 242 | - return@addListener | ||
| 243 | - } | ||
| 244 | - cameraProvider!!.unbindAll() | ||
| 245 | - textureEntry = textureRegistry.createSurfaceTexture() | ||
| 246 | - if (textureEntry == null) { | ||
| 247 | - result.error("textureEntry", "textureEntry is null", null) | ||
| 248 | - return@addListener | ||
| 249 | - } | ||
| 250 | - // Preview | ||
| 251 | - val surfaceProvider = Preview.SurfaceProvider { request -> | ||
| 252 | - val texture = textureEntry!!.surfaceTexture() | ||
| 253 | - texture.setDefaultBufferSize( | ||
| 254 | - request.resolution.width, | ||
| 255 | - request.resolution.height | ||
| 256 | - ) | ||
| 257 | - val surface = Surface(texture) | ||
| 258 | - request.provideSurface(surface, executor) { } | ||
| 259 | - } | 196 | + cameraProviderFuture.addListener({ |
| 197 | + cameraProvider = cameraProviderFuture.get() | ||
| 198 | + if (cameraProvider == null) { | ||
| 199 | + throw CameraError() | ||
| 200 | + } | ||
| 201 | + cameraProvider!!.unbindAll() | ||
| 202 | + textureEntry = textureRegistry.createSurfaceTexture() | ||
| 203 | + | ||
| 204 | + // Preview | ||
| 205 | + val surfaceProvider = Preview.SurfaceProvider { request -> | ||
| 206 | + val texture = textureEntry!!.surfaceTexture() | ||
| 207 | + texture.setDefaultBufferSize( | ||
| 208 | + request.resolution.width, | ||
| 209 | + request.resolution.height | ||
| 210 | + ) | ||
| 260 | 211 | ||
| 261 | - // Build the preview to be shown on the Flutter texture | ||
| 262 | - val previewBuilder = Preview.Builder() | ||
| 263 | - if (ratio != null) { | ||
| 264 | - previewBuilder.setTargetAspectRatio(ratio) | ||
| 265 | - } | ||
| 266 | - preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } | 212 | + val surface = Surface(texture) |
| 213 | + request.provideSurface(surface, executor) { } | ||
| 214 | + } | ||
| 267 | 215 | ||
| 268 | - // Build the analyzer to be passed on to MLKit | ||
| 269 | - val analysisBuilder = ImageAnalysis.Builder() | ||
| 270 | - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | ||
| 271 | - if (ratio != null) { | ||
| 272 | - analysisBuilder.setTargetAspectRatio(ratio) | ||
| 273 | - } | 216 | + // Build the preview to be shown on the Flutter texture |
| 217 | + val previewBuilder = Preview.Builder() | ||
| 218 | + preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } | ||
| 219 | + | ||
| 220 | + // Build the analyzer to be passed on to MLKit | ||
| 221 | + val analysisBuilder = ImageAnalysis.Builder() | ||
| 222 | + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | ||
| 274 | // analysisBuilder.setTargetResolution(Size(1440, 1920)) | 223 | // analysisBuilder.setTargetResolution(Size(1440, 1920)) |
| 275 | - val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } | 224 | + val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) } |
| 276 | 225 | ||
| 277 | - // Select the correct camera | ||
| 278 | - val selector = | ||
| 279 | - if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | 226 | + camera = cameraProvider!!.bindToLifecycle( |
| 227 | + activity as LifecycleOwner, | ||
| 228 | + cameraPosition, | ||
| 229 | + preview, | ||
| 230 | + analysis | ||
| 231 | + ) | ||
| 280 | 232 | ||
| 281 | - camera = cameraProvider!!.bindToLifecycle( | ||
| 282 | - activity as LifecycleOwner, | ||
| 283 | - selector, | ||
| 284 | - preview, | ||
| 285 | - analysis | ||
| 286 | - ) | 233 | + // Register the torch listener |
| 234 | + camera!!.cameraInfo.torchState.observe(activity) { state -> | ||
| 235 | + // TorchState.OFF = 0; TorchState.ON = 1 | ||
| 236 | + torchStateCallback(state) | ||
| 237 | + } | ||
| 287 | 238 | ||
| 288 | - val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 289 | - val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 290 | - Log.i("LOG", "Analyzer: $analysisSize") | ||
| 291 | - Log.i("LOG", "Preview: $previewSize") | 239 | +// val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) |
| 240 | +// val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 241 | +// Log.i("LOG", "Analyzer: $analysisSize") | ||
| 242 | +// Log.i("LOG", "Preview: $previewSize") | ||
| 292 | 243 | ||
| 293 | - if (camera == null) { | ||
| 294 | - result.error("camera", "camera is null", null) | ||
| 295 | - return@addListener | ||
| 296 | - } | 244 | + // Enable torch if provided |
| 245 | + camera!!.cameraControl.enableTorch(torch) | ||
| 297 | 246 | ||
| 298 | - // Register the torch listener | ||
| 299 | - camera!!.cameraInfo.torchState.observe(activity) { state -> | ||
| 300 | - // TorchState.OFF = 0; TorchState.ON = 1 | ||
| 301 | - sink?.success(mapOf("name" to "torchState", "data" to state)) | ||
| 302 | - } | 247 | + val resolution = preview!!.resolutionInfo!!.resolution |
| 248 | + val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 249 | + val width = resolution.width.toDouble() | ||
| 250 | + val height = resolution.height.toDouble() | ||
| 303 | 251 | ||
| 304 | - // Enable torch if provided | ||
| 305 | - camera!!.cameraControl.enableTorch(torch) | ||
| 306 | - | ||
| 307 | - val resolution = preview!!.resolutionInfo!!.resolution | ||
| 308 | - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 309 | - val width = resolution.width.toDouble() | ||
| 310 | - val height = resolution.height.toDouble() | ||
| 311 | - val size = if (portrait) mapOf( | ||
| 312 | - "width" to width, | ||
| 313 | - "height" to height | ||
| 314 | - ) else mapOf("width" to height, "height" to width) | ||
| 315 | - val answer = mapOf( | ||
| 316 | - "textureId" to textureEntry!!.id(), | ||
| 317 | - "size" to size, | ||
| 318 | - "torchable" to camera!!.cameraInfo.hasFlashUnit() | 252 | + mobileScannerStartedCallback( |
| 253 | + MobileScannerStartParameters( | ||
| 254 | + if (portrait) width else height, | ||
| 255 | + if (portrait) height else width, | ||
| 256 | + camera!!.cameraInfo.hasFlashUnit(), | ||
| 257 | + textureEntry!!.id() | ||
| 319 | ) | 258 | ) |
| 320 | - result.success(answer) | ||
| 321 | - }, executor) | ||
| 322 | - } | ||
| 323 | - } | ||
| 324 | - | ||
| 325 | - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | ||
| 326 | - if (camera == null) { | ||
| 327 | - result.error(TAG, "Called toggleTorch() while stopped!", null) | ||
| 328 | - return | ||
| 329 | - } | ||
| 330 | - camera!!.cameraControl.enableTorch(call.arguments == 1) | ||
| 331 | - result.success(null) | ||
| 332 | - } | ||
| 333 | - | ||
| 334 | -// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) { | ||
| 335 | -// analyzeMode = call.arguments as Int | ||
| 336 | -// result.success(null) | ||
| 337 | -// } | ||
| 338 | - | ||
| 339 | - private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | ||
| 340 | - val uri = Uri.fromFile(File(call.arguments.toString())) | ||
| 341 | - val inputImage = InputImage.fromFilePath(activity, uri) | ||
| 342 | - | ||
| 343 | - var barcodeFound = false | ||
| 344 | - scanner.process(inputImage) | ||
| 345 | - .addOnSuccessListener { barcodes -> | ||
| 346 | - for (barcode in barcodes) { | ||
| 347 | - barcodeFound = true | ||
| 348 | - sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) | ||
| 349 | - } | ||
| 350 | - } | ||
| 351 | - .addOnFailureListener { e -> | ||
| 352 | - Log.e(TAG, e.message, e) | ||
| 353 | - result.error(TAG, e.message, e) | ||
| 354 | - } | ||
| 355 | - .addOnCompleteListener { result.success(barcodeFound) } | 259 | + ) |
| 260 | + }, executor) | ||
| 356 | 261 | ||
| 357 | } | 262 | } |
| 358 | 263 | ||
| 359 | - private fun stop(result: MethodChannel.Result) { | 264 | + /** |
| 265 | + * Stop barcode scanning. | ||
| 266 | + */ | ||
| 267 | + fun stop() { | ||
| 360 | if (camera == null && preview == null) { | 268 | if (camera == null && preview == null) { |
| 361 | - result.error(TAG, "Called stop() while already stopped!", null) | ||
| 362 | - return | 269 | + throw AlreadyStopped() |
| 363 | } | 270 | } |
| 364 | 271 | ||
| 365 | val owner = activity as LifecycleOwner | 272 | val owner = activity as LifecycleOwner |
| @@ -367,81 +274,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -367,81 +274,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 367 | cameraProvider?.unbindAll() | 274 | cameraProvider?.unbindAll() |
| 368 | textureEntry?.release() | 275 | textureEntry?.release() |
| 369 | 276 | ||
| 370 | -// analyzeMode = AnalyzeMode.NONE | ||
| 371 | camera = null | 277 | camera = null |
| 372 | preview = null | 278 | preview = null |
| 373 | textureEntry = null | 279 | textureEntry = null |
| 374 | cameraProvider = null | 280 | cameraProvider = null |
| 281 | + } | ||
| 375 | 282 | ||
| 376 | - result.success(null) | 283 | + /** |
| 284 | + * Toggles the flash light on or off. | ||
| 285 | + */ | ||
| 286 | + fun toggleTorch(enableTorch: Boolean) { | ||
| 287 | + if (camera == null) { | ||
| 288 | + throw TorchWhenStopped() | ||
| 289 | + } | ||
| 290 | + camera!!.cameraControl.enableTorch(enableTorch) | ||
| 377 | } | 291 | } |
| 378 | 292 | ||
| 293 | + /** | ||
| 294 | + * Analyze a single image. | ||
| 295 | + */ | ||
| 296 | + fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) { | ||
| 297 | + val inputImage = InputImage.fromFilePath(activity, image) | ||
| 298 | + | ||
| 299 | + scanner.process(inputImage) | ||
| 300 | + .addOnSuccessListener { barcodes -> | ||
| 301 | + val barcodeMap = barcodes.map { barcode -> barcode.data } | ||
| 302 | + | ||
| 303 | + if (barcodeMap.isNotEmpty()) { | ||
| 304 | + analyzerCallback(barcodeMap) | ||
| 305 | + } else { | ||
| 306 | + analyzerCallback(null) | ||
| 307 | + } | ||
| 308 | + } | ||
| 309 | + .addOnFailureListener { e -> | ||
| 310 | + mobileScannerErrorCallback( | ||
| 311 | + e.localizedMessage ?: e.toString() | ||
| 312 | + ) | ||
| 313 | + } | ||
| 314 | + } | ||
| 379 | 315 | ||
| 380 | - private val Barcode.data: Map<String, Any?> | ||
| 381 | - get() = mapOf( | ||
| 382 | - "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 383 | - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 384 | - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 385 | - "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 386 | - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 387 | - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue | ||
| 388 | - ) | ||
| 389 | - | ||
| 390 | - private val Point.data: Map<String, Double> | ||
| 391 | - get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | ||
| 392 | - | ||
| 393 | - private val Barcode.CalendarEvent.data: Map<String, Any?> | ||
| 394 | - get() = mapOf( | ||
| 395 | - "description" to description, "end" to end?.rawValue, "location" to location, | ||
| 396 | - "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 397 | - "summary" to summary | ||
| 398 | - ) | ||
| 399 | - | ||
| 400 | - private val Barcode.ContactInfo.data: Map<String, Any?> | ||
| 401 | - get() = mapOf( | ||
| 402 | - "addresses" to addresses.map { address -> address.data }, | ||
| 403 | - "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 404 | - "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 405 | - "title" to title, "urls" to urls | ||
| 406 | - ) | ||
| 407 | - | ||
| 408 | - private val Barcode.Address.data: Map<String, Any?> | ||
| 409 | - get() = mapOf( | ||
| 410 | - "addressLines" to addressLines.map { addressLine -> addressLine.toString() }, | ||
| 411 | - "type" to type | ||
| 412 | - ) | ||
| 413 | - | ||
| 414 | - private val Barcode.PersonName.data: Map<String, Any?> | ||
| 415 | - get() = mapOf( | ||
| 416 | - "first" to first, "formattedName" to formattedName, "last" to last, | ||
| 417 | - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 418 | - "suffix" to suffix | ||
| 419 | - ) | ||
| 420 | - | ||
| 421 | - private val Barcode.DriverLicense.data: Map<String, Any?> | ||
| 422 | - get() = mapOf( | ||
| 423 | - "addressCity" to addressCity, "addressState" to addressState, | ||
| 424 | - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 425 | - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 426 | - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 427 | - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName | ||
| 428 | - ) | ||
| 429 | - | ||
| 430 | - private val Barcode.Email.data: Map<String, Any?> | ||
| 431 | - get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | ||
| 432 | - | ||
| 433 | - private val Barcode.GeoPoint.data: Map<String, Any?> | ||
| 434 | - get() = mapOf("latitude" to lat, "longitude" to lng) | ||
| 435 | - | ||
| 436 | - private val Barcode.Phone.data: Map<String, Any?> | ||
| 437 | - get() = mapOf("number" to number, "type" to type) | ||
| 438 | - | ||
| 439 | - private val Barcode.Sms.data: Map<String, Any?> | ||
| 440 | - get() = mapOf("message" to message, "phoneNumber" to phoneNumber) | ||
| 441 | - | ||
| 442 | - private val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 443 | - get() = mapOf("title" to title, "url" to url) | ||
| 444 | - | ||
| 445 | - private val Barcode.WiFi.data: Map<String, Any?> | ||
| 446 | - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) | ||
| 447 | -} | ||
| 316 | +} |
| 1 | package dev.steenbakker.mobile_scanner | 1 | package dev.steenbakker.mobile_scanner |
| 2 | 2 | ||
| 3 | -import androidx.annotation.NonNull | 3 | +import android.net.Uri |
| 4 | +import androidx.camera.core.CameraSelector | ||
| 5 | +import androidx.camera.core.ExperimentalGetImage | ||
| 6 | +import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||
| 7 | +import dev.steenbakker.mobile_scanner.objects.BarcodeFormats | ||
| 8 | +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed | ||
| 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin | 9 | import io.flutter.embedding.engine.plugins.FlutterPlugin |
| 5 | import io.flutter.embedding.engine.plugins.activity.ActivityAware | 10 | import io.flutter.embedding.engine.plugins.activity.ActivityAware |
| 6 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding | 11 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding |
| 7 | -import io.flutter.plugin.common.EventChannel | 12 | +import io.flutter.plugin.common.MethodCall |
| 8 | import io.flutter.plugin.common.MethodChannel | 13 | import io.flutter.plugin.common.MethodChannel |
| 14 | +import java.io.File | ||
| 9 | 15 | ||
| 10 | /** MobileScannerPlugin */ | 16 | /** MobileScannerPlugin */ |
| 11 | -class MobileScannerPlugin : FlutterPlugin, ActivityAware { | ||
| 12 | - private var flutter: FlutterPlugin.FlutterPluginBinding? = null | ||
| 13 | - private var activity: ActivityPluginBinding? = null | 17 | +class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler { |
| 18 | + | ||
| 19 | + private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null | ||
| 20 | + private var activityPluginBinding: ActivityPluginBinding? = null | ||
| 14 | private var handler: MobileScanner? = null | 21 | private var handler: MobileScanner? = null |
| 15 | private var method: MethodChannel? = null | 22 | private var method: MethodChannel? = null |
| 16 | - private var event: EventChannel? = null | ||
| 17 | 23 | ||
| 18 | - override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 19 | - this.flutter = binding | 24 | + private lateinit var barcodeHandler: BarcodeHandler |
| 25 | + | ||
| 26 | + private var permissionResult: MethodChannel.Result? = null | ||
| 27 | + private var analyzerResult: MethodChannel.Result? = null | ||
| 28 | + | ||
| 29 | + private val permissionCallback: PermissionCallback = {hasPermission: Boolean -> | ||
| 30 | + permissionResult?.success(hasPermission) | ||
| 31 | + permissionResult = null | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? -> | ||
| 35 | + if (image != null) { | ||
| 36 | + barcodeHandler.publishEvent(mapOf( | ||
| 37 | + "name" to "barcode", | ||
| 38 | + "data" to barcodes, | ||
| 39 | + "image" to image | ||
| 40 | + )) | ||
| 41 | + } else { | ||
| 42 | + barcodeHandler.publishEvent(mapOf( | ||
| 43 | + "name" to "barcode", | ||
| 44 | + "data" to barcodes | ||
| 45 | + )) | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?-> | ||
| 50 | + if (barcodes != null) { | ||
| 51 | + barcodeHandler.publishEvent(mapOf( | ||
| 52 | + "name" to "barcode", | ||
| 53 | + "data" to barcodes | ||
| 54 | + )) | ||
| 55 | + analyzerResult?.success(true) | ||
| 56 | + } else { | ||
| 57 | + analyzerResult?.success(false) | ||
| 58 | + } | ||
| 59 | + analyzerResult = null | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + private val errorCallback: MobileScannerErrorCallback = {error: String -> | ||
| 63 | + barcodeHandler.publishEvent(mapOf( | ||
| 64 | + "name" to "error", | ||
| 65 | + "data" to error, | ||
| 66 | + )) | ||
| 20 | } | 67 | } |
| 21 | 68 | ||
| 22 | - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 23 | - this.flutter = null | 69 | + private val torchStateCallback: TorchStateCallback = {state: Int -> |
| 70 | + barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) | ||
| 24 | } | 71 | } |
| 25 | 72 | ||
| 26 | - override fun onAttachedToActivity(binding: ActivityPluginBinding) { | ||
| 27 | - activity = binding | ||
| 28 | - handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry) | ||
| 29 | - method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method") | ||
| 30 | - event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event") | ||
| 31 | - method!!.setMethodCallHandler(handler) | ||
| 32 | - event!!.setStreamHandler(handler) | ||
| 33 | - activity!!.addRequestPermissionsResultListener(handler!!) | 73 | + @ExperimentalGetImage |
| 74 | + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { | ||
| 75 | + if (handler == null) { | ||
| 76 | + result.error("MobileScanner", "Called ${call.method} before initializing.", null) | ||
| 77 | + return | ||
| 78 | + } | ||
| 79 | + when (call.method) { | ||
| 80 | + "state" -> result.success(handler!!.hasCameraPermission()) | ||
| 81 | + "request" -> requestPermission(result) | ||
| 82 | + "start" -> start(call, result) | ||
| 83 | + "torch" -> toggleTorch(call, result) | ||
| 84 | + "stop" -> stop(result) | ||
| 85 | + "analyzeImage" -> analyzeImage(call, result) | ||
| 86 | + else -> result.notImplemented() | ||
| 87 | + } | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 91 | + method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method") | ||
| 92 | + method!!.setMethodCallHandler(this) | ||
| 93 | + | ||
| 94 | + barcodeHandler = BarcodeHandler(binding) | ||
| 95 | + | ||
| 96 | + this.flutterPluginBinding = binding | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 100 | + this.flutterPluginBinding = null | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { | ||
| 104 | + handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback | ||
| 105 | + ) | ||
| 106 | + activityPluginBinding.addRequestPermissionsResultListener(handler!!) | ||
| 107 | + | ||
| 108 | + this.activityPluginBinding = activityPluginBinding | ||
| 34 | } | 109 | } |
| 35 | 110 | ||
| 36 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { | 111 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { |
| @@ -38,16 +113,117 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | @@ -38,16 +113,117 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | ||
| 38 | } | 113 | } |
| 39 | 114 | ||
| 40 | override fun onDetachedFromActivity() { | 115 | override fun onDetachedFromActivity() { |
| 41 | - activity!!.removeRequestPermissionsResultListener(handler!!) | ||
| 42 | - event!!.setStreamHandler(null) | 116 | + activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!) |
| 43 | method!!.setMethodCallHandler(null) | 117 | method!!.setMethodCallHandler(null) |
| 44 | - event = null | ||
| 45 | method = null | 118 | method = null |
| 46 | handler = null | 119 | handler = null |
| 47 | - activity = null | 120 | + activityPluginBinding = null |
| 48 | } | 121 | } |
| 49 | 122 | ||
| 50 | override fun onDetachedFromActivityForConfigChanges() { | 123 | override fun onDetachedFromActivityForConfigChanges() { |
| 51 | onDetachedFromActivity() | 124 | onDetachedFromActivity() |
| 52 | } | 125 | } |
| 126 | + | ||
| 127 | + private fun requestPermission(result: MethodChannel.Result) { | ||
| 128 | + permissionResult = result | ||
| 129 | + handler!!.requestPermission(permissionCallback) | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + @ExperimentalGetImage | ||
| 133 | + private fun start(call: MethodCall, result: MethodChannel.Result) { | ||
| 134 | + val torch: Boolean = call.argument<Boolean>("torch") ?: false | ||
| 135 | + val facing: Int = call.argument<Int>("facing") ?: 0 | ||
| 136 | + val formats: List<Int>? = call.argument<List<Int>>("formats") | ||
| 137 | + val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false | ||
| 138 | + val speed: Int = call.argument<Int>("speed") ?: 1 | ||
| 139 | + val timeout: Int = call.argument<Int>("timeout") ?: 250 | ||
| 140 | + | ||
| 141 | + var barcodeScannerOptions: BarcodeScannerOptions? = null | ||
| 142 | + if (formats != null) { | ||
| 143 | + val formatsList: MutableList<Int> = mutableListOf() | ||
| 144 | + for (index in formats) { | ||
| 145 | + formatsList.add(BarcodeFormats.values()[index].intValue) | ||
| 146 | + } | ||
| 147 | + barcodeScannerOptions = if (formatsList.size == 1) { | ||
| 148 | + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) | ||
| 149 | + .build() | ||
| 150 | + } else { | ||
| 151 | + BarcodeScannerOptions.Builder().setBarcodeFormats( | ||
| 152 | + formatsList.first(), | ||
| 153 | + *formatsList.subList(1, formatsList.size).toIntArray() | ||
| 154 | + ).build() | ||
| 155 | + } | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + val position = | ||
| 159 | + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 160 | + | ||
| 161 | + val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} | ||
| 162 | + | ||
| 163 | + try { | ||
| 164 | + handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = { | ||
| 165 | + result.success(mapOf( | ||
| 166 | + "textureId" to it.id, | ||
| 167 | + "size" to mapOf("width" to it.width, "height" to it.height), | ||
| 168 | + "torchable" to it.hasFlashUnit | ||
| 169 | + )) | ||
| 170 | + }, | ||
| 171 | + timeout.toLong()) | ||
| 172 | + | ||
| 173 | + } catch (e: AlreadyStarted) { | ||
| 174 | + result.error( | ||
| 175 | + "MobileScanner", | ||
| 176 | + "Called start() while already started", | ||
| 177 | + null | ||
| 178 | + ) | ||
| 179 | + } catch (e: NoCamera) { | ||
| 180 | + result.error( | ||
| 181 | + "MobileScanner", | ||
| 182 | + "No camera found or failed to open camera!", | ||
| 183 | + null | ||
| 184 | + ) | ||
| 185 | + } catch (e: TorchError) { | ||
| 186 | + result.error( | ||
| 187 | + "MobileScanner", | ||
| 188 | + "Error occurred when setting torch!", | ||
| 189 | + null | ||
| 190 | + ) | ||
| 191 | + } catch (e: CameraError) { | ||
| 192 | + result.error( | ||
| 193 | + "MobileScanner", | ||
| 194 | + "Error occurred when setting up camera!", | ||
| 195 | + null | ||
| 196 | + ) | ||
| 197 | + } catch (e: Exception) { | ||
| 198 | + result.error( | ||
| 199 | + "MobileScanner", | ||
| 200 | + "Unknown error occurred..", | ||
| 201 | + null | ||
| 202 | + ) | ||
| 203 | + } | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + private fun stop(result: MethodChannel.Result) { | ||
| 207 | + try { | ||
| 208 | + handler!!.stop() | ||
| 209 | + result.success(null) | ||
| 210 | + } catch (e: AlreadyStopped) { | ||
| 211 | + result.error("MobileScanner", "Called stop() while already stopped!", null) | ||
| 212 | + } | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | ||
| 216 | + analyzerResult = result | ||
| 217 | + val uri = Uri.fromFile(File(call.arguments.toString())) | ||
| 218 | + handler!!.analyzeImage(uri, analyzerCallback) | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | ||
| 222 | + try { | ||
| 223 | + handler!!.toggleTorch(call.arguments == 1) | ||
| 224 | + result.success(null) | ||
| 225 | + } catch (e: AlreadyStopped) { | ||
| 226 | + result.error("MobileScanner", "Called toggleTorch() while stopped!", null) | ||
| 227 | + } | ||
| 228 | + } | ||
| 53 | } | 229 | } |
| 1 | +package dev.steenbakker.mobile_scanner | ||
| 2 | + | ||
| 3 | +import android.graphics.ImageFormat | ||
| 4 | +import android.graphics.Point | ||
| 5 | +import android.graphics.Rect | ||
| 6 | +import android.graphics.YuvImage | ||
| 7 | +import android.media.Image | ||
| 8 | +import com.google.mlkit.vision.barcode.common.Barcode | ||
| 9 | +import java.io.ByteArrayOutputStream | ||
| 10 | + | ||
| 11 | +fun Image.toByteArray(): ByteArray { | ||
| 12 | + val yBuffer = planes[0].buffer // Y | ||
| 13 | + val vuBuffer = planes[2].buffer // VU | ||
| 14 | + | ||
| 15 | + val ySize = yBuffer.remaining() | ||
| 16 | + val vuSize = vuBuffer.remaining() | ||
| 17 | + | ||
| 18 | + val nv21 = ByteArray(ySize + vuSize) | ||
| 19 | + | ||
| 20 | + yBuffer.get(nv21, 0, ySize) | ||
| 21 | + vuBuffer.get(nv21, ySize, vuSize) | ||
| 22 | + | ||
| 23 | + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) | ||
| 24 | + val out = ByteArrayOutputStream() | ||
| 25 | + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out) | ||
| 26 | + return out.toByteArray() | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +val Barcode.data: Map<String, Any?> | ||
| 30 | + get() = mapOf( | ||
| 31 | + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 32 | + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 33 | + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 34 | + "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 35 | + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 36 | + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue | ||
| 37 | + ) | ||
| 38 | + | ||
| 39 | +private val Point.data: Map<String, Double> | ||
| 40 | + get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | ||
| 41 | + | ||
| 42 | +private val Barcode.CalendarEvent.data: Map<String, Any?> | ||
| 43 | + get() = mapOf( | ||
| 44 | + "description" to description, "end" to end?.rawValue, "location" to location, | ||
| 45 | + "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 46 | + "summary" to summary | ||
| 47 | + ) | ||
| 48 | + | ||
| 49 | +private val Barcode.ContactInfo.data: Map<String, Any?> | ||
| 50 | + get() = mapOf( | ||
| 51 | + "addresses" to addresses.map { address -> address.data }, | ||
| 52 | + "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 53 | + "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 54 | + "title" to title, "urls" to urls | ||
| 55 | + ) | ||
| 56 | + | ||
| 57 | +private val Barcode.Address.data: Map<String, Any?> | ||
| 58 | + get() = mapOf( | ||
| 59 | + "addressLines" to addressLines.map { addressLine -> addressLine.toString() }, | ||
| 60 | + "type" to type | ||
| 61 | + ) | ||
| 62 | + | ||
| 63 | +private val Barcode.PersonName.data: Map<String, Any?> | ||
| 64 | + get() = mapOf( | ||
| 65 | + "first" to first, "formattedName" to formattedName, "last" to last, | ||
| 66 | + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 67 | + "suffix" to suffix | ||
| 68 | + ) | ||
| 69 | + | ||
| 70 | +private val Barcode.DriverLicense.data: Map<String, Any?> | ||
| 71 | + get() = mapOf( | ||
| 72 | + "addressCity" to addressCity, "addressState" to addressState, | ||
| 73 | + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 74 | + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 75 | + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 76 | + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName | ||
| 77 | + ) | ||
| 78 | + | ||
| 79 | +private val Barcode.Email.data: Map<String, Any?> | ||
| 80 | + get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | ||
| 81 | + | ||
| 82 | +private val Barcode.GeoPoint.data: Map<String, Any?> | ||
| 83 | + get() = mapOf("latitude" to lat, "longitude" to lng) | ||
| 84 | + | ||
| 85 | +private val Barcode.Phone.data: Map<String, Any?> | ||
| 86 | + get() = mapOf("number" to number, "type" to type) | ||
| 87 | + | ||
| 88 | +private val Barcode.Sms.data: Map<String, Any?> | ||
| 89 | + get() = mapOf("message" to message, "phoneNumber" to phoneNumber) | ||
| 90 | + | ||
| 91 | +private val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 92 | + get() = mapOf("title" to title, "url" to url) | ||
| 93 | + | ||
| 94 | +private val Barcode.WiFi.data: Map<String, Any?> | ||
| 95 | + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) |
android/src/main/kotlin/dev/steenbakker/mobile_scanner/exceptions/NoPermissionException.kt
deleted
100644 → 0
| 1 | -package dev.steenbakker.mobile_scanner.exceptions | ||
| 2 | - | ||
| 3 | -internal class NoPermissionException : RuntimeException() | ||
| 4 | - | ||
| 5 | -//internal class Exception(val reason: Reason) : | ||
| 6 | -// java.lang.Exception("Mobile Scanner failed because $reason") { | ||
| 7 | -// | ||
| 8 | -// internal enum class Reason { | ||
| 9 | -// noHardware, noPermissions, noBackCamera | ||
| 10 | -// } | ||
| 11 | -//} |
| 1 | -package dev.steenbakker.mobile_scanner | ||
| 2 | - | ||
| 3 | -import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 1 | +package dev.steenbakker.mobile_scanner.objects |
| 4 | 2 | ||
| 5 | enum class BarcodeFormats(val intValue: Int) { | 3 | enum class BarcodeFormats(val intValue: Int) { |
| 6 | UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN), | 4 | UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN), |
ios/Classes/DetectionSpeed.swift
0 → 100644
| @@ -49,7 +49,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -49,7 +49,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 49 | super.init() | 49 | super.init() |
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | - /// Check permissions for video | 52 | + /// Check if we already have camera permission. |
| 53 | func checkPermission() -> Int { | 53 | func checkPermission() -> Int { |
| 54 | let status = AVCaptureDevice.authorizationStatus(for: .video) | 54 | let status = AVCaptureDevice.authorizationStatus(for: .video) |
| 55 | switch status { | 55 | switch status { |
| @@ -66,6 +66,44 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -66,6 +66,44 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 66 | func requestPermission(_ result: @escaping FlutterResult) { | 66 | func requestPermission(_ result: @escaping FlutterResult) { |
| 67 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | 67 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) |
| 68 | } | 68 | } |
| 69 | + | ||
| 70 | + /// Gets called when a new image is added to the buffer | ||
| 71 | + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| 72 | + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | ||
| 73 | + print("Failed to get image buffer from sample buffer.") | ||
| 74 | + return | ||
| 75 | + } | ||
| 76 | + latestBuffer = imageBuffer | ||
| 77 | + registry?.textureFrameAvailable(textureId) | ||
| 78 | + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) { | ||
| 79 | + i = 0 | ||
| 80 | + let ciImage = latestBuffer.image | ||
| 81 | + | ||
| 82 | + let image = VisionImage(image: ciImage) | ||
| 83 | + image.orientation = imageOrientation( | ||
| 84 | + deviceOrientation: UIDevice.current.orientation, | ||
| 85 | + defaultOrientation: .portrait, | ||
| 86 | + position: videoPosition | ||
| 87 | + ) | ||
| 88 | + | ||
| 89 | + scanner.process(image) { [self] barcodes, error in | ||
| 90 | + if (detectionSpeed == DetectionSpeed.noDuplicates) { | ||
| 91 | + let newScannedBarcodes = barcodes?.map { barcode in | ||
| 92 | + return barcode.rawValue | ||
| 93 | + } | ||
| 94 | + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | ||
| 95 | + return | ||
| 96 | + } else { | ||
| 97 | + barcodesString = newScannedBarcodes | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + mobileScannerCallback(barcodes, error, ciImage) | ||
| 102 | + } | ||
| 103 | + } else { | ||
| 104 | + i+=1 | ||
| 105 | + } | ||
| 106 | + } | ||
| 69 | 107 | ||
| 70 | /// Start scanning for barcodes | 108 | /// Start scanning for barcodes |
| 71 | func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { | 109 | func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { |
| @@ -136,13 +174,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -136,13 +174,6 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 136 | return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) | 174 | return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) |
| 137 | } | 175 | } |
| 138 | 176 | ||
| 139 | - struct MobileScannerStartParameters { | ||
| 140 | - var width: Double = 0.0 | ||
| 141 | - var height: Double = 0.0 | ||
| 142 | - var hasTorch = false | ||
| 143 | - var textureId: Int64 = 0 | ||
| 144 | - } | ||
| 145 | - | ||
| 146 | /// Stop scanning for barcodes | 177 | /// Stop scanning for barcodes |
| 147 | func stop() throws { | 178 | func stop() throws { |
| 148 | if (device == nil) { | 179 | if (device == nil) { |
| @@ -192,43 +223,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -192,43 +223,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 192 | 223 | ||
| 193 | var barcodesString: Array<String?>? | 224 | var barcodesString: Array<String?>? |
| 194 | 225 | ||
| 195 | - /// Gets called when a new image is added to the buffer | ||
| 196 | - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| 197 | - guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | ||
| 198 | - print("Failed to get image buffer from sample buffer.") | ||
| 199 | - return | ||
| 200 | - } | ||
| 201 | - latestBuffer = imageBuffer | ||
| 202 | - registry?.textureFrameAvailable(textureId) | ||
| 203 | - if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) { | ||
| 204 | - i = 0 | ||
| 205 | - let ciImage = latestBuffer.image | ||
| 206 | 226 | ||
| 207 | - let image = VisionImage(image: ciImage) | ||
| 208 | - image.orientation = imageOrientation( | ||
| 209 | - deviceOrientation: UIDevice.current.orientation, | ||
| 210 | - defaultOrientation: .portrait, | ||
| 211 | - position: videoPosition | ||
| 212 | - ) | ||
| 213 | - | ||
| 214 | - scanner.process(image) { [self] barcodes, error in | ||
| 215 | - if (detectionSpeed == DetectionSpeed.noDuplicates) { | ||
| 216 | - let newScannedBarcodes = barcodes?.map { barcode in | ||
| 217 | - return barcode.rawValue | ||
| 218 | - } | ||
| 219 | - if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | ||
| 220 | - return | ||
| 221 | - } else { | ||
| 222 | - barcodesString = newScannedBarcodes | ||
| 223 | - } | ||
| 224 | - } | ||
| 225 | - | ||
| 226 | - mobileScannerCallback(barcodes, error, ciImage) | ||
| 227 | - } | ||
| 228 | - } else { | ||
| 229 | - i+=1 | ||
| 230 | - } | ||
| 231 | - } | ||
| 232 | 227 | ||
| 233 | // /// Convert image buffer to jpeg | 228 | // /// Convert image buffer to jpeg |
| 234 | // private func ciImageToJpeg(ciImage: CIImage) -> Data { | 229 | // private func ciImageToJpeg(ciImage: CIImage) -> Data { |
| @@ -270,6 +265,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -270,6 +265,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 270 | } | 265 | } |
| 271 | return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) | 266 | return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) |
| 272 | } | 267 | } |
| 273 | - | 268 | + |
| 269 | + struct MobileScannerStartParameters { | ||
| 270 | + var width: Double = 0.0 | ||
| 271 | + var height: Double = 0.0 | ||
| 272 | + var hasTorch = false | ||
| 273 | + var textureId: Int64 = 0 | ||
| 274 | + } | ||
| 274 | } | 275 | } |
| 275 | 276 |
| @@ -56,11 +56,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -56,11 +56,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 56 | 56 | ||
| 57 | /// Parses all parameters and starts the mobileScanner | 57 | /// Parses all parameters and starts the mobileScanner |
| 58 | private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 58 | private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 59 | - // let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int | ||
| 60 | let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false | 59 | let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false |
| 61 | let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1 | 60 | let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1 |
| 62 | let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? [] | 61 | let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? [] |
| 63 | let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false | 62 | let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false |
| 63 | + let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0 | ||
| 64 | 64 | ||
| 65 | let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} | 65 | let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} |
| 66 | var barcodeOptions: BarcodeScannerOptions? = nil | 66 | var barcodeOptions: BarcodeScannerOptions? = nil |
| @@ -75,10 +75,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -75,10 +75,10 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 75 | 75 | ||
| 76 | 76 | ||
| 77 | let position = facing == 0 ? AVCaptureDevice.Position.front : .back | 77 | let position = facing == 0 ? AVCaptureDevice.Position.front : .back |
| 78 | - let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)! | 78 | + let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! |
| 79 | 79 | ||
| 80 | do { | 80 | do { |
| 81 | - let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed) | 81 | + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed) |
| 82 | result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch]) | 82 | result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch]) |
| 83 | } catch MobileScannerError.alreadyStarted { | 83 | } catch MobileScannerError.alreadyStarted { |
| 84 | result(FlutterError(code: "MobileScanner", | 84 | result(FlutterError(code: "MobileScanner", |
| @@ -90,7 +90,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -90,7 +90,7 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 90 | details: nil)) | 90 | details: nil)) |
| 91 | } catch MobileScannerError.torchError(let error) { | 91 | } catch MobileScannerError.torchError(let error) { |
| 92 | result(FlutterError(code: "MobileScanner", | 92 | result(FlutterError(code: "MobileScanner", |
| 93 | - message: "Error occured when setting toch!", | 93 | + message: "Error occured when setting torch!", |
| 94 | details: error)) | 94 | details: error)) |
| 95 | } catch MobileScannerError.cameraError(let error) { | 95 | } catch MobileScannerError.cameraError(let error) { |
| 96 | result(FlutterError(code: "MobileScanner", | 96 | result(FlutterError(code: "MobileScanner", |
| @@ -162,9 +162,3 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | @@ -162,9 +162,3 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 162 | } | 162 | } |
| 163 | } | 163 | } |
| 164 | } | 164 | } |
| 165 | - | ||
| 166 | -enum DetectionSpeed: Int { | ||
| 167 | - case noDuplicates = 0 | ||
| 168 | - case normal = 1 | ||
| 169 | - case unrestricted = 2 | ||
| 170 | -} |
| @@ -4,7 +4,7 @@ enum DetectionSpeed { | @@ -4,7 +4,7 @@ enum DetectionSpeed { | ||
| 4 | /// barcode has been scanned. | 4 | /// barcode has been scanned. |
| 5 | noDuplicates, | 5 | noDuplicates, |
| 6 | 6 | ||
| 7 | - /// Front facing camera. | 7 | + /// The barcode scanner will wait |
| 8 | normal, | 8 | normal, |
| 9 | 9 | ||
| 10 | /// Back facing camera. | 10 | /// Back facing camera. |
| @@ -13,11 +13,10 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | @@ -13,11 +13,10 @@ import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | ||
| 13 | class MobileScannerController { | 13 | class MobileScannerController { |
| 14 | MobileScannerController({ | 14 | MobileScannerController({ |
| 15 | this.facing = CameraFacing.back, | 15 | this.facing = CameraFacing.back, |
| 16 | - this.detectionSpeed = DetectionSpeed.noDuplicates, | ||
| 17 | - // this.ratio, | 16 | + this.detectionSpeed = DetectionSpeed.normal, |
| 17 | + this.detectionTimeoutMs = 250, | ||
| 18 | this.torchEnabled = false, | 18 | this.torchEnabled = false, |
| 19 | this.formats, | 19 | this.formats, |
| 20 | - // this.autoResume = true, | ||
| 21 | this.returnImage = false, | 20 | this.returnImage = false, |
| 22 | this.onPermissionSet, | 21 | this.onPermissionSet, |
| 23 | }) { | 22 | }) { |
| @@ -39,11 +38,6 @@ class MobileScannerController { | @@ -39,11 +38,6 @@ class MobileScannerController { | ||
| 39 | /// Default: CameraFacing.back | 38 | /// Default: CameraFacing.back |
| 40 | final CameraFacing facing; | 39 | final CameraFacing facing; |
| 41 | 40 | ||
| 42 | - // /// Analyze the image in 4:3 or 16:9 | ||
| 43 | - // /// | ||
| 44 | - // /// Only on Android | ||
| 45 | - // final Ratio? ratio; | ||
| 46 | - | ||
| 47 | /// Enable or disable the torch (Flash) on start | 41 | /// Enable or disable the torch (Flash) on start |
| 48 | /// | 42 | /// |
| 49 | /// Default: disabled | 43 | /// Default: disabled |
| @@ -62,6 +56,8 @@ class MobileScannerController { | @@ -62,6 +56,8 @@ class MobileScannerController { | ||
| 62 | /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices | 56 | /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices |
| 63 | final DetectionSpeed detectionSpeed; | 57 | final DetectionSpeed detectionSpeed; |
| 64 | 58 | ||
| 59 | + final int detectionTimeoutMs; | ||
| 60 | + | ||
| 65 | /// Sets the barcode stream | 61 | /// Sets the barcode stream |
| 66 | final StreamController<BarcodeCapture> _barcodesController = | 62 | final StreamController<BarcodeCapture> _barcodesController = |
| 67 | StreamController.broadcast(); | 63 | StreamController.broadcast(); |
| @@ -97,10 +93,9 @@ class MobileScannerController { | @@ -97,10 +93,9 @@ class MobileScannerController { | ||
| 97 | 93 | ||
| 98 | cameraFacingState.value = cameraFacingOverride ?? facing; | 94 | cameraFacingState.value = cameraFacingOverride ?? facing; |
| 99 | arguments['facing'] = cameraFacingState.value.index; | 95 | arguments['facing'] = cameraFacingState.value.index; |
| 100 | - | ||
| 101 | - // if (ratio != null) arguments['ratio'] = ratio; | ||
| 102 | arguments['torch'] = torchEnabled; | 96 | arguments['torch'] = torchEnabled; |
| 103 | arguments['speed'] = detectionSpeed.index; | 97 | arguments['speed'] = detectionSpeed.index; |
| 98 | + arguments['timeout'] = detectionTimeoutMs; | ||
| 104 | 99 | ||
| 105 | if (formats != null) { | 100 | if (formats != null) { |
| 106 | if (Platform.isAndroid) { | 101 | if (Platform.isAndroid) { |
| @@ -281,7 +276,7 @@ class MobileScannerController { | @@ -281,7 +276,7 @@ class MobileScannerController { | ||
| 281 | _barcodesController.add( | 276 | _barcodesController.add( |
| 282 | BarcodeCapture( | 277 | BarcodeCapture( |
| 283 | barcodes: parsed, | 278 | barcodes: parsed, |
| 284 | - image: event['image'] as Uint8List, | 279 | + image: event['image'] as Uint8List?, |
| 285 | ), | 280 | ), |
| 286 | ); | 281 | ); |
| 287 | break; | 282 | break; |
-
Please register or login to post a comment