Julian Steenbakker
Committed by GitHub

Merge pull request #362 from juliansteenbakker/bug/stop_while_stopped

refactor: android code + cleanup
Showing 32 changed files with 853 additions and 616 deletions
1 -## NEXT 1 +## 3.0.0-beta.2
2 Breaking changes: 2 Breaking changes:
  3 +* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
  4 +in the MobileScanner widget.
  5 +* allowDuplicates is removed and replaced by MobileScannerSpeed enum.
  6 +* onPermissionSet in MobileScanner widget is deprecated and will be removed. Use the onPermissionSet
  7 +onPermissionSet callback in MobileScannerController instead.
3 * [iOS] The minimum deployment target is now 11.0 or higher. 8 * [iOS] The minimum deployment target is now 11.0 or higher.
  9 +
  10 +Features:
  11 +* The returnImage is working for both iOS and Android. You can enable it in the MobileScannerController.
  12 +The image will be returned in the BarcodeCapture object provided by onDetect.
  13 +* You can now control the DetectionSpeed, as well as the timeout of the DetectionSpeed. For more
  14 +info see the DetectionSpeed documentation. This replaces the allowDuplicates function.
  15 +
  16 +Other improvements:
  17 +* Both the [iOS] and [Android] codebases have been refactored completely.
4 * [iOS] Updated POD dependencies 18 * [iOS] Updated POD dependencies
5 19
6 ## 3.0.0-beta.1 20 ## 3.0.0-beta.1
@@ -76,7 +76,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -76,7 +76,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
76 appBar: AppBar(title: const Text('Mobile Scanner')), 76 appBar: AppBar(title: const Text('Mobile Scanner')),
77 body: MobileScanner( 77 body: MobileScanner(
78 allowDuplicates: false, 78 allowDuplicates: false,
79 - onDetect: (barcode, args) { 79 + onDetect: (barcode) {
80 if (barcode.rawValue == null) { 80 if (barcode.rawValue == null) {
81 debugPrint('Failed to scan Barcode'); 81 debugPrint('Failed to scan Barcode');
82 } else { 82 } else {
@@ -101,7 +101,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -101,7 +101,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
101 allowDuplicates: false, 101 allowDuplicates: false,
102 controller: MobileScannerController( 102 controller: MobileScannerController(
103 facing: CameraFacing.front, torchEnabled: true), 103 facing: CameraFacing.front, torchEnabled: true),
104 - onDetect: (barcode, args) { 104 + onDetect: (barcode) {
105 if (barcode.rawValue == null) { 105 if (barcode.rawValue == null) {
106 debugPrint('Failed to scan Barcode'); 106 debugPrint('Failed to scan Barcode');
107 } else { 107 } else {
@@ -163,7 +163,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -163,7 +163,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
163 body: MobileScanner( 163 body: MobileScanner(
164 allowDuplicates: false, 164 allowDuplicates: false,
165 controller: cameraController, 165 controller: cameraController,
166 - onDetect: (barcode, args) { 166 + onDetect: (barcode) {
167 if (barcode.rawValue == null) { 167 if (barcode.rawValue == null) {
168 debugPrint('Failed to scan Barcode'); 168 debugPrint('Failed to scan Barcode');
169 } else { 169 } else {
@@ -189,7 +189,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; @@ -189,7 +189,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
189 torchEnabled: true, 189 torchEnabled: true,
190 returnImage: true, 190 returnImage: true,
191 ), 191 ),
192 - onDetect: (barcode, args) { 192 + onDetect: (barcode) {
193 if (barcode.rawValue == null) { 193 if (barcode.rawValue == null) {
194 debugPrint('Failed to scan Barcode'); 194 debugPrint('Failed to scan Barcode');
195 } else { 195 } else {
@@ -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)
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),
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +enum class DetectionSpeed(val intValue: Int) {
  4 + NO_DUPLICATES(0),
  5 + NORMAL(1),
  6 + UNRESTRICTED(2)
  7 +}
  1 +package dev.steenbakker.mobile_scanner.objects
  2 +
  3 +class MobileScannerStartParameters(
  4 + val width: Double = 0.0,
  5 + val height: Double,
  6 + val hasFlashUnit: Boolean,
  7 + val id: Long
  8 +)
1 buildscript { 1 buildscript {
2 - ext.kotlin_version = '1.7.20' 2 + ext.kotlin_version = '1.7.21'
3 repositories { 3 repositories {
4 google() 4 google()
5 mavenCentral() 5 mavenCentral()
@@ -19,6 +19,12 @@ class _BarcodeListScannerWithControllerState @@ -19,6 +19,12 @@ class _BarcodeListScannerWithControllerState
19 torchEnabled: true, 19 torchEnabled: true,
20 // formats: [BarcodeFormat.qrCode] 20 // formats: [BarcodeFormat.qrCode]
21 // facing: CameraFacing.front, 21 // facing: CameraFacing.front,
  22 + onPermissionSet: (hasPermission) {
  23 + // Do something with permission callback
  24 + },
  25 + // detectionSpeed: DetectionSpeed.normal
  26 + // detectionTimeoutMs: 1000,
  27 + // returnImage: false,
22 ); 28 );
23 29
24 bool isStarted = true; 30 bool isStarted = true;
@@ -34,16 +40,18 @@ class _BarcodeListScannerWithControllerState @@ -34,16 +40,18 @@ class _BarcodeListScannerWithControllerState
34 MobileScanner( 40 MobileScanner(
35 controller: controller, 41 controller: controller,
36 fit: BoxFit.contain, 42 fit: BoxFit.contain,
37 - // allowDuplicates: true,  
38 // controller: MobileScannerController( 43 // controller: MobileScannerController(
39 // torchEnabled: true, 44 // torchEnabled: true,
40 // facing: CameraFacing.front, 45 // facing: CameraFacing.front,
41 // ), 46 // ),
42 - onDetect: (barcodeCapture, arguments) { 47 + onDetect: (barcodeCapture) {
43 setState(() { 48 setState(() {
44 this.barcodeCapture = barcodeCapture; 49 this.barcodeCapture = barcodeCapture;
45 }); 50 });
46 }, 51 },
  52 + onStart: (arguments) {
  53 + // Do something with start arguments
  54 + },
47 ), 55 ),
48 Align( 56 Align(
49 alignment: Alignment.bottomCenter, 57 alignment: Alignment.bottomCenter,
@@ -99,7 +107,7 @@ class _BarcodeListScannerWithControllerState @@ -99,7 +107,7 @@ class _BarcodeListScannerWithControllerState
99 height: 50, 107 height: 50,
100 child: FittedBox( 108 child: FittedBox(
101 child: Text( 109 child: Text(
102 - '${barcodeCapture?.barcodes.map((e) => e.rawValue)}', 110 + '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}',
103 overflow: TextOverflow.fade, 111 overflow: TextOverflow.fade,
104 style: Theme.of(context) 112 style: Theme.of(context)
105 .textTheme 113 .textTheme
@@ -16,9 +16,15 @@ class _BarcodeScannerWithControllerState @@ -16,9 +16,15 @@ class _BarcodeScannerWithControllerState
16 BarcodeCapture? barcode; 16 BarcodeCapture? barcode;
17 17
18 MobileScannerController controller = MobileScannerController( 18 MobileScannerController controller = MobileScannerController(
19 - torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted, 19 + torchEnabled: true,
20 // formats: [BarcodeFormat.qrCode] 20 // formats: [BarcodeFormat.qrCode]
21 // facing: CameraFacing.front, 21 // facing: CameraFacing.front,
  22 + onPermissionSet: (hasPermission) {
  23 + // Do something with permission callback
  24 + },
  25 + // detectionSpeed: DetectionSpeed.normal
  26 + // detectionTimeoutMs: 1000,
  27 + // returnImage: false,
22 ); 28 );
23 29
24 bool isStarted = true; 30 bool isStarted = true;
@@ -34,12 +40,11 @@ class _BarcodeScannerWithControllerState @@ -34,12 +40,11 @@ class _BarcodeScannerWithControllerState
34 MobileScanner( 40 MobileScanner(
35 controller: controller, 41 controller: controller,
36 fit: BoxFit.contain, 42 fit: BoxFit.contain,
37 - // allowDuplicates: true,  
38 // controller: MobileScannerController( 43 // controller: MobileScannerController(
39 // torchEnabled: true, 44 // torchEnabled: true,
40 // facing: CameraFacing.front, 45 // facing: CameraFacing.front,
41 // ), 46 // ),
42 - onDetect: (barcode, args) { 47 + onDetect: (barcode) {
43 setState(() { 48 setState(() {
44 this.barcode = barcode; 49 this.barcode = barcode;
45 }); 50 });
@@ -18,10 +18,15 @@ class _BarcodeScannerReturningImageState @@ -18,10 +18,15 @@ class _BarcodeScannerReturningImageState
18 MobileScannerArguments? arguments; 18 MobileScannerArguments? arguments;
19 19
20 MobileScannerController controller = MobileScannerController( 20 MobileScannerController controller = MobileScannerController(
21 - // torchEnabled: true,  
22 - returnImage: true, 21 + torchEnabled: true,
23 // formats: [BarcodeFormat.qrCode] 22 // formats: [BarcodeFormat.qrCode]
24 // facing: CameraFacing.front, 23 // facing: CameraFacing.front,
  24 + onPermissionSet: (hasPermission) {
  25 + // Do something with permission callback
  26 + },
  27 + // detectionSpeed: DetectionSpeed.normal
  28 + // detectionTimeoutMs: 1000,
  29 + returnImage: true,
25 ); 30 );
26 31
27 bool isStarted = true; 32 bool isStarted = true;
@@ -29,49 +34,40 @@ class _BarcodeScannerReturningImageState @@ -29,49 +34,40 @@ class _BarcodeScannerReturningImageState
29 @override 34 @override
30 Widget build(BuildContext context) { 35 Widget build(BuildContext context) {
31 return Scaffold( 36 return Scaffold(
32 - backgroundColor: Colors.black,  
33 - body: Builder(  
34 - builder: (context) {  
35 - return Column(  
36 - children: [  
37 - Container(  
38 - color: Colors.blueGrey,  
39 - width: double.infinity,  
40 - height: 0.33 * MediaQuery.of(context).size.height,  
41 - child: barcode?.image != null  
42 - ? Transform.rotate(  
43 - angle: 90 * pi / 180,  
44 - child: Image(  
45 - gaplessPlayback: true,  
46 - image: MemoryImage(barcode!.image!),  
47 - fit: BoxFit.contain,  
48 - ),  
49 - )  
50 - : const ColoredBox(  
51 - color: Colors.white,  
52 - child: Center(  
53 - child: Text(  
54 - 'Your scanned barcode will appear here!',  
55 - ),  
56 - ), 37 + body: SafeArea(
  38 + child: Column(
  39 + children: [
  40 + Expanded(
  41 + child: barcode?.image != null
  42 + ? Transform.rotate(
  43 + angle: 90 * pi / 180,
  44 + child: Image(
  45 + gaplessPlayback: true,
  46 + image: MemoryImage(barcode!.image!),
  47 + fit: BoxFit.contain,
57 ), 48 ),
58 - ),  
59 - Container(  
60 - height: 0.66 * MediaQuery.of(context).size.height, 49 + )
  50 + : const Center(
  51 + child: Text(
  52 + 'Your scanned barcode will appear here!',
  53 + ),
  54 + ),
  55 + ),
  56 + Expanded(
  57 + flex: 2,
  58 + child: ColoredBox(
61 color: Colors.grey, 59 color: Colors.grey,
62 child: Stack( 60 child: Stack(
63 children: [ 61 children: [
64 MobileScanner( 62 MobileScanner(
65 controller: controller, 63 controller: controller,
66 fit: BoxFit.contain, 64 fit: BoxFit.contain,
67 - // allowDuplicates: true,  
68 // controller: MobileScannerController( 65 // controller: MobileScannerController(
69 // torchEnabled: true, 66 // torchEnabled: true,
70 // facing: CameraFacing.front, 67 // facing: CameraFacing.front,
71 // ), 68 // ),
72 - onDetect: (barcode, arguments) { 69 + onDetect: (barcode) {
73 setState(() { 70 setState(() {
74 - this.arguments = arguments;  
75 this.barcode = barcode; 71 this.barcode = barcode;
76 }); 72 });
77 }, 73 },
@@ -85,38 +81,33 @@ class _BarcodeScannerReturningImageState @@ -85,38 +81,33 @@ class _BarcodeScannerReturningImageState
85 child: Row( 81 child: Row(
86 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 82 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
87 children: [ 83 children: [
88 - ColoredBox(  
89 - color: arguments != null && !arguments!.hasTorch  
90 - ? Colors.red  
91 - : Colors.white,  
92 - child: IconButton(  
93 - // color: ,  
94 - icon: ValueListenableBuilder(  
95 - valueListenable: controller.torchState,  
96 - builder: (context, state, child) {  
97 - if (state == null) { 84 + IconButton(
  85 + color: Colors.white,
  86 + icon: ValueListenableBuilder(
  87 + valueListenable: controller.torchState,
  88 + builder: (context, state, child) {
  89 + if (state == null) {
  90 + return const Icon(
  91 + Icons.flash_off,
  92 + color: Colors.grey,
  93 + );
  94 + }
  95 + switch (state as TorchState) {
  96 + case TorchState.off:
98 return const Icon( 97 return const Icon(
99 Icons.flash_off, 98 Icons.flash_off,
100 color: Colors.grey, 99 color: Colors.grey,
101 ); 100 );
102 - }  
103 - switch (state as TorchState) {  
104 - case TorchState.off:  
105 - return const Icon(  
106 - Icons.flash_off,  
107 - color: Colors.grey,  
108 - );  
109 - case TorchState.on:  
110 - return const Icon(  
111 - Icons.flash_on,  
112 - color: Colors.yellow,  
113 - );  
114 - }  
115 - },  
116 - ),  
117 - iconSize: 32.0,  
118 - onPressed: () => controller.toggleTorch(), 101 + case TorchState.on:
  102 + return const Icon(
  103 + Icons.flash_on,
  104 + color: Colors.yellow,
  105 + );
  106 + }
  107 + },
119 ), 108 ),
  109 + iconSize: 32.0,
  110 + onPressed: () => controller.toggleTorch(),
120 ), 111 ),
121 IconButton( 112 IconButton(
122 color: Colors.white, 113 color: Colors.white,
@@ -174,9 +165,9 @@ class _BarcodeScannerReturningImageState @@ -174,9 +165,9 @@ class _BarcodeScannerReturningImageState
174 ], 165 ],
175 ), 166 ),
176 ), 167 ),
177 - ],  
178 - );  
179 - }, 168 + ),
  169 + ],
  170 + ),
180 ), 171 ),
181 ); 172 );
182 } 173 }
@@ -24,8 +24,7 @@ class _BarcodeScannerWithoutControllerState @@ -24,8 +24,7 @@ class _BarcodeScannerWithoutControllerState
24 children: [ 24 children: [
25 MobileScanner( 25 MobileScanner(
26 fit: BoxFit.contain, 26 fit: BoxFit.contain,
27 - // allowDuplicates: false,  
28 - onDetect: (capture, arguments) { 27 + onDetect: (capture) {
29 setState(() { 28 setState(() {
30 this.capture = capture; 29 this.capture = capture;
31 }); 30 });
@@ -8,7 +8,7 @@ environment: @@ -8,7 +8,7 @@ environment:
8 dependencies: 8 dependencies:
9 flutter: 9 flutter:
10 sdk: flutter 10 sdk: flutter
11 - image_picker: ^0.8.5+3 11 + image_picker: ^0.8.6
12 12
13 mobile_scanner: 13 mobile_scanner:
14 path: ../ 14 path: ../
  1 +//
  2 +// DetectionSpeed.swift
  3 +// mobile_scanner
  4 +//
  5 +// Created by Julian Steenbakker on 11/11/2022.
  6 +//
  7 +
  8 +enum DetectionSpeed: Int {
  9 + case noDuplicates = 0
  10 + case normal = 1
  11 + case unrestricted = 2
  12 +}
@@ -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,54 +223,18 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -192,54 +223,18 @@ 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 -  
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 226
233 - /// Convert image buffer to jpeg  
234 - private func ciImageToJpeg(ciImage: CIImage) -> Data {  
235 227
236 - // let ciImage = CIImage(cvPixelBuffer: latestBuffer)  
237 - let context:CIContext = CIContext.init(options: nil)  
238 - let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!  
239 - let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)  
240 -  
241 - return uiImage.jpegData(compressionQuality: 0.8)!;  
242 - } 228 +// /// Convert image buffer to jpeg
  229 +// private func ciImageToJpeg(ciImage: CIImage) -> Data {
  230 +//
  231 +// // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
  232 +// let context:CIContext = CIContext.init(options: nil)
  233 +// let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
  234 +// let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
  235 +//
  236 +// return uiImage.jpegData(compressionQuality: 0.8)!;
  237 +// }
243 238
244 /// Rotates images accordingly 239 /// Rotates images accordingly
245 func imageOrientation( 240 func imageOrientation(
@@ -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
@@ -21,7 +21,7 @@ extension CVBuffer { @@ -21,7 +21,7 @@ extension CVBuffer {
21 var image: UIImage { 21 var image: UIImage {
22 let ciImage = CIImage(cvPixelBuffer: self) 22 let ciImage = CIImage(cvPixelBuffer: self)
23 let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) 23 let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
24 - return UIImage(cgImage: cgImage!) 24 + return UIImage(cgImage: cgImage!, scale: 1.0, orientation: UIImage.Orientation.left)
25 } 25 }
26 26
27 var image1: UIImage { 27 var image1: UIImage {
@@ -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 -}  
1 library mobile_scanner; 1 library mobile_scanner;
2 2
3 -export 'src/barcode.dart';  
4 -export 'src/barcode_capture.dart';  
5 export 'src/enums/camera_facing.dart'; 3 export 'src/enums/camera_facing.dart';
6 export 'src/enums/detection_speed.dart'; 4 export 'src/enums/detection_speed.dart';
7 export 'src/enums/mobile_scanner_state.dart'; 5 export 'src/enums/mobile_scanner_state.dart';
8 export 'src/enums/ratio.dart'; 6 export 'src/enums/ratio.dart';
9 export 'src/enums/torch_state.dart'; 7 export 'src/enums/torch_state.dart';
10 export 'src/mobile_scanner.dart'; 8 export 'src/mobile_scanner.dart';
11 -export 'src/mobile_scanner_arguments.dart';  
12 export 'src/mobile_scanner_controller.dart'; 9 export 'src/mobile_scanner_controller.dart';
  10 +export 'src/objects/barcode.dart';
  11 +export 'src/objects/barcode_capture.dart';
  12 +export 'src/objects/mobile_scanner_arguments.dart';
@@ -2,11 +2,19 @@ @@ -2,11 +2,19 @@
2 enum DetectionSpeed { 2 enum DetectionSpeed {
3 /// The scanner will only scan a barcode once, and never again until another 3 /// The scanner will only scan a barcode once, and never again until another
4 /// barcode has been scanned. 4 /// barcode has been scanned.
  5 + ///
  6 + /// NOTE: This mode does analyze every frame in order to check if the value
  7 + /// has changed.
5 noDuplicates, 8 noDuplicates,
6 9
7 - /// Front facing camera. 10 + /// The barcode scanner will scan one barcode, and wait 250 Miliseconds before
  11 + /// scanning again. This will prevent memory issues on older devices.
  12 + ///
  13 + /// You can change the timeout duration with [detectionTimeout] parameter.
8 normal, 14 normal,
9 15
10 - /// Back facing camera. 16 + /// Let the scanner detect barcodes without restriction.
  17 + ///
  18 + /// NOTE: This can cause memory issues with older devices.
11 unrestricted, 19 unrestricted,
12 } 20 }
1 -enum MobileScannerState { undetermined, authorized, denied } 1 +/// The authorization state of the scanner.
  2 +enum MobileScannerState {
  3 + /// The scanner has yet to request weather it is [authorized] or [denied]
  4 + undetermined,
  5 +
  6 + /// The scanner has the required permissions.
  7 + authorized,
  8 +
  9 + /// The user denied the required permissions.
  10 + denied
  11 +}
1 -enum Ratio { ratio_4_3, ratio_16_9 } 1 +// This enum is not used yet
  2 +// enum Ratio { ratio_4_3, ratio_16_9 }
1 import 'package:flutter/foundation.dart'; 1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
3 -import 'package:mobile_scanner/src/barcode_capture.dart';  
4 -import 'package:mobile_scanner/src/mobile_scanner_arguments.dart';  
5 import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; 3 import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  4 +import 'package:mobile_scanner/src/objects/barcode_capture.dart';
  5 +import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
  6 +
  7 +typedef MobileScannerCallback = void Function(BarcodeCapture barcodes);
  8 +typedef MobileScannerArgumentsCallback = void Function(
  9 + MobileScannerArguments? arguments,
  10 +);
6 11
7 /// A widget showing a live camera preview. 12 /// A widget showing a live camera preview.
8 class MobileScanner extends StatefulWidget { 13 class MobileScanner extends StatefulWidget {
@@ -10,14 +15,21 @@ class MobileScanner extends StatefulWidget { @@ -10,14 +15,21 @@ class MobileScanner extends StatefulWidget {
10 final MobileScannerController? controller; 15 final MobileScannerController? controller;
11 16
12 /// Calls the provided [onPermissionSet] callback when the permission is set. 17 /// Calls the provided [onPermissionSet] callback when the permission is set.
  18 + // @Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
  19 + // ignore: deprecated_consistency
13 final Function(bool permissionGranted)? onPermissionSet; 20 final Function(bool permissionGranted)? onPermissionSet;
14 21
15 /// Function that gets called when a Barcode is detected. 22 /// Function that gets called when a Barcode is detected.
16 /// 23 ///
17 /// [barcode] The barcode object with all information about the scanned code. 24 /// [barcode] The barcode object with all information about the scanned code.
18 - /// [startArguments] Information about the state of the MobileScanner widget  
19 - final Function(BarcodeCapture capture, MobileScannerArguments? arguments)  
20 - onDetect; 25 + /// [startInternalArguments] Information about the state of the MobileScanner widget
  26 + final MobileScannerCallback onDetect;
  27 +
  28 + /// Function that gets called when the scanner is started.
  29 + ///
  30 + /// [arguments] The start arguments of the scanner. This contains the size of
  31 + /// the scanner which can be used to draw a box over the scanner.
  32 + final MobileScannerArgumentsCallback? onStart;
21 33
22 /// Handles how the widget should fit the screen. 34 /// Handles how the widget should fit the screen.
23 final BoxFit fit; 35 final BoxFit fit;
@@ -29,10 +41,12 @@ class MobileScanner extends StatefulWidget { @@ -29,10 +41,12 @@ class MobileScanner extends StatefulWidget {
29 const MobileScanner({ 41 const MobileScanner({
30 super.key, 42 super.key,
31 required this.onDetect, 43 required this.onDetect,
  44 + this.onStart,
32 this.controller, 45 this.controller,
33 this.autoResume = true, 46 this.autoResume = true,
34 this.fit = BoxFit.cover, 47 this.fit = BoxFit.cover,
35 - this.onPermissionSet, 48 + @Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
  49 + this.onPermissionSet,
36 }); 50 });
37 51
38 @override 52 @override
@@ -49,27 +63,39 @@ class _MobileScannerState extends State<MobileScanner> @@ -49,27 +63,39 @@ class _MobileScannerState extends State<MobileScanner>
49 WidgetsBinding.instance.addObserver(this); 63 WidgetsBinding.instance.addObserver(this);
50 controller = widget.controller ?? 64 controller = widget.controller ??
51 MobileScannerController(onPermissionSet: widget.onPermissionSet); 65 MobileScannerController(onPermissionSet: widget.onPermissionSet);
52 - if (!controller.isStarting) controller.start(); 66 + if (!controller.isStarting) {
  67 + _startScanner();
  68 + }
53 } 69 }
54 70
55 - AppLifecycleState? _lastState; 71 + Future<void> _startScanner() async {
  72 + final arguments = await controller.start();
  73 + widget.onStart?.call(arguments);
  74 + }
  75 +
  76 + bool resumeFromBackground = false;
56 77
57 @override 78 @override
58 void didChangeAppLifecycleState(AppLifecycleState state) { 79 void didChangeAppLifecycleState(AppLifecycleState state) {
  80 + // App state changed before it is initialized.
  81 + if (controller.isStarting) {
  82 + return;
  83 + }
  84 +
59 switch (state) { 85 switch (state) {
60 case AppLifecycleState.resumed: 86 case AppLifecycleState.resumed:
61 - if (!controller.isStarting &&  
62 - widget.autoResume &&  
63 - _lastState != AppLifecycleState.inactive) controller.start(); 87 + resumeFromBackground = false;
  88 + _startScanner();
64 break; 89 break;
65 case AppLifecycleState.paused: 90 case AppLifecycleState.paused:
66 - case AppLifecycleState.detached:  
67 - controller.stop(); 91 + resumeFromBackground = true;
  92 + break;
  93 + case AppLifecycleState.inactive:
  94 + if (!resumeFromBackground) controller.stop();
68 break; 95 break;
69 default: 96 default:
70 break; 97 break;
71 } 98 }
72 - _lastState = state;  
73 } 99 }
74 100
75 @override 101 @override
@@ -82,7 +108,7 @@ class _MobileScannerState extends State<MobileScanner> @@ -82,7 +108,7 @@ class _MobileScannerState extends State<MobileScanner>
82 return const ColoredBox(color: Colors.black); 108 return const ColoredBox(color: Colors.black);
83 } else { 109 } else {
84 controller.barcodes.listen((barcode) { 110 controller.barcodes.listen((barcode) {
85 - widget.onDetect(barcode, value! as MobileScannerArguments); 111 + widget.onDetect(barcode);
86 }); 112 });
87 return ClipRect( 113 return ClipRect(
88 child: SizedBox( 114 child: SizedBox(
@@ -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 }) {
@@ -31,7 +30,8 @@ class MobileScannerController { @@ -31,7 +30,8 @@ class MobileScannerController {
31 .listen((data) => _handleEvent(data as Map)); 30 .listen((data) => _handleEvent(data as Map));
32 } 31 }
33 32
34 - //Must be static to keep the same value on new instances 33 + /// The hashcode of the controller to check if the correct object is mounted.
  34 + /// Must be static to keep the same value on new instances
35 static int? controllerHashcode; 35 static int? controllerHashcode;
36 36
37 /// Select which camera should be used. 37 /// Select which camera should be used.
@@ -39,11 +39,6 @@ class MobileScannerController { @@ -39,11 +39,6 @@ class MobileScannerController {
39 /// Default: CameraFacing.back 39 /// Default: CameraFacing.back
40 final CameraFacing facing; 40 final CameraFacing facing;
41 41
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 42 /// Enable or disable the torch (Flash) on start
48 /// 43 ///
49 /// Default: disabled 44 /// Default: disabled
@@ -62,6 +57,13 @@ class MobileScannerController { @@ -62,6 +57,13 @@ class MobileScannerController {
62 /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices 57 /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
63 final DetectionSpeed detectionSpeed; 58 final DetectionSpeed detectionSpeed;
64 59
  60 + /// Sets the timeout of scanner.
  61 + /// The timeout is set in miliseconds.
  62 + ///
  63 + /// NOTE: The timeout only works if the [detectionSpeed] is set to
  64 + /// [DetectionSpeed.normal] (which is the default value).
  65 + final int detectionTimeoutMs;
  66 +
65 /// Sets the barcode stream 67 /// Sets the barcode stream
66 final StreamController<BarcodeCapture> _barcodesController = 68 final StreamController<BarcodeCapture> _barcodesController =
67 StreamController.broadcast(); 69 StreamController.broadcast();
@@ -89,6 +91,7 @@ class MobileScannerController { @@ -89,6 +91,7 @@ class MobileScannerController {
89 ValueNotifier(facing); 91 ValueNotifier(facing);
90 92
91 bool isStarting = false; 93 bool isStarting = false;
  94 +
92 bool? _hasTorch; 95 bool? _hasTorch;
93 96
94 /// Set the starting arguments for the camera 97 /// Set the starting arguments for the camera
@@ -97,10 +100,9 @@ class MobileScannerController { @@ -97,10 +100,9 @@ class MobileScannerController {
97 100
98 cameraFacingState.value = cameraFacingOverride ?? facing; 101 cameraFacingState.value = cameraFacingOverride ?? facing;
99 arguments['facing'] = cameraFacingState.value.index; 102 arguments['facing'] = cameraFacingState.value.index;
100 -  
101 - // if (ratio != null) arguments['ratio'] = ratio;  
102 arguments['torch'] = torchEnabled; 103 arguments['torch'] = torchEnabled;
103 arguments['speed'] = detectionSpeed.index; 104 arguments['speed'] = detectionSpeed.index;
  105 + arguments['timeout'] = detectionTimeoutMs;
104 106
105 if (formats != null) { 107 if (formats != null) {
106 if (Platform.isAndroid) { 108 if (Platform.isAndroid) {
@@ -118,9 +120,9 @@ class MobileScannerController { @@ -118,9 +120,9 @@ class MobileScannerController {
118 Future<MobileScannerArguments?> start({ 120 Future<MobileScannerArguments?> start({
119 CameraFacing? cameraFacingOverride, 121 CameraFacing? cameraFacingOverride,
120 }) async { 122 }) async {
121 - debugPrint('Hashcode controller: $hashCode');  
122 if (isStarting) { 123 if (isStarting) {
123 debugPrint("Called start() while starting."); 124 debugPrint("Called start() while starting.");
  125 + return null;
124 } 126 }
125 isStarting = true; 127 isStarting = true;
126 128
@@ -157,10 +159,10 @@ class MobileScannerController { @@ -157,10 +159,10 @@ class MobileScannerController {
157 ); 159 );
158 } on PlatformException catch (error) { 160 } on PlatformException catch (error) {
159 debugPrint('${error.code}: ${error.message}'); 161 debugPrint('${error.code}: ${error.message}');
160 - isStarting = false;  
161 if (error.code == "MobileScannerWeb") { 162 if (error.code == "MobileScannerWeb") {
162 onPermissionSet?.call(false); 163 onPermissionSet?.call(false);
163 } 164 }
  165 + isStarting = false;
164 return null; 166 return null;
165 } 167 }
166 168
@@ -177,32 +179,33 @@ class MobileScannerController { @@ -177,32 +179,33 @@ class MobileScannerController {
177 } 179 }
178 180
179 if (kIsWeb) { 181 if (kIsWeb) {
  182 + // If we reach this line, it means camera permission has been granted
180 onPermissionSet?.call( 183 onPermissionSet?.call(
181 true, 184 true,
182 - ); // If we reach this line, it means camera permission has been granted  
183 -  
184 - startArguments.value = MobileScannerArguments(  
185 - webId: startResult['ViewID'] as String?,  
186 - size: Size(  
187 - startResult['videoWidth'] as double? ?? 0,  
188 - startResult['videoHeight'] as double? ?? 0,  
189 - ),  
190 - hasTorch: _hasTorch!,  
191 - );  
192 - } else {  
193 - startArguments.value = MobileScannerArguments(  
194 - textureId: startResult['textureId'] as int?,  
195 - size: toSize(startResult['size'] as Map? ?? {}),  
196 - hasTorch: _hasTorch!,  
197 ); 185 );
198 } 186 }
  187 +
199 isStarting = false; 188 isStarting = false;
200 - return startArguments.value!; 189 + return startArguments.value = MobileScannerArguments(
  190 + size: kIsWeb
  191 + ? Size(
  192 + startResult['videoWidth'] as double? ?? 0,
  193 + startResult['videoHeight'] as double? ?? 0,
  194 + )
  195 + : toSize(startResult['size'] as Map? ?? {}),
  196 + hasTorch: _hasTorch!,
  197 + textureId: kIsWeb ? null : startResult['textureId'] as int?,
  198 + webId: kIsWeb ? startResult['ViewID'] as String? : null,
  199 + );
201 } 200 }
202 201
203 /// Stops the camera, but does not dispose this controller. 202 /// Stops the camera, but does not dispose this controller.
204 Future<void> stop() async { 203 Future<void> stop() async {
205 - await _methodChannel.invokeMethod('stop'); 204 + try {
  205 + await _methodChannel.invokeMethod('stop');
  206 + } catch (e) {
  207 + debugPrint('$e');
  208 + }
206 } 209 }
207 210
208 /// Switches the torch on or off. 211 /// Switches the torch on or off.
@@ -277,7 +280,7 @@ class MobileScannerController { @@ -277,7 +280,7 @@ class MobileScannerController {
277 _barcodesController.add( 280 _barcodesController.add(
278 BarcodeCapture( 281 BarcodeCapture(
279 barcodes: parsed, 282 barcodes: parsed,
280 - image: event['image'] as Uint8List, 283 + image: event['image'] as Uint8List?,
281 ), 284 ),
282 ); 285 );
283 break; 286 break;
@@ -12,11 +12,6 @@ class Barcode { @@ -12,11 +12,6 @@ class Barcode {
12 /// Returns null if the corner points can not be determined. 12 /// Returns null if the corner points can not be determined.
13 final List<Offset>? corners; 13 final List<Offset>? corners;
14 14
15 - /// Returns raw bytes of the image buffer  
16 - ///  
17 - /// Returns null if the image was not returned  
18 - final Uint8List? image;  
19 -  
20 /// Returns barcode format 15 /// Returns barcode format
21 final BarcodeFormat format; 16 final BarcodeFormat format;
22 17
@@ -79,7 +74,6 @@ class Barcode { @@ -79,7 +74,6 @@ class Barcode {
79 74
80 Barcode({ 75 Barcode({
81 this.corners, 76 this.corners,
82 - this.image,  
83 this.format = BarcodeFormat.ean13, 77 this.format = BarcodeFormat.ean13,
84 this.rawBytes, 78 this.rawBytes,
85 this.type = BarcodeType.text, 79 this.type = BarcodeType.text,
@@ -97,7 +91,7 @@ class Barcode { @@ -97,7 +91,7 @@ class Barcode {
97 }); 91 });
98 92
99 /// Create a [Barcode] from native data. 93 /// Create a [Barcode] from native data.
100 - Barcode.fromNative(Map data, {this.image}) 94 + Barcode.fromNative(Map data)
101 : corners = toCorners(data['corners'] as List?), 95 : corners = toCorners(data['corners'] as List?),
102 format = toFormat(data['format'] as int), 96 format = toFormat(data['format'] as int),
103 rawBytes = data['rawBytes'] as Uint8List?, 97 rawBytes = data['rawBytes'] as Uint8List?,
1 import 'dart:typed_data'; 1 import 'dart:typed_data';
2 2
3 -import 'package:mobile_scanner/src/barcode.dart'; 3 +import 'package:mobile_scanner/src/objects/barcode.dart';
4 4
  5 +/// The return object after a frame is scanned.
  6 +///
  7 +/// [barcodes] A list with barcodes. A scanned frame can contain multiple
  8 +/// barcodes.
  9 +/// [image] If enabled, an image of the scanned frame.
5 class BarcodeCapture { 10 class BarcodeCapture {
6 - List<Barcode> barcodes;  
7 - Uint8List? image; 11 + final List<Barcode> barcodes;
  12 +
  13 + final Uint8List? image;
8 14
9 BarcodeCapture({ 15 BarcodeCapture({
10 required this.barcodes, 16 required this.barcodes,
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 2
3 -/// Camera args for [CameraView]. 3 +/// The start arguments of the scanner.
4 class MobileScannerArguments { 4 class MobileScannerArguments {
5 - /// The texture id.  
6 - final int? textureId;  
7 -  
8 - /// Size of the texture. 5 + /// The output size of the camera.
  6 + /// This value can be used to draw a box in the image.
9 final Size size; 7 final Size size;
10 8
  9 + /// A bool which is true if the device has a torch.
11 final bool hasTorch; 10 final bool hasTorch;
12 11
  12 + /// The texture id of the capture used internally.
  13 + final int? textureId;
  14 +
  15 + /// The texture id of the capture used internally if device is web.
13 final String? webId; 16 final String? webId;
14 17
15 - /// Create a [MobileScannerArguments].  
16 MobileScannerArguments({ 18 MobileScannerArguments({
17 - this.textureId,  
18 required this.size, 19 required this.size,
19 required this.hasTorch, 20 required this.hasTorch,
  21 + this.textureId,
20 this.webId, 22 this.webId,
21 }); 23 });
22 } 24 }
1 name: mobile_scanner 1 name: mobile_scanner
2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS. 2 description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
3 -version: 3.0.0-beta.1 3 +version: 3.0.0-beta.2
4 repository: https://github.com/juliansteenbakker/mobile_scanner 4 repository: https://github.com/juliansteenbakker/mobile_scanner
5 5
6 environment: 6 environment: