Julian Steenbakker

Merge branch 'master' into feature/scan-window

# Conflicts:
#	android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt
#	example/ios/Runner.xcodeproj/project.pbxproj
#	example/lib/main.dart
#	ios/Classes/SwiftMobileScannerPlugin.swift
#	lib/src/mobile_scanner.dart
#	lib/src/mobile_scanner_controller.dart
Showing 61 changed files with 2570 additions and 1237 deletions

Too many changes to show.

To preserve performance only 61 of 61+ files are displayed.

# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# review when someone opens a pull request.
# For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners
* @juliansteenbakker
... ...
... ... @@ -6,15 +6,33 @@ updates:
interval: "weekly"
reviewers:
- "juliansteenbakker"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: gradle
directory: "/android"
schedule:
interval: "weekly"
reviewers:
- "juliansteenbakker"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: gradle
directory: "/example/android"
schedule:
interval: "weekly"
reviewers:
- "juliansteenbakker"
\ No newline at end of file
- "juliansteenbakker"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
include: "scope"
reviewers:
- "juliansteenbakker"
... ...
# .github/workflows/auto-author-assign.yml
name: 'Auto Author Assign'
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
assign-author:
runs-on: ubuntu-latest
steps:
- uses: toshimaru/auto-author-assign@v1.6.1
... ...
... ... @@ -11,12 +11,12 @@ jobs:
analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/setup-java@v3.4.1
- uses: actions/checkout@v3.1.0
- uses: actions/setup-java@v3.6.0
with:
java-version: 11
distribution: temurin
- uses: subosito/flutter-action@v2.6.1
- uses: subosito/flutter-action@v2.8.0
with:
cache: true
- name: Version
... ... @@ -28,12 +28,12 @@ jobs:
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/setup-java@v3.4.1
- uses: actions/checkout@v3.1.0
- uses: actions/setup-java@v3.6.0
with:
java-version: 11
distribution: temurin
- uses: subosito/flutter-action@v2.6.1
- uses: subosito/flutter-action@v2.8.0
with:
cache: true
- name: Format
... ...
name: Run release-please
on:
push:
branches:
- master
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v3.6.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: simple
... ...
name: "Semantic PRs"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
... ...
## 3.0.0-beta.3
Deprecated:
* The `onStart` method has been renamed to `onScannerStarted`.
* The `onPermissionSet` argument of the `MobileScannerController` is now deprecated.
Breaking changes:
* `MobileScannerException` now uses an `errorCode` instead of a `message`.
* `MobileScannerException` now contains additional details from the original error.
* Refactored `MobileScannerController.start()` to throw `MobileScannerException`s
with consistent error codes, rather than string messages.
To handle permission errors, consider catching the result of `MobileScannerController.start()`.
* The `autoResume` attribute has been removed from the `MobileScanner` widget.
The controller already automatically resumes, so it had no effect.
* Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef.
Improvements:
* Toggling the device torch now does nothing if the device has no torch, rather than throwing an error.
* Removed `called stop while already stopped` messages.
Features:
* Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder.
* Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically.
* Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch.
Fixes:
* Fixes the missing gradle setup for the Android project, which prevented gradle sync from working.
* Fixes `MobileScannerController.stop()` throwing when already stopped.
* Fixes `MobileScannerController.toggleTorch()` throwing if the device has no torch.
Now it does nothing if the torch is not available.
* Fixes a memory leak where the `MobileScanner` would keep listening to the barcode events.
* Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`.
Now it only depends on its layout constraints.
* Fixed a potential crash when the scanner is restarted due to the app being resumed.
## 3.0.0-beta.2
Breaking changes:
* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
in the MobileScanner widget.
* onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image.
* allowDuplicates is removed and replaced by MobileScannerSpeed enum.
* onPermissionSet in MobileScanner widget is deprecated and will be removed. Use the onPermissionSet
onPermissionSet callback in MobileScannerController instead.
* [iOS] The minimum deployment target is now 11.0 or higher.
Features:
* The returnImage is working for both iOS and Android. You can enable it in the MobileScannerController.
The image will be returned in the BarcodeCapture object provided by onDetect.
* You can now control the DetectionSpeed, as well as the timeout of the DetectionSpeed. For more
info see the DetectionSpeed documentation. This replaces the allowDuplicates function.
Other improvements:
* Both the [iOS] and [Android] codebases have been refactored completely.
* [iOS] Updated POD dependencies
## 3.0.0-beta.1
Breaking changes:
* [Android] SDK updated to SDK 33.
Features:
* [Web] Add binaryData for raw value.
* [iOS] Captures the last scanned barcode with Barcode.image.
* [iOS] Add support for multiple formats on iOS with BarcodeScannerOptions.
* Add displayValue which returns barcode value in a user-friendly format.
* Add autoResume option to MobileScannerController which automatically resumes the camera when the application is resumed
Other changes:
* [Android] Revert camera2 dependency to stable release
* [iOS] Update barcode scanning library to latest version
* Several minor code improvements
## 2.0.0
Breaking changes:
This version is only compatible with flutter 3.0.0 and later.
... ...
... ... @@ -36,22 +36,23 @@ NSPhotoLibraryUsageDescription - describe why your app needs permission for the
### macOS
macOS 10.13 or newer. Reason: Apple Vision library.
Ensure that you granted camera permission in XCode -> Signing & Capabilities:
<img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png">
### Web
Add this to `web/index.html`:
```html
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
```
Web only supports QR codes for now.
Do you have experience with Flutter Web development? [Help me with migrating from jsQR to qr-scanner for full barcode support!](https://github.com/juliansteenbakker/mobile_scanner/issues/54)
## Features Supported
| Features | Android | iOS | macOS | Web |
|------------------------|--------------------|--------------------|-------|-----|
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
## Usage
... ... @@ -71,15 +72,15 @@ import 'package:mobile_scanner/mobile_scanner.dart';
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
allowDuplicates: false,
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
}
}),
// fit: BoxFit.contain,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
```
... ... @@ -94,17 +95,18 @@ import 'package:mobile_scanner/mobile_scanner.dart';
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
allowDuplicates: false,
controller: MobileScannerController(
facing: CameraFacing.front, torchEnabled: true),
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
}
}),
// fit: BoxFit.contain,
controller: MobileScannerController(
facing: CameraFacing.front, torchEnabled: true,
),
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
```
... ... @@ -157,15 +159,82 @@ import 'package:mobile_scanner/mobile_scanner.dart';
],
),
body: MobileScanner(
allowDuplicates: false,
controller: cameraController,
onDetect: (barcode, args) {
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
} else {
final String code = barcode.rawValue!;
debugPrint('Barcode found! $code');
}
}));
// fit: BoxFit.contain,
controller: cameraController,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
```
Example with controller and returning images
```dart
import 'package:mobile_scanner/mobile_scanner.dart';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
fit: BoxFit.contain,
controller: MobileScannerController(
// facing: CameraFacing.back,
// torchEnabled: false,
returnImage: true,
),
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
if (image != null) {
showDialog(
context: context,
builder: (context) =>
Image(image: MemoryImage(image)),
);
Future.delayed(const Duration(seconds: 5), () {
Navigator.pop(context);
});
}
},
),
);
}
```
### BarcodeCapture
The onDetect function returns a BarcodeCapture objects which contains the following items.
| Property name | Type | Description |
|---------------|---------------|-----------------------------------|
| barcodes | List<Barcode> | A list with scanned barcodes. |
| image | Uint8List? | If enabled, an image of the scan. |
You can use the following properties of the Barcode object.
| Property name | Type | Description |
|---------------|----------------|-------------------------------------|
| format | BarcodeFormat | |
| rawBytes | Uint8List? | binary scan result |
| rawValue | String? | Value if barcode is in UTF-8 format |
| displayValue | String? | |
| type | BarcodeType | |
| calendarEvent | CalendarEvent? | |
| contactInfo | ContactInfo? | |
| driverLicense | DriverLicense? | |
| email | Email? | |
| geoPoint | GeoPoint? | |
| phone | Phone? | |
| sms | SMS? | |
| url | UrlBookmark? | |
| wifi | WiFi? | WiFi Access-Point details |
... ...
... ... @@ -2,14 +2,15 @@ group 'dev.steenbakker.mobile_scanner'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.7.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
... ... @@ -24,7 +25,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 32
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
... ... @@ -50,18 +51,8 @@ dependencies {
// Use this dependency to bundle the model with your app
implementation 'com.google.mlkit:barcode-scanning:17.0.2'
// Use this dependency to use the dynamically downloaded model in Google Play Services
// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0'
// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
implementation "androidx.camera:camera-camera2:1.2.0-alpha04"
implementation 'androidx.camera:camera-lifecycle:1.2.0-alpha04'
// // The following line is optional, as the core library is included indirectly by camera-camera2
// implementation "androidx.camera:camera-core:1.1.0-alpha11"
// implementation "androidx.camera:camera-camera2:1.1.0-alpha11"
// // If you want to additionally use the CameraX Lifecycle library
// implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11"
// // If you want to additionally use the CameraX View class
// implementation "androidx.camera:camera-view:1.0.0-alpha31"
// // If you want to additionally use the CameraX Extensions library
// implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
implementation 'androidx.camera:camera-camera2:1.1.0'
implementation 'androidx.camera:camera-lifecycle:1.1.0'
}
... ...
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
... ...
package dev.steenbakker.mobile_scanner
import androidx.annotation.IntDef
@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE)
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class AnalyzeMode {
companion object {
const val NONE = 0
const val BARCODE = 1
}
}
\ No newline at end of file
package dev.steenbakker.mobile_scanner
import android.os.Handler
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
private val eventChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
"dev.steenbakker.mobile_scanner/scanner/event"
)
init {
eventChannel.setStreamHandler(this)
}
fun publishEvent(event: Map<String, Any>) {
Handler(Looper.getMainLooper()).post {
eventSink?.success(event)
}
}
override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
}
override fun onCancel(event: Any?) {
this.eventSink = null
}
}
\ No newline at end of file
... ...
... ... @@ -7,12 +7,9 @@ import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.util.Log
import android.util.Size
import android.util.Rational
import android.media.Image
import android.os.Handler
import android.os.Looper
import android.view.Surface
import androidx.annotation.NonNull
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
... ... @@ -20,117 +17,160 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.common.InputImage.IMAGE_FORMAT_NV21
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
typealias TorchStateCallback = (state: Int) -> Unit
typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
import java.io.File
import kotlin.math.roundToInt
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)
: MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener {
class NoCamera : Exception()
class AlreadyStarted : Exception()
class AlreadyStopped : Exception()
class TorchError : Exception()
class CameraError : Exception()
class TorchWhenStopped : Exception()
class MobileScanner(
private val activity: Activity,
private val textureRegistry: TextureRegistry,
private val mobileScannerCallback: MobileScannerCallback,
private val mobileScannerErrorCallback: MobileScannerErrorCallback
) :
PluginRegistry.RequestPermissionsResultListener {
companion object {
private const val REQUEST_CODE = 22022022
private val TAG = MobileScanner::class.java.simpleName
/**
* When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
* @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode
*/
private const val REQUEST_CODE = 0x0786
}
private var sink: EventChannel.EventSink? = null
private var listener: PluginRegistry.RequestPermissionsResultListener? = null
private var cameraProvider: ProcessCameraProvider? = null
private var camera: Camera? = null
private var pendingPermissionResult: MethodChannel.Result? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
private var scanWindow: List<Float>? = null;
// @AnalyzeMode
// private var analyzeMode: Int = AnalyzeMode.NONE
@ExperimentalGetImage
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
"state" -> checkPermission(result)
"request" -> requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
// "analyze" -> switchAnalyzeMode(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"updateScanWindow" -> updateScanWindow(call)
else -> result.notImplemented()
}
}
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
private var lastScanned: List<String?>? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
this.sink = events
}
private var scannerTimeout = false
override fun onCancel(arguments: Any?) {
sink = null
}
private var returnImage = false
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean {
return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
}
private var scanner = BarcodeScanning.getClient()
private fun checkPermission(result: MethodChannel.Result) {
/**
* Check if we already have camera permission.
*/
fun hasCameraPermission(): Int {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val state =
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1
else 0
result.success(state)
val hasPermission = ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
return if (hasPermission) {
1
} else {
0
}
}
private fun requestPermission(result: MethodChannel.Result) {
listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults ->
if (requestCode != REQUEST_CODE) {
false
} else {
val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED
result.success(authorized)
listener = null
true
}
/**
* Request camera permissions.
*/
fun requestPermission(result: MethodChannel.Result) {
if(pendingPermissionResult != null) {
return
}
pendingPermissionResult = result
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
/**
* Calls the callback after permissions are requested.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
if (requestCode != REQUEST_CODE) {
return false
}
pendingPermissionResult?.success(grantResults[0] == PackageManager.PERMISSION_GRANTED)
pendingPermissionResult = null
return true
}
/**
* callback for the camera. Every frame is passed through this function.
*/
@ExperimentalGetImage
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val mediaImage = imageProxy.image ?: return@Analyzer
var inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
return@Analyzer
} else if (detectionSpeed == DetectionSpeed.NORMAL) {
scannerTimeout = true
}
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
print("image: ")
println(inputImage.getWidth());
println(inputImage.getHeight());
print("barcode: ")
println(barcode.getBoundingBox());
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
if(!match) continue
if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue }
if (newScannedBarcodes == lastScanned) {
// New scanned is duplicate, returning
return@addOnSuccessListener
}
lastScanned = newScannedBarcodes
}
val barcodeMap = barcodes.map { barcode -> barcode.data }
val event = mapOf("name" to "barcode", "data" to barcode.data)
sink?.success(event)
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
if(!match) continue
}
if (barcodeMap.isNotEmpty()) {
mobileScannerCallback(
barcodeMap,
if (returnImage) mediaImage.toByteArray() else null
)
}
}
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
.addOnFailureListener { e -> Log.e(TAG, e.message, e) }
.addOnCompleteListener { imageProxy.close() }
}
private var scanner = BarcodeScanning.getClient()
if (detectionSpeed == DetectionSpeed.NORMAL) {
// Set timer and continue
Handler(Looper.getMainLooper()).postDelayed({
scannerTimeout = false
}, detectionTimeout)
}
}
private fun updateScanWindow(call: MethodCall) {
scanWindow = call.argument<List<Float>>("rect")
... ... @@ -138,7 +178,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// scales the scanWindow to the provided inputImage and checks if that scaled
// scanWindow contains the barcode
private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean {
private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean {
val barcodeBoundingBox = barcode.getBoundingBox()
if(barcodeBoundingBox == null) return false
... ... @@ -150,157 +190,118 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val right = (scanWindow[2] * imageWidth).roundToInt()
val bottom = (scanWindow[3] * imageHeight).roundToInt()
val scaledScanWindow = Rect(left, top, right, bottom)
val scaledScanWindow = Rect(left, top, right, bottom)
print("scanWindow: ")
println(scaledScanWindow)
print("scanWindow: ")
println(scaledScanWindow)
return scaledScanWindow.contains(barcodeBoundingBox)
}
/**
* Start barcode scanning by initializing the camera and barcode scanner.
*/
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)
val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit())
result.success(answer)
} else {
val facing: Int = call.argument<Int>("facing") ?: 0
val ratio: Int = call.argument<Int>("ratio") ?: 1
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val formats: List<Int>? = call.argument<List<Int>>("formats")
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
scanner = if (formatsList.size == 1) {
BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build())
} else {
BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build())
}
}
fun start(
barcodeScannerOptions: BarcodeScannerOptions?,
returnImage: Boolean,
cameraPosition: CameraSelector,
torch: Boolean,
detectionSpeed: DetectionSpeed,
torchStateCallback: TorchStateCallback,
mobileScannerStartedCallback: MobileScannerStartedCallback,
detectionTimeout: Long
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
this.returnImage = returnImage
val future = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
throw AlreadyStarted()
}
future.addListener({
cameraProvider = future.get()
if (cameraProvider == null) {
result.error("cameraProvider", "cameraProvider is null", null)
return@addListener
}
cameraProvider!!.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
if (textureEntry == null) {
result.error("textureEntry", "textureEntry is null", null)
return@addListener
}
// Select the correct camera
val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(request.resolution.width, request.resolution.height)
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
scanner = if (barcodeScannerOptions != null) {
BarcodeScanning.getClient(barcodeScannerOptions)
} else {
BarcodeScanning.getClient()
}
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder().setTargetAspectRatio(ratio)
preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
// bind to lifecycle temporarily to fetch dimensions.
cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview)
val previewResolution = preview!!.resolutionInfo!!.resolution
val previewRotation = preview!!.getTargetRotation()
// Build the analyzer to be passed on to MLKit
val analysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetAspectRatio(ratio)
.setTargetRotation(previewRotation)
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
val viewPort = ViewPort.Builder(Rational(previewResolution.width, previewResolution.height), previewRotation).build()
val useCaseGroup = UseCaseGroup.Builder()
.setViewPort(viewPort)
.addUseCase(preview!!)
.addUseCase(analysis)
.build()
cameraProvider!!.unbindAll()
camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, useCaseGroup)
if (camera == null) {
result.error("camera", "camera is null", null)
return@addListener
}
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
// Register the torch listener
camera!!.cameraInfo.torchState.observe(activity) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
sink?.success(mapOf("name" to "torchState", "data" to state))
}
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
if (cameraProvider == null) {
throw CameraError()
}
cameraProvider!!.unbindAll()
textureEntry = textureRegistry.createSurfaceTexture()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)
val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit())
result.success(answer)
}, executor)
}
}
// Build the preview to be shown on the Flutter texture
val previewBuilder = Preview.Builder()
preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) }
// Build the analyzer to be passed on to MLKit
val analysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// analysisBuilder.setTargetResolution(Size(1440, 1920))
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) }
camera = cameraProvider!!.bindToLifecycle(
activity as LifecycleOwner,
cameraPosition,
preview,
analysis
)
// Register the torch listener
camera!!.cameraInfo.torchState.observe(activity) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
torchStateCallback(state)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
if (camera == null) {
result.error(TAG,"Called toggleTorch() while stopped!", null)
return
}
camera!!.cameraControl.enableTorch(call.arguments == 1)
result.success(null)
}
// val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
// val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
// Log.i("LOG", "Analyzer: $analysisSize")
// Log.i("LOG", "Preview: $previewSize")
// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) {
// analyzeMode = call.arguments as Int
// result.success(null)
// }
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
val uri = Uri.fromFile( File(call.arguments.toString()))
val inputImage = InputImage.fromFilePath(activity, uri)
val resolution = preview!!.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
var barcodeFound = false
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
barcodeFound = true
sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
}
}
.addOnFailureListener { e -> Log.e(TAG, e.message, e)
result.error(TAG, e.message, e)}
.addOnCompleteListener { result.success(barcodeFound) }
mobileScannerStartedCallback(
MobileScannerStartParameters(
if (portrait) width else height,
if (portrait) height else width,
camera!!.cameraInfo.hasFlashUnit(),
textureEntry!!.id()
)
)
}, executor)
}
private fun stop(result: MethodChannel.Result) {
/**
* Stop barcode scanning.
*/
fun stop() {
if (camera == null && preview == null) {
result.error(TAG,"Called stop() while already stopped!", null)
return
throw AlreadyStopped()
}
val owner = activity as LifecycleOwner
... ... @@ -308,68 +309,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
cameraProvider?.unbindAll()
textureEntry?.release()
// analyzeMode = AnalyzeMode.NONE
camera = null
preview = null
textureEntry = null
cameraProvider = null
result.success(null)
}
/**
* Toggles the flash light on or off.
*/
fun toggleTorch(enableTorch: Boolean) {
if (camera == null) {
throw TorchWhenStopped()
}
camera!!.cameraControl.enableTorch(enableTorch)
}
private val Barcode.data: Map<String, Any?>
get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf("addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf("addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
private val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
private val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
/**
* Analyze a single image.
*/
fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) {
val inputImage = InputImage.fromFilePath(activity, image)
private val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
val barcodeMap = barcodes.map { barcode -> barcode.data }
private val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
if (barcodeMap.isNotEmpty()) {
analyzerCallback(barcodeMap)
} else {
analyzerCallback(null)
}
}
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
}
}
private val Barcode.WiFi.data: Map<String, Any?>
get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
}
... ...
package dev.steenbakker.mobile_scanner
import androidx.annotation.NonNull
import android.net.Uri
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.io.File
/** MobileScannerPlugin */
class MobileScannerPlugin : FlutterPlugin, ActivityAware {
private var flutter: FlutterPlugin.FlutterPluginBinding? = null
private var activity: ActivityPluginBinding? = null
class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler {
private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null
private var activityPluginBinding: ActivityPluginBinding? = null
private var handler: MobileScanner? = null
private var method: MethodChannel? = null
private var event: EventChannel? = null
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = binding
private lateinit var barcodeHandler: BarcodeHandler
private var analyzerResult: MethodChannel.Result? = null
private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? ->
if (image != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes,
"image" to image
))
} else {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
}
}
private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
if (barcodes != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
analyzerResult?.success(true)
} else {
analyzerResult?.success(false)
}
analyzerResult = null
}
private val errorCallback: MobileScannerErrorCallback = {error: String ->
barcodeHandler.publishEvent(mapOf(
"name" to "error",
"data" to error,
))
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
this.flutter = null
private val torchStateCallback: TorchStateCallback = {state: Int ->
barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding
handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry)
method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event")
method!!.setMethodCallHandler(handler)
event!!.setStreamHandler(handler)
activity!!.addRequestPermissionsResultListener(handler!!)
@ExperimentalGetImage
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (handler == null) {
result.error("MobileScanner", "Called ${call.method} before initializing.", null)
return
}
when (call.method) {
"state" -> result.success(handler!!.hasCameraPermission())
"request" -> handler!!.requestPermission(result)
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
else -> result.notImplemented()
}
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method")
method!!.setMethodCallHandler(this)
barcodeHandler = BarcodeHandler(binding)
this.flutterPluginBinding = binding
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
this.flutterPluginBinding = null
}
override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) {
handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback
)
activityPluginBinding.addRequestPermissionsResultListener(handler!!)
this.activityPluginBinding = activityPluginBinding
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
... ... @@ -38,16 +107,112 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware {
}
override fun onDetachedFromActivity() {
activity!!.removeRequestPermissionsResultListener(handler!!)
event!!.setStreamHandler(null)
activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!)
method!!.setMethodCallHandler(null)
event = null
method = null
handler = null
activity = null
activityPluginBinding = null
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val facing: Int = call.argument<Int>("facing") ?: 0
val formats: List<Int>? = call.argument<List<Int>>("formats")
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
val speed: Int = call.argument<Int>("speed") ?: 1
val timeout: Int = call.argument<Int>("timeout") ?: 250
var barcodeScannerOptions: BarcodeScannerOptions? = null
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
for (index in formats) {
formatsList.add(BarcodeFormats.values()[index].intValue)
}
barcodeScannerOptions = if (formatsList.size == 1) {
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
} else {
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
}
}
val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong())
} catch (e: AlreadyStarted) {
result.error(
"MobileScanner",
"Called start() while already started",
null
)
} catch (e: NoCamera) {
result.error(
"MobileScanner",
"No camera found or failed to open camera!",
null
)
} catch (e: TorchError) {
result.error(
"MobileScanner",
"Error occurred when setting torch!",
null
)
} catch (e: CameraError) {
result.error(
"MobileScanner",
"Error occurred when setting up camera!",
null
)
} catch (e: Exception) {
result.error(
"MobileScanner",
"Unknown error occurred..",
null
)
}
}
private fun stop(result: MethodChannel.Result) {
try {
handler!!.stop()
result.success(null)
} catch (e: AlreadyStopped) {
result.success(null)
}
}
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))
handler!!.analyzeImage(uri, analyzerCallback)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
try {
handler!!.toggleTorch(call.arguments == 1)
result.success(null)
} catch (e: AlreadyStopped) {
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
}
... ...
package dev.steenbakker.mobile_scanner
import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import com.google.mlkit.vision.barcode.common.Barcode
import java.io.ByteArrayOutputStream
fun Image.toByteArray(): ByteArray {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
return out.toByteArray()
}
val Barcode.data: Map<String, Any?>
get() = mapOf(
"corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf(
"description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary
)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf(
"addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls
)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf(
"addressLines" to addressLines.map { addressLine -> addressLine.toString() },
"type" to type
)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf(
"first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix
)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf(
"addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
private val Barcode.GeoPoint.data: Map<String, Any?>
get() = mapOf("latitude" to lat, "longitude" to lng)
private val Barcode.Phone.data: Map<String, Any?>
get() = mapOf("number" to number, "type" to type)
private val Barcode.Sms.data: Map<String, Any?>
get() = mapOf("message" to message, "phoneNumber" to phoneNumber)
private val Barcode.UrlBookmark.data: Map<String, Any?>
get() = mapOf("title" to title, "url" to url)
private val Barcode.WiFi.data: Map<String, Any?>
get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid)
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.exceptions
internal class NoPermissionException : RuntimeException()
//internal class Exception(val reason: Reason) :
// java.lang.Exception("Mobile Scanner failed because $reason") {
//
// internal enum class Reason {
// noHardware, noPermissions, noBackCamera
// }
//}
\ No newline at end of file
package dev.steenbakker.mobile_scanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
package dev.steenbakker.mobile_scanner.objects
enum class BarcodeFormats(val intValue: Int) {
UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN),
... ...
package dev.steenbakker.mobile_scanner.objects
enum class DetectionSpeed(val intValue: Int) {
NO_DUPLICATES(0),
NORMAL(1),
UNRESTRICTED(2)
}
\ No newline at end of file
... ...
package dev.steenbakker.mobile_scanner.objects
class MobileScannerStartParameters(
val width: Double = 0.0,
val height: Double,
val hasFlashUnit: Boolean,
val id: Long
)
\ No newline at end of file
... ...
... ... @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 32
compileSdkVersion 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
... ... @@ -45,7 +45,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dev.steenbakker.mobile_scanner_example"
minSdkVersion 21
targetSdkVersion 32
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
... ...
... ... @@ -5,7 +5,7 @@
android:label="mobile_scanner_example"
android:icon="@mipmap/ic_launcher">
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
... ...
package dev.steenbakker.mobile_scanner_example
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity: FlutterActivity() {
class MainActivity : FlutterFragmentActivity() {
}
... ...
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.7.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
... ...
#Tue May 31 10:34:01 CEST 2022
#Tue Aug 23 15:51:00 CEST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
... ...
... ... @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>
... ...
# Uncomment this line to define a global platform for your project
platform :ios, '10.0'
platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ... @@ -37,5 +37,9 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
... ...
... ... @@ -13,7 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E133769572782C32D37D8AC /* Pods_Runner.framework */; };
A5A2C2B73A9F26060DE9FB22 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
... ... @@ -32,9 +32,9 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
32FD382A786B3A0080FE63FD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5E133769572782C32D37D8AC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54E006799E73DEAB41FD3623 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
... ... @@ -45,8 +45,8 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
D5B36FCD262B39F867CFDEEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F0D5742F0690BE32D07B033A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
... ... @@ -54,27 +54,19 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */,
A5A2C2B73A9F26060DE9FB22 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0F766276E0F46921DEBF581B /* Frameworks */ = {
isa = PBXGroup;
children = (
5E133769572782C32D37D8AC /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
203D5C95A734778D93D18369 /* Pods */ = {
isa = PBXGroup;
children = (
E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */,
1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */,
E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */,
D5B36FCD262B39F867CFDEEE /* Pods-Runner.debug.xcconfig */,
32FD382A786B3A0080FE63FD /* Pods-Runner.release.xcconfig */,
F0D5742F0690BE32D07B033A /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
... ... @@ -97,7 +89,7 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
203D5C95A734778D93D18369 /* Pods */,
0F766276E0F46921DEBF581B /* Frameworks */,
FF36E403CAC9E06A5A96BB9F /* Frameworks */,
);
sourceTree = "<group>";
};
... ... @@ -124,6 +116,14 @@
path = Runner;
sourceTree = "<group>";
};
FF36E403CAC9E06A5A96BB9F /* Frameworks */ = {
isa = PBXGroup;
children = (
54E006799E73DEAB41FD3623 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
... ... @@ -131,14 +131,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */,
B086C54F5791A4E759CB6822 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */,
F825A499E8C466DB9DC6247D /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
... ... @@ -197,57 +197,57 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */ = {
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
name = "Thin Binary";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
B086C54F5791A4E759CB6822 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
name = "Run Script";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */ = {
F825A499E8C466DB9DC6247D /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
... ... @@ -339,7 +339,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -364,7 +364,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample;
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
... ... @@ -420,7 +420,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
... ... @@ -469,7 +469,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -496,7 +496,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample;
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
... ... @@ -522,7 +522,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample;
PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeListScannerWithController extends StatefulWidget {
const BarcodeListScannerWithController({Key? key}) : super(key: key);
@override
_BarcodeListScannerWithControllerState createState() =>
_BarcodeListScannerWithControllerState();
}
class _BarcodeListScannerWithControllerState
extends State<BarcodeListScannerWithController>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcodeCapture;
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
void _startOrStop() {
if (isStarted) {
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
}
});
}
setState(() {
isStarted = !isStarted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
onDetect: (barcodeCapture) {
setState(() {
this.barcodeCapture = barcodeCapture;
});
},
onScannerStarted: (arguments) {
// Do something with arguments.
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
if (state == null) {
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
}
switch (state as TorchState) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: _startOrStop,
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
'${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
if (state == null) {
return const Icon(Icons.camera_front);
}
switch (state as CameraFacing) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
// Pick an image
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
),
);
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
),
);
}
}
},
),
],
),
),
),
],
);
},
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -13,16 +13,44 @@ class BarcodeScannerWithController extends StatefulWidget {
class _BarcodeScannerWithControllerState
extends State<BarcodeScannerWithController>
with SingleTickerProviderStateMixin {
String? barcode;
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
void _startOrStop() {
if (isStarted) {
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
}
});
}
setState(() {
isStarted = !isStarted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
... ... @@ -34,14 +62,9 @@ class _BarcodeScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode, args) {
onDetect: (barcode) {
setState(() {
this.barcode = barcode.rawValue;
this.barcode = barcode;
});
},
),
... ... @@ -88,10 +111,7 @@ class _BarcodeScannerWithControllerState
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted ? controller.stop() : controller.start();
isStarted = !isStarted;
}),
onPressed: _startOrStop,
),
Center(
child: SizedBox(
... ... @@ -99,7 +119,8 @@ class _BarcodeScannerWithControllerState
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ... @@ -169,4 +190,10 @@ class _BarcodeScannerWithControllerState
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
... ...
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerReturningImage extends StatefulWidget {
const BarcodeScannerReturningImage({Key? key}) : super(key: key);
@override
_BarcodeScannerReturningImageState createState() =>
_BarcodeScannerReturningImageState();
}
class _BarcodeScannerReturningImageState
extends State<BarcodeScannerReturningImage>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcode;
MobileScannerArguments? arguments;
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
returnImage: true,
);
bool isStarted = true;
void _startOrStop() {
if (isStarted) {
controller.stop();
} else {
controller.start().catchError((error) {
final exception = error as MobileScannerException;
switch (exception.errorCode) {
case MobileScannerErrorCode.controllerUninitialized:
break; // This error code is not used by `start()`.
case MobileScannerErrorCode.genericError:
debugPrint('Scanner failed to start');
break;
case MobileScannerErrorCode.permissionDenied:
debugPrint('Camera permission denied');
break;
}
});
}
setState(() {
isStarted = !isStarted;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
child: barcode?.image != null
? Transform.rotate(
angle: 90 * pi / 180,
child: Image(
gaplessPlayback: true,
image: MemoryImage(barcode!.image!),
fit: BoxFit.contain,
),
)
: const Center(
child: Text(
'Your scanned barcode will appear here!',
),
),
),
Expanded(
flex: 2,
child: ColoredBox(
color: Colors.grey,
child: Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
if (state == null) {
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
}
switch (state as TorchState) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: _startOrStop,
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
if (state == null) {
return const Icon(Icons.camera_front);
}
switch (state as CameraFacing) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
],
),
),
),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget {
class _BarcodeScannerWithoutControllerState
extends State<BarcodeScannerWithoutController>
with SingleTickerProviderStateMixin {
String? barcode;
BarcodeCapture? capture;
@override
Widget build(BuildContext context) {
... ... @@ -24,10 +24,9 @@ class _BarcodeScannerWithoutControllerState
children: [
MobileScanner(
fit: BoxFit.contain,
// allowDuplicates: false,
onDetect: (barcode, args) {
onDetect: (capture) {
setState(() {
this.barcode = barcode.rawValue;
this.capture = capture;
});
},
),
... ... @@ -46,7 +45,8 @@ class _BarcodeScannerWithoutControllerState
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
capture?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
import 'package:mobile_scanner_example/barcode_scanner_window.dart';
import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
... ... @@ -22,6 +24,17 @@ class MyHome extends StatelessWidget {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const BarcodeListScannerWithController(),
),
);
},
child: const Text('MobileScanner with List Controller'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerWithController(),
),
);
... ... @@ -42,6 +55,17 @@ class MyHome extends StatelessWidget {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerReturningImage(),
),
);
},
child:
const Text('MobileScanner with Controller (returning image)'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const BarcodeScannerWithoutController(),
),
... ...
... ... @@ -8,7 +8,7 @@ environment:
dependencies:
flutter:
sdk: flutter
image_picker: ^0.8.5+3
image_picker: ^0.8.6
mobile_scanner:
path: ../
... ...
... ... @@ -28,8 +28,8 @@
<title>example</title>
<link rel="manifest" href="manifest.json">
<!-- <script src="https://cdn.jsdelivr.net/npm/qr-scanner@1.4.1/qr-scanner.min.js"></script>-->
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script>
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
... ...
//
// BarcodeHandler.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 24/08/2022.
//
import Foundation
public class BarcodeHandler: NSObject, FlutterStreamHandler {
var event: [String: Any?] = [:]
private var eventSink: FlutterEventSink?
private let eventChannel: FlutterEventChannel
init(registrar: FlutterPluginRegistrar) {
eventChannel = FlutterEventChannel(name:
"dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
super.init()
eventChannel.setStreamHandler(self)
}
func publishEvent(_ event: [String: Any?]) {
self.event = event
eventSink?(event)
}
public func onListen(withArguments arguments: Any?,
eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
}
... ...
//
// DetectionSpeed.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 11/11/2022.
//
enum DetectionSpeed: Int {
case noDuplicates = 0
case normal = 1
case unrestricted = 2
}
... ...
//
// SwiftMobileScanner.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 15/02/2022.
//
import Foundation
import AVFoundation
import MLKitVision
import MLKitBarcodeScanning
typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
/// Capture session of the camera
var captureSession: AVCaptureSession!
/// The selected camera
var device: AVCaptureDevice!
/// Barcode scanner for results
var scanner = BarcodeScanner.barcodeScanner()
/// Return image buffer with the Barcode event
var returnImage: Bool = false
/// Default position of camera
var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
/// When results are found, this callback will be called
let mobileScannerCallback: MobileScannerCallback
/// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
private let registry: FlutterTextureRegistry?
/// Image to be sent to the texture
var latestBuffer: CVImageBuffer!
/// Texture id of the camera preview for Flutter
private var textureId: Int64!
var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) {
self.registry = registry
self.mobileScannerCallback = mobileScannerCallback
super.init()
}
/// Check if we already have camera permission.
func checkPermission() -> Int {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
return 0
case .authorized:
return 1
default:
return 2
}
}
/// Request permissions for video
func requestPermission(_ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get image buffer from sample buffer.")
return
}
latestBuffer = imageBuffer
registry?.textureFrameAvailable(textureId)
if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
i = 0
let ciImage = latestBuffer.image
let image = VisionImage(image: ciImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: videoPosition
)
scanner.process(image) { [self] barcodes, error in
if (detectionSpeed == DetectionSpeed.noDuplicates) {
let newScannedBarcodes = barcodes?.map { barcode in
return barcode.rawValue
}
if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
return
} else {
barcodesString = newScannedBarcodes
}
}
mobileScannerCallback(barcodes, error, ciImage)
}
} else {
i+=1
}
}
/// Start scanning for barcodes
func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
self.detectionSpeed = detectionSpeed
if (device != nil) {
throw MobileScannerError.alreadyStarted
}
scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
captureSession = AVCaptureSession()
textureId = registry?.register(self)
// Open the camera device
if #available(iOS 10.0, *) {
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first
} else {
device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first
}
if (device == nil) {
throw MobileScannerError.noCamera
}
// Enable the torch if parameter is set and torch is available
if (device.hasTorch && device.isTorchAvailable) {
do {
try device.lockForConfiguration()
device.torchMode = torch
device.unlockForConfiguration()
} catch {
throw MobileScannerError.torchError(error)
}
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
captureSession.beginConfiguration()
// Add device input
do {
let input = try AVCaptureDeviceInput(device: device)
captureSession.addInput(input)
} catch {
throw MobileScannerError.cameraError(error)
}
captureSession.sessionPreset = AVCaptureSession.Preset.photo;
// Add video output.
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
videoOutput.alwaysDiscardsLateVideoFrames = true
videoPosition = cameraPosition
// calls captureOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
for connection in videoOutput.connections {
connection.videoOrientation = .portrait
if cameraPosition == .front && connection.isVideoMirroringSupported {
connection.isVideoMirrored = true
}
}
captureSession.commitConfiguration()
captureSession.startRunning()
let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
}
/// Stop scanning for barcodes
func stop() throws {
if (device == nil) {
throw MobileScannerError.alreadyStopped
}
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession.outputs {
captureSession.removeOutput(output)
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
registry?.unregisterTexture(textureId)
textureId = nil
captureSession = nil
device = nil
}
/// Toggle the flashlight between on and off
func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
if (device == nil) {
throw MobileScannerError.torchWhenStopped
}
do {
try device.lockForConfiguration()
device.torchMode = torch
device.unlockForConfiguration()
} catch {
throw MobileScannerError.torchError(error)
}
}
/// Analyze a single image
func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
let image = VisionImage(image: image)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: position
)
scanner.process(image, completion: callback)
}
var i = 0
var barcodesString: Array<String?>?
// /// Convert image buffer to jpeg
// private func ciImageToJpeg(ciImage: CIImage) -> Data {
//
// // let ciImage = CIImage(cvPixelBuffer: latestBuffer)
// let context:CIContext = CIContext.init(options: nil)
// let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
// let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
//
// return uiImage.jpegData(compressionQuality: 0.8)!;
// }
/// Rotates images accordingly
func imageOrientation(
deviceOrientation: UIDeviceOrientation,
defaultOrientation: UIDeviceOrientation,
position: AVCaptureDevice.Position
) -> UIImage.Orientation {
switch deviceOrientation {
case .portrait:
return position == .front ? .leftMirrored : .right
case .landscapeLeft:
return position == .front ? .downMirrored : .up
case .portraitUpsideDown:
return position == .front ? .rightMirrored : .left
case .landscapeRight:
return position == .front ? .upMirrored : .down
case .faceDown, .faceUp, .unknown:
return .up
@unknown default:
return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back)
}
}
/// Sends output of OutputBuffer to a Flutter texture
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
if latestBuffer == nil {
return nil
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var textureId: Int64 = 0
}
}
... ...
//
// MobileScannerError.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 24/08/2022.
//
import Foundation
enum MobileScannerError: Error {
case noCamera
case alreadyStarted
case alreadyStopped
case torchError(_ error: Error)
case cameraError(_ error: Error)
case torchWhenStopped
case analyzerError(_ error: Error)
}
... ...
... ... @@ -21,7 +21,7 @@ extension CVBuffer {
var image: UIImage {
let ciImage = CIImage(cvPixelBuffer: self)
let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
return UIImage(cgImage: cgImage!)
return UIImage(cgImage: cgImage!, scale: 1.0, orientation: UIImage.Orientation.left)
}
var image1: UIImage {
... ...
//
// SwiftMobileScanner.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 15/02/2022.
//
import Foundation
import AVFoundation
import Flutter
import MLKitVision
import MLKitBarcodeScanning
import AVFoundation
import UIKit
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
let registry: FlutterTextureRegistry
// Sink for publishing event changes
var sink: FlutterEventSink!
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
// Texture id of the camera preview
var textureId: Int64!
/// The mobile scanner object that handles all logic
private let mobileScanner: MobileScanner
// Capture session of the camera
var captureSession: AVCaptureSession!
// The selected camera
var device: AVCaptureDevice!
// Image to be sent to the texture
var latestBuffer: CVImageBuffer!
// var analyzeMode: Int = 0
var analyzing: Bool = false
var position = AVCaptureDevice.Position.back
/// The handler sends all information via an event channel back to Flutter
private let barcodeHandler: BarcodeHandler
var scanWindow: CGRect?
var scanner = BarcodeScanner.barcodeScanner()
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftMobileScannerPlugin(registrar.textures())
let method = FlutterMethodChannel(name:
"dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
let event = FlutterEventChannel(name:
"dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: method)
event.setStreamHandler(instance)
}
init(_ registry: FlutterTextureRegistry) {
self.registry = registry
init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
if barcodes != nil {
let barcodesMap = barcodes!.map { barcode in
return barcode.data
}
if (!barcodesMap.isEmpty) {
barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)])
}
} else if (error != nil){
barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
}
})
self.barcodeHandler = barcodeHandler
super.init()
}
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures())
let methodChannel = FlutterMethodChannel(name:
"dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "state":
checkPermission(call, result)
result(mobileScanner.checkPermission())
case "request":
requestPermission(call, result)
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
case "start":
start(call, result)
case "torch":
toggleTorch(call, result)
// case "analyze":
// switchAnalyzeMode(call, result)
case "stop":
stop(result)
case "torch":
toggleTorch(call, result)
case "analyzeImage":
analyzeImage(call, result)
case "updateScanWindow":
... ... @@ -71,365 +59,113 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
}
}
// FlutterStreamHandler
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
sink = events
return nil
}
// FlutterStreamHandler
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
sink = nil
return nil
}
// FlutterTexture
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
if latestBuffer == nil {
return nil
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
/// Parses all parameters and starts the mobileScanner
private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
registry.textureFrameAvailable(textureId)
let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
var barcodeOptions: BarcodeScannerOptions? = nil
// switch analyzeMode {
// case 1: // barcode
if analyzing {
return
}
analyzing = true
let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)
var image = VisionImage(image: buffer!.image)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
)
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
if scanWindow != nil {
let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)
if (!match) {
continue
}
}
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
}
}
analyzing = false
}
// default: // none
// break
// }
}
func imageOrientation(
deviceOrientation: UIDeviceOrientation,
defaultOrientation: UIDeviceOrientation
) -> UIImage.Orientation {
switch deviceOrientation {
case .portrait:
return position == .front ? .leftMirrored : .right
case .landscapeLeft:
return position == .front ? .downMirrored : .up
case .portraitUpsideDown:
return position == .front ? .rightMirrored : .left
case .landscapeRight:
return position == .front ? .upMirrored : .down
case .faceDown, .faceUp, .unknown:
return .up
@unknown default:
return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait)
}
}
func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
result(0)
case .authorized:
result(1)
default:
result(2)
}
}
func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
func updateScanWindow(_ call: FlutterMethodCall) {
let argReader = MapArgumentReader(call.arguments as? [String: Any])
let scanWindowData: Array? = argReader.floatArray(key: "rect")
if (scanWindowData == nil) {
return
}
let minX = scanWindowData![0]
let minY = scanWindowData![1]
if (formatList.count != 0) {
var barcodeFormats: BarcodeFormat = []
for index in formats {
barcodeFormats.insert(BarcodeFormat(rawValue: index))
}
barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
}
let width = scanWindowData![2] - minX
let height = scanWindowData![3] - minY
scanWindow = CGRect(x: minX, y: minY, width: width, height: height)
}
func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool {
let barcodeBoundingBox = barcode.frame
let position = facing == 0 ? AVCaptureDevice.Position.front : .back
let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!
let imageWidth = inputImage.size.width;
let imageHeight = inputImage.size.height;
let minX = scanWindow.minX * imageWidth
let minY = scanWindow.minY * imageHeight
let width = scanWindow.width * imageWidth
let height = scanWindow.height * imageHeight
let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height)
return scaledScanWindow.contains(barcodeBoundingBox)
}
func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device != nil) {
do {
let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed)
result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
} catch MobileScannerError.alreadyStarted {
result(FlutterError(code: "MobileScanner",
message: "Called start() while already started!",
details: nil))
return
}
textureId = registry.register(self)
captureSession = AVCaptureSession()
let argReader = MapArgumentReader(call.arguments as? [String: Any])
// let ratio: Int = argReader.int(key: "ratio")
let torch: Bool = argReader.bool(key: "torch") ?? false
let facing: Int = argReader.int(key: "facing") ?? 1
let formats: Array = argReader.intArray(key: "formats") ?? []
let formatList: NSMutableArray = []
for index in formats {
formatList.add(BarcodeFormat(rawValue: index))
}
if (formatList.count != 0) {
let barcodeOptions = BarcodeScannerOptions(formats: formatList.firstObject as! BarcodeFormat)
scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)
}
// Set the camera to use
position = facing == 0 ? AVCaptureDevice.Position.front : .back
// Open the camera device
if #available(iOS 10.0, *) {
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first
} else {
device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first
}
if (device == nil) {
message: "Called start() while already started!",
details: nil))
} catch MobileScannerError.noCamera {
result(FlutterError(code: "MobileScanner",
message: "No camera found or failed to open camera!",
details: nil))
return
}
// Enable the torch if parameter is set and torch is available
if (device.hasTorch && device.isTorchAvailable) {
do {
try device.lockForConfiguration()
device.torchMode = torch ? .on : .off
device.unlockForConfiguration()
} catch {
error.throwNative(result)
}
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
captureSession.beginConfiguration()
// Add device input
do {
let input = try AVCaptureDeviceInput(device: device)
captureSession.addInput(input)
message: "No camera found or failed to open camera!",
details: nil))
} catch MobileScannerError.torchError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting torch!",
details: error))
} catch MobileScannerError.cameraError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting up camera!",
details: error))
} catch {
error.throwNative(result)
}
captureSession.sessionPreset = AVCaptureSession.Preset.photo;
// Add video output.
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
for connection in videoOutput.connections {
connection.videoOrientation = .portrait
if position == .front && connection.isVideoMirroringSupported {
connection.isVideoMirrored = true
}
result(FlutterError(code: "MobileScanner",
message: "Unknown error occured..",
details: nil))
}
captureSession.commitConfiguration()
captureSession.startRunning()
}
let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
let width = Double(demensions.height)
let height = Double(demensions.width)
let size = ["width": width, "height": height]
let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]
result(answer)
/// Stops the mobileScanner and closes the texture
private func stop(_ result: @escaping FlutterResult) {
do {
try mobileScanner.stop()
} catch {}
result(nil)
}
func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device == nil) {
result(FlutterError(code: "MobileScanner",
message: "Called toggleTorch() while stopped!",
details: nil))
return
}
/// Toggles the torch
private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
do {
try device.lockForConfiguration()
device.torchMode = call.arguments as! Int == 1 ? .on : .off
device.unlockForConfiguration()
result(nil)
try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
} catch {
error.throwNative(result)
result(FlutterError(code: "MobileScanner",
message: "Called toggleTorch() while stopped!",
details: nil))
}
result(nil)
}
// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
// analyzeMode = call.arguments as! Int
// result(nil)
// }
func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let uiImage = UIImage(contentsOfFile: call.arguments as! String)
/// Analyzes a single image
private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
if (uiImage == nil) {
result(FlutterError(code: "MobileScanner",
message: "No image found in analyzeImage!",
details: nil))
message: "No image found in analyzeImage!",
details: nil))
return
}
let image = VisionImage(image: uiImage!)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
)
var barcodeFound = false
scanner.process(image) { [self] barcodes, error in
mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
if scanWindow != nil {
let match = isbarCodeInScanWindow(scanWindow!, barcode, uiImage!)
if (!match) {
continue
}
}
barcodeFound = true
if scanWindow != nil {
let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image)
if (!match) {
continue
}
}
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
barcodeHandler.publishEvent(event)
}
} else if error != nil {
result(FlutterError(code: "MobileScanner",
message: error?.localizedDescription,
details: "analyzeImage()"))
barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
}
analyzing = false
result(barcodeFound)
}
}
func stop(_ result: FlutterResult) {
if (device == nil) {
result(FlutterError(code: "MobileScanner",
message: "Called stop() while already stopped!",
details: nil))
return
}
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession.outputs {
captureSession.removeOutput(output)
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
registry.unregisterTexture(textureId)
// analyzeMode = 0
latestBuffer = nil
captureSession = nil
device = nil
textureId = nil
})
result(nil)
}
// Observer for torch state
/// Observer for torch state
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
switch keyPath {
case "torchMode":
// off = 0; on = 1; auto = 2;
let state = change?[.newKey] as? Int
let event: [String: Any?] = ["name": "torchState", "data": state]
sink?(event)
barcodeHandler.publishEvent(["name": "torchState", "data": state])
default:
break
}
}
}
class MapArgumentReader {
let args: [String: Any]?
init(_ args: [String: Any]?) {
self.args = args
}
func string(key: String) -> String? {
return args?[key] as? String
}
func int(key: String) -> Int? {
return (args?[key] as? NSNumber)?.intValue
}
func bool(key: String) -> Bool? {
return (args?[key] as? NSNumber)?.boolValue
}
func stringArray(key: String) -> [String]? {
return args?[key] as? [String]
}
func intArray(key: String) -> [Int]? {
return args?[key] as? [Int]
}
func floatArray(key: String) -> [CGFloat]? {
return args?[key] as? [CGFloat]
}
}
... ...
... ... @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '0.0.1'
s.version = '3.0.0'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
... ... @@ -15,8 +15,8 @@ An universal scanner for Flutter based on MLKit.
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 2.6.0'
s.platform = :ios, '10.0'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0'
s.platform = :ios, '11.0'
s.static_framework = true
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
... ...
library mobile_scanner;
export 'src/enums/camera_facing.dart';
export 'src/enums/detection_speed.dart';
export 'src/enums/mobile_scanner_error_code.dart';
export 'src/enums/mobile_scanner_state.dart';
export 'src/enums/ratio.dart';
export 'src/enums/torch_state.dart';
export 'src/mobile_scanner.dart';
export 'src/mobile_scanner_arguments.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/mobile_scanner_exception.dart';
export 'src/objects/barcode.dart';
export 'src/objects/barcode_capture.dart';
export 'src/objects/mobile_scanner_arguments.dart';
... ...
library mobile_scanner_web;
export 'src/web/base.dart';
export 'src/web/jsqr.dart';
export 'src/web/zxing.dart';
... ...
... ... @@ -2,12 +2,10 @@ import 'dart:async';
import 'dart:html' as html;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/web/jsqr.dart';
import 'package:mobile_scanner/src/web/media.dart';
import 'package:mobile_scanner/mobile_scanner_web.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
/// This plugin is the web implementation of mobile_scanner.
/// It only supports QR codes.
... ... @@ -24,7 +22,6 @@ class MobileScannerWebPlugin {
registrar,
);
final MobileScannerWebPlugin instance = MobileScannerWebPlugin();
WidgetsFlutterBinding.ensureInitialized();
channel.setMethodCallHandler(instance.handleMethodCall);
event.setController(instance.controller);
... ... @@ -33,20 +30,14 @@ class MobileScannerWebPlugin {
// Controller to send events back to the framework
StreamController controller = StreamController.broadcast();
// The video stream. Will be initialized later to see which camera needs to be used.
html.MediaStream? _localStream;
html.VideoElement video = html.VideoElement();
// ID of the video feed
String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}';
// Determine wether device has flas
bool hasFlash = false;
// Timer used to capture frames to be analyzed
Timer? _frameInterval;
static final html.DivElement vidDiv = html.DivElement();
html.DivElement vidDiv = html.DivElement();
static WebBarcodeReaderBase barCodeReader =
ZXingBarcodeReader(videoContainer: vidDiv);
StreamSubscription? _barCodeStreamSubscription;
/// Handle incomming messages
Future<dynamic> handleMethodCall(MethodCall call) async {
... ... @@ -68,20 +59,11 @@ class MobileScannerWebPlugin {
/// Can enable or disable the flash if available
Future<void> _torch(arguments) async {
if (hasFlash) {
final track = _localStream?.getVideoTracks();
await track!.first.applyConstraints({
'advanced': {'torch': arguments == 1}
});
} else {
controller.addError('Device has no flash');
}
barCodeReader.toggleTorch(enabled: arguments == 1);
}
/// Starts the video stream and the scanner
Future<Map> _start(Map arguments) async {
vidDiv.children = [video];
var cameraFacing = CameraFacing.front;
if (arguments.containsKey('facing')) {
cameraFacing = CameraFacing.values[arguments['facing'] as int];
... ... @@ -91,64 +73,45 @@ class MobileScannerWebPlugin {
// ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls
ui.platformViewRegistry.registerViewFactory(
viewID,
(int id) => vidDiv
..style.width = '100%'
..style.height = '100%',
(int id) {
return vidDiv
..style.width = '100%'
..style.height = '100%';
},
);
// Check if stream is running
if (_localStream != null) {
if (barCodeReader.isStarted) {
return {
'ViewID': viewID,
'videoWidth': video.videoWidth,
'videoHeight': video.videoHeight
'videoWidth': barCodeReader.videoWidth,
'videoHeight': barCodeReader.videoHeight,
'torchable': barCodeReader.hasTorch,
};
}
try {
// Check if browser supports multiple camera's and set if supported
final Map? capabilities =
html.window.navigator.mediaDevices?.getSupportedConstraints();
if (capabilities != null && capabilities['facingMode'] as bool) {
final constraints = {
'video': VideoOptions(
facingMode:
cameraFacing == CameraFacing.front ? 'user' : 'environment',
)
};
_localStream =
await html.window.navigator.mediaDevices?.getUserMedia(constraints);
} else {
_localStream = await html.window.navigator.mediaDevices
?.getUserMedia({'video': true});
}
video.srcObject = _localStream;
// TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533
// final track = _localStream?.getVideoTracks();
// if (track != null) {
// final imageCapture = html.ImageCapture(track.first);
// final photoCapabilities = await imageCapture.getPhotoCapabilities();
// }
// required to tell iOS safari we don't want fullscreen
video.setAttribute('playsinline', 'true');
await video.play();
// Then capture a frame to be analyzed every 200 miliseconds
_frameInterval =
Timer.periodic(const Duration(milliseconds: 200), (timer) {
_captureFrame();
await barCodeReader.start(
cameraFacing: cameraFacing,
);
_barCodeStreamSubscription =
barCodeReader.detectBarcodeContinuously().listen((code) {
if (code != null) {
controller.add({
'name': 'barcodeWeb',
'data': {
'rawValue': code.rawValue,
'rawBytes': code.rawBytes,
},
});
}
});
return {
'ViewID': viewID,
'videoWidth': video.videoWidth,
'videoHeight': video.videoHeight,
'torchable': hasFlash
'videoWidth': barCodeReader.videoWidth,
'videoHeight': barCodeReader.videoHeight,
'torchable': barCodeReader.hasTorch,
};
} catch (e) {
throw PlatformException(code: 'MobileScannerWeb', message: '$e');
... ... @@ -171,36 +134,8 @@ class MobileScannerWebPlugin {
/// Stops the video feed and analyzer
Future<void> cancel() async {
try {
// Stop the camera stream
_localStream?.getTracks().forEach((track) {
if (track.readyState == 'live') {
track.stop();
}
});
} catch (e) {
debugPrint('Failed to stop stream: $e');
}
video.srcObject = null;
_localStream = null;
_frameInterval?.cancel();
_frameInterval = null;
}
/// Captures a frame and analyzes it for QR codes
Future<dynamic> _captureFrame() async {
if (_localStream == null) return null;
final canvas =
html.CanvasElement(width: video.videoWidth, height: video.videoHeight);
final ctx = canvas.context2D;
ctx.drawImage(video, 0, 0);
final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
final code = jsQR(imgData.data, canvas.width, canvas.height);
if (code != null) {
controller.add({'name': 'barcodeWeb', 'data': code.data});
}
barCodeReader.stop();
await _barCodeStreamSubscription?.cancel();
_barCodeStreamSubscription = null;
}
}
... ...
/// The facing of a camera.
enum CameraFacing {
/// Front facing camera.
front,
/// Back facing camera.
back,
}
... ...
/// The detection speed of the scanner.
enum DetectionSpeed {
/// The scanner will only scan a barcode once, and never again until another
/// barcode has been scanned.
///
/// NOTE: This mode does analyze every frame in order to check if the value
/// has changed.
noDuplicates,
/// The barcode scanner will scan one barcode, and wait 250 Miliseconds before
/// scanning again. This will prevent memory issues on older devices.
///
/// You can change the timeout duration with [detectionTimeout] parameter.
normal,
/// Let the scanner detect barcodes without restriction.
///
/// NOTE: This can cause memory issues with older devices.
unrestricted,
}
... ...
/// This enum defines the different error codes for the mobile scanner.
enum MobileScannerErrorCode {
/// The controller was used
/// while it was not yet initialized using [MobileScannerController.start].
controllerUninitialized,
/// A generic error occurred.
///
/// This error code is used for all errors that do not have a specific error code.
genericError,
/// The permission to use the camera was denied.
permissionDenied,
}
... ...
/// The authorization state of the scanner.
enum MobileScannerState {
/// The scanner has not yet requested the required permissions.
undetermined,
/// The scanner has the required permissions.
authorized,
/// The user denied the required permissions.
denied
}
... ...
// This enum is not used yet
// enum Ratio { ratio_4_3, ratio_16_9 }
... ...
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
/// Torch is on.
on,
}
... ...
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide applyBoxFit;
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/objects/barcode_utility.dart';
enum Ratio { ratio_4_3, ratio_16_9 }
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
/// A widget showing a live camera preview.
/// The [MobileScanner] widget displays a live camera preview.
class MobileScanner extends StatefulWidget {
/// The controller of the camera.
/// The controller that manages the barcode scanner.
///
/// If this is null, the scanner will manage its own controller.
final MobileScannerController? controller;
/// Function that gets called when a Barcode is detected.
/// The [BoxFit] for the camera preview.
///
/// [barcode] The barcode object with all information about the scanned code.
/// [args] Information about the state of the MobileScanner widget
final Function(Barcode barcode, MobileScannerArguments? args) onDetect;
/// Defaults to [BoxFit.cover].
final BoxFit fit;
/// TODO: Function that gets called when the Widget is initialized. Can be usefull
/// to check wether the device has a torch(flash) or not.
///
/// [args] Information about the state of the MobileScanner widget
// final Function(MobileScannerArguments args)? onInitialize;
/// The function that signals when new codes were detected by the [controller].
final void Function(BarcodeCapture barcodes) onDetect;
/// Handles how the widget should fit the screen.
final BoxFit fit;
/// The function that signals when the barcode scanner is started.
@Deprecated('Use onScannerStarted() instead.')
final void Function(MobileScannerArguments? arguments)? onStart;
/// Set to false if you don't want duplicate scans.
final bool allowDuplicates;
/// The function that signals when the barcode scanner is started.
final void Function(MobileScannerArguments? arguments)? onScannerStarted;
/// The function that builds a placeholder widget when the scanner
/// is not yet displaying its camera preview.
///
/// If this is null, a black [ColoredBox] is used as placeholder.
final Widget Function(BuildContext, Widget?)? placeholderBuilder;
/// if set barcodes will only be scanned if they fall within this [Rect]
/// useful for having a cut-out overlay for example. these [Rect]
... ... @@ -35,14 +41,17 @@ class MobileScanner extends StatefulWidget {
/// [BoxFit]
final Rect? scanWindow;
/// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
/// Create a new [MobileScanner] using the provided [controller]
/// and [onBarcodeDetected] callback.
const MobileScanner({
super.key,
required this.onDetect,
this.controller,
this.fit = BoxFit.cover,
this.allowDuplicates = false,
required this.onDetect,
@Deprecated('Use onScannerStarted() instead.') this.onStart,
this.onScannerStarted,
this.placeholderBuilder,
this.scanWindow,
super.key,
});
@override
... ... @@ -51,32 +60,71 @@ class MobileScanner extends StatefulWidget {
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
late MobileScannerController controller;
/// The subscription that listens to barcode detection.
StreamSubscription<BarcodeCapture>? _barcodesSubscription;
/// The internally managed controller.
late MobileScannerController _controller;
/// Whether the controller should resume
/// when the application comes back to the foreground.
bool _resumeFromBackground = false;
/// Start the given [scanner].
void _startScanner(MobileScannerController scanner) {
if (!_controller.autoStart) {
debugPrint(
'mobile_scanner: not starting automatically because autoStart is set to false in the controller.',
);
return;
}
scanner.start().then((arguments) {
// ignore: deprecated_member_use_from_same_package
widget.onStart?.call(arguments);
widget.onScannerStarted?.call(arguments);
});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
controller = widget.controller ?? MobileScannerController();
if (!controller.isStarting) controller.start();
_controller = widget.controller ?? MobileScannerController();
_barcodesSubscription = _controller.barcodes.listen(
widget.onDetect,
);
if (!_controller.isStarting) {
_startScanner(_controller);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// App state changed before the controller was initialized.
if (_controller.isStarting) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
if (!controller.isStarting && controller.autoResume) controller.start();
_resumeFromBackground = false;
_startScanner(_controller);
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
_resumeFromBackground = true;
break;
case AppLifecycleState.inactive:
if (!_resumeFromBackground) {
_controller.stop();
}
break;
case AppLifecycleState.detached:
controller.stop();
break;
}
}
String? lastScanned;
/// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible,
/// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize]
///
... ... @@ -86,11 +134,11 @@ class _MobileScannerState extends State<MobileScanner>
/// since the textures size and the actuall image (on the texture size) might not be the same, we also need to
/// calculate the scanWindow in terms of percentages of the texture, not pixels.
Rect calculateScanWindowRelativeToTextureInPercentage(
BoxFit fit,
Rect scanWindow,
Size textureSize,
Size widgetSize,
) {
BoxFit fit,
Rect scanWindow,
Size textureSize,
Size widgetSize,
) {
/// map the texture size to get its new size after fitted to screen
final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);
... ... @@ -128,78 +176,53 @@ class _MobileScannerState extends State<MobileScanner>
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, BoxConstraints constraints) {
return ValueListenableBuilder(
valueListenable: controller.args,
builder: (context, value, child) {
value = value as MobileScannerArguments?;
if (value == null) {
return const ColoredBox(color: Colors.black);
} else {
if (widget.scanWindow != null) {
final window = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
value.size,
Size(constraints.maxWidth, constraints.maxHeight),
);
controller.updateScanWindow(window);
}
controller.barcodes.listen((barcode) {
if (!widget.allowDuplicates) {
if (lastScanned == barcode.rawValue) return;
lastScanned = barcode.rawValue;
widget.onDetect(barcode, value! as MobileScannerArguments);
} else {
widget.onDetect(barcode, value! as MobileScannerArguments);
}
});
return ClipRect(
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
return ValueListenableBuilder<MobileScannerArguments?>(
valueListenable: _controller.startArguments,
builder: (context, value, child) {
if (value == null) {
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
}
if (widget.scanWindow != null) {
final window = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
value.size,
Size(constraints.maxWidth, constraints.maxHeight),
);
controller.updateScanWindow(window);
}
return ClipRect(
child: LayoutBuilder(
builder: (_, constraints) {
return SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
),
);
}
},
},
),
);
},
);
}
@override
void didUpdateWidget(covariant MobileScanner oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller == null) {
if (widget.controller != null) {
controller.dispose();
controller = widget.controller!;
}
} else {
if (widget.controller == null) {
controller = MobileScannerController();
} else if (oldWidget.controller != widget.controller) {
controller = widget.controller!;
}
}
}
@override
void dispose() {
controller.dispose();
WidgetsBinding.instance.removeObserver(this);
_barcodesSubscription?.cancel();
_controller.dispose();
super.dispose();
}
}
... ...
... ... @@ -5,160 +5,125 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/objects/barcode_utility.dart';
import 'package:mobile_scanner/src/barcode_utility.dart';
/// The facing of a camera.
enum CameraFacing {
/// Front facing camera.
front,
/// Back facing camera.
back,
}
enum MobileScannerState { undetermined, authorized, denied }
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
/// The [MobileScannerController] holds all the logic of this plugin,
/// where as the [MobileScanner] class is the frontend of this plugin.
class MobileScannerController {
MobileScannerController({
this.facing = CameraFacing.back,
this.detectionSpeed = DetectionSpeed.normal,
this.detectionTimeoutMs = 250,
this.torchEnabled = false,
this.formats,
this.returnImage = false,
@Deprecated('Instead, use the result of calling `start()` to determine if permissions were granted.')
this.onPermissionSet,
this.autoStart = true,
}) {
// In case a new instance is created before calling dispose()
if (controllerHashcode != null) {
stop();
}
controllerHashcode = hashCode;
events = _eventChannel
.receiveBroadcastStream()
.listen((data) => _handleEvent(data as Map));
}
/// Torch is on.
on,
}
/// The hashcode of the controller to check if the correct object is mounted.
/// Must be static to keep the same value on new instances
static int? controllerHashcode;
// enum AnalyzeMode { none, barcode }
/// Select which camera should be used.
///
/// Default: CameraFacing.back
final CameraFacing facing;
class MobileScannerController {
MethodChannel methodChannel =
const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
EventChannel eventChannel =
const EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
/// Enable or disable the torch (Flash) on start
///
/// Default: disabled
final bool torchEnabled;
//Must be static to keep the same value on new instances
static int? _controllerHashcode;
StreamSubscription? events;
/// Set to true if you want to return the image buffer with the Barcode event
///
/// Only supported on iOS and Android
final bool returnImage;
final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null);
final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
late final ValueNotifier<CameraFacing> cameraFacingState;
final Ratio? ratio;
final bool? torchEnabled;
/// If provided, the scanner will only detect those specific formats
final List<BarcodeFormat>? formats;
/// If provided, the scanner will only detect those specific formats.
/// Sets the speed of detections.
///
/// WARNING: On iOS, only 1 format is supported.
final List<BarcodeFormat>? formats;
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
final DetectionSpeed detectionSpeed;
CameraFacing facing;
bool hasTorch = false;
late StreamController<Barcode> barcodesController;
/// Sets the timeout of scanner.
/// The timeout is set in miliseconds.
///
/// NOTE: The timeout only works if the [detectionSpeed] is set to
/// [DetectionSpeed.normal] (which is the default value).
final int detectionTimeoutMs;
/// Whether to automatically resume the camera when the application is resumed
bool autoResume;
/// Automatically start the mobileScanner on initialization.
final bool autoStart;
Stream<Barcode> get barcodes => barcodesController.stream;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
MobileScannerController({
this.facing = CameraFacing.back,
this.ratio,
this.torchEnabled,
this.formats,
this.autoResume = true,
}) {
// In case a new instance is created before calling dispose()
if (_controllerHashcode != null) {
stop();
}
_controllerHashcode = hashCode;
static const MethodChannel _methodChannel =
MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
static const EventChannel _eventChannel =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
cameraFacingState = ValueNotifier(facing);
@Deprecated(
'Instead, use the result of calling `start()` to determine if permissions were granted.',
)
Function(bool permissionGranted)? onPermissionSet;
// Sets analyze mode and barcode stream
barcodesController = StreamController.broadcast(
// onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),
// onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),
);
/// Listen to events from the platform specific code
late StreamSubscription events;
// Listen to events from the platform specific code
events = eventChannel
.receiveBroadcastStream()
.listen((data) => handleEvent(data as Map));
}
/// A notifier that provides several arguments about the MobileScanner
final ValueNotifier<MobileScannerArguments?> startArguments =
ValueNotifier(null);
void handleEvent(Map event) {
final name = event['name'];
final data = event['data'];
switch (name) {
case 'torchState':
final state = TorchState.values[data as int? ?? 0];
torchState.value = state;
break;
case 'barcode':
final barcode = Barcode.fromNative(data as Map? ?? {});
barcodesController.add(barcode);
break;
case 'barcodeMac':
barcodesController.add(
Barcode(
rawValue: (data as Map)['payload'] as String?,
),
);
break;
case 'barcodeWeb':
barcodesController.add(Barcode(rawValue: data as String?));
break;
default:
throw UnimplementedError();
}
}
/// A notifier that provides the state of the Torch (Flash)
final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
// TODO: Add more analyzers like text analyzer
// void setAnalyzeMode(int mode) {
// if (hashCode != _controllerHashcode) {
// return;
// }
// methodChannel.invokeMethod('analyze', mode);
// }
/// A notifier that provides the state of which camera is being used
late final ValueNotifier<CameraFacing> cameraFacingState =
ValueNotifier(facing);
// List<BarcodeFormats>? formats = _defaultBarcodeFormats,
bool isStarting = false;
/// Start barcode scanning. This will first check if the required permissions
/// are set.
Future<void> start() async {
ensure('startAsync');
if (isStarting) {
throw Exception('mobile_scanner: Called start() while already starting.');
}
isStarting = true;
// setAnalyzeMode(AnalyzeMode.barcode.index);
bool? _hasTorch;
// Check authorization status
if (!kIsWeb) {
MobileScannerState state = MobileScannerState
.values[await methodChannel.invokeMethod('state') as int? ?? 0];
switch (state) {
case MobileScannerState.undetermined:
final bool result =
await methodChannel.invokeMethod('request') as bool? ?? false;
state = result
? MobileScannerState.authorized
: MobileScannerState.denied;
break;
case MobileScannerState.denied:
isStarting = false;
throw PlatformException(code: 'NO ACCESS');
case MobileScannerState.authorized:
break;
}
/// Returns whether the device has a torch.
///
/// Throws an error if the controller is not initialized.
bool get hasTorch {
if (_hasTorch == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerUninitialized,
);
}
cameraFacingState.value = facing;
return _hasTorch!;
}
/// Set the starting arguments for the camera
Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
final Map<String, dynamic> arguments = {};
cameraFacingState.value = cameraFacingOverride ?? facing;
arguments['facing'] = cameraFacingState.value.index;
arguments['torch'] = torchEnabled;
arguments['speed'] = detectionSpeed.index;
arguments['timeout'] = detectionTimeoutMs;
// Set the starting arguments for the camera
final Map arguments = {};
arguments['facing'] = facing.index;
/* if (scanWindow != null) {
arguments['scanWindow'] = [
scanWindow!.left,
... ... @@ -167,8 +132,6 @@ class MobileScannerController {
scanWindow!.bottom,
];
} */
if (ratio != null) arguments['ratio'] = ratio;
if (torchEnabled != null) arguments['torch'] = torchEnabled;
if (formats != null) {
if (Platform.isAndroid) {
... ... @@ -177,92 +140,145 @@ class MobileScannerController {
arguments['formats'] = formats!.map((e) => e.rawValue).toList();
}
}
arguments['returnImage'] = true;
return arguments;
}
/// Start scanning for barcodes.
/// Upon calling this method, the necessary camera permission will be requested.
///
/// Returns an instance of [MobileScannerArguments]
/// when the scanner was successfully started.
/// Returns null if the scanner is currently starting.
///
/// Throws a [MobileScannerException] if starting the scanner failed.
Future<MobileScannerArguments?> start({
CameraFacing? cameraFacingOverride,
}) async {
if (isStarting) {
debugPrint("Called start() while starting.");
return null;
}
isStarting = true;
// Check authorization status
if (!kIsWeb) {
final MobileScannerState state = MobileScannerState
.values[await _methodChannel.invokeMethod('state') as int? ?? 0];
switch (state) {
case MobileScannerState.undetermined:
final bool result =
await _methodChannel.invokeMethod('request') as bool? ?? false;
if (!result) {
isStarting = false;
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
}
break;
case MobileScannerState.denied:
isStarting = false;
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
case MobileScannerState.authorized:
break;
}
}
// Start the camera with arguments
Map<String, dynamic>? startResult = {};
try {
startResult = await methodChannel.invokeMapMethod<String, dynamic>(
startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
'start',
arguments,
_argumentsToMap(cameraFacingOverride: cameraFacingOverride),
);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
if (error.code == "MobileScannerWeb") {
errorCode = MobileScannerErrorCode.permissionDenied;
}
isStarting = false;
// setAnalyzeMode(AnalyzeMode.none.index);
return;
throw MobileScannerException(
errorCode: errorCode,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
if (startResult == null) {
isStarting = false;
throw PlatformException(code: 'INITIALIZATION ERROR');
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
);
}
hasTorch = startResult['torchable'] as bool? ?? false;
if (kIsWeb) {
args.value = MobileScannerArguments(
webId: startResult['ViewID'] as String?,
size: Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
),
hasTorch: hasTorch,
);
} else {
args.value = MobileScannerArguments(
textureId: startResult['textureId'] as int?,
size: toSize(startResult['size'] as Map? ?? {}),
hasTorch: hasTorch,
);
_hasTorch = startResult['torchable'] as bool? ?? false;
if (_hasTorch! && torchEnabled) {
torchState.value = TorchState.on;
}
isStarting = false;
return startArguments.value = MobileScannerArguments(
size: kIsWeb
? Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
)
: toSize(startResult['size'] as Map? ?? {}),
hasTorch: _hasTorch!,
textureId: kIsWeb ? null : startResult['textureId'] as int?,
webId: kIsWeb ? startResult['ViewID'] as String? : null,
);
}
/// Stops the camera, but does not dispose this controller.
Future<void> stop() async {
try {
await methodChannel.invokeMethod('stop');
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
await _methodChannel.invokeMethod('stop');
} catch (e) {
debugPrint('$e');
}
}
/// Switches the torch on or off.
///
/// Only works if torch is available.
/// Does nothing if the device has no torch.
///
/// Throws if the controller was not initialized.
Future<void> toggleTorch() async {
ensure('toggleTorch');
if (!hasTorch) {
debugPrint('Device has no torch/flash.');
final hasTorch = _hasTorch;
if (hasTorch == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerUninitialized,
);
} else if (!hasTorch) {
return;
}
final TorchState state =
torchState.value =
torchState.value == TorchState.off ? TorchState.on : TorchState.off;
try {
await methodChannel.invokeMethod('torch', state.index);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
}
await _methodChannel.invokeMethod('torch', torchState.value.index);
}
/// Switches the torch on or off.
///
/// Only works if torch is available.
Future<void> switchCamera() async {
ensure('switchCamera');
try {
await methodChannel.invokeMethod('stop');
} on PlatformException catch (error) {
debugPrint(
'${error.code}: camera is stopped! Please start before switching camera.',
);
return;
}
facing =
facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back;
await start();
await _methodChannel.invokeMethod('stop');
final CameraFacing facingToUse =
cameraFacingState.value == CameraFacing.back
? CameraFacing.front
: CameraFacing.back;
await start(cameraFacingOverride: facingToUse);
}
/// Handles a local image file.
... ... @@ -271,28 +287,77 @@ class MobileScannerController {
///
/// [path] The path of the image on the devices
Future<bool> analyzeImage(String path) async {
return methodChannel
return _methodChannel
.invokeMethod<bool>('analyzeImage', path)
.then<bool>((bool? value) => value ?? false);
}
/// Disposes the MobileScannerController and closes all listeners.
///
/// If you call this, you cannot use this controller object anymore.
void dispose() {
if (hashCode == _controllerHashcode) {
stop();
events?.cancel();
events = null;
_controllerHashcode = null;
stop();
events.cancel();
_barcodesController.close();
if (hashCode == controllerHashcode) {
controllerHashcode = null;
}
barcodesController.close();
}
/// Checks if the MobileScannerController is bound to the correct MobileScanner object.
void ensure(String name) {
final message =
'MobileScannerController.$name called after MobileScannerController.dispose\n'
'MobileScannerController methods should not be used after calling dispose.';
assert(hashCode == _controllerHashcode, message);
/// Handles a returning event from the platform side
void _handleEvent(Map event) {
final name = event['name'];
final data = event['data'];
switch (name) {
case 'torchState':
final state = TorchState.values[data as int? ?? 0];
torchState.value = state;
break;
case 'barcode':
if (data == null) return;
final parsed = (data as List)
.map((value) => Barcode.fromNative(value as Map))
.toList();
_barcodesController.add(
BarcodeCapture(
barcodes: parsed,
image: event['image'] as Uint8List?,
),
);
break;
case 'barcodeMac':
_barcodesController.add(
BarcodeCapture(
barcodes: [
Barcode(
rawValue: (data as Map)['payload'] as String?,
)
],
),
);
break;
case 'barcodeWeb':
final barcode = data as Map?;
_barcodesController.add(
BarcodeCapture(
barcodes: [
Barcode(
rawValue: barcode?['rawValue'] as String?,
rawBytes: barcode?['rawBytes'] as Uint8List?,
)
],
),
);
break;
case 'error':
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(message: data as String?),
);
default:
throw UnimplementedError(name as String?);
}
}
/// updates the native scanwindow
... ...
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
/// This class represents an exception thrown by the mobile scanner.
class MobileScannerException implements Exception {
const MobileScannerException({
required this.errorCode,
this.errorDetails,
});
/// The error code of the exception.
final MobileScannerErrorCode errorCode;
/// The additional error details that came with the [errorCode].
final MobileScannerErrorDetails? errorDetails;
@override
String toString() {
if (errorDetails != null && errorDetails?.message != null) {
return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}";
}
return "MobileScannerException: ${errorCode.name}";
}
}
/// The raw error details for a [MobileScannerException].
class MobileScannerErrorDetails {
const MobileScannerErrorDetails({
this.code,
this.details,
this.message,
});
/// The error code from the [PlatformException].
final String? code;
/// The details from the [PlatformException].
final Object? details;
/// The error message from the [PlatformException].
final String? message;
}
... ...
import 'dart:typed_data';
import 'dart:ui';
import 'package:mobile_scanner/src/objects/barcode_utility.dart';
import 'package:mobile_scanner/src/barcode_utility.dart';
/// Represents a single recognized barcode and its value.
class Barcode {
... ...
import 'dart:typed_data';
import 'package:mobile_scanner/src/objects/barcode.dart';
/// The return object after a frame is scanned.
///
/// [barcodes] A list with barcodes. A scanned frame can contain multiple
/// barcodes.
/// [image] If enabled, an image of the scanned frame.
class BarcodeCapture {
final List<Barcode> barcodes;
final Uint8List? image;
BarcodeCapture({
required this.barcodes,
this.image,
});
}
... ...
import 'package:flutter/material.dart';
/// Camera args for [CameraView].
/// The start arguments of the scanner.
class MobileScannerArguments {
/// The texture id.
final int? textureId;
/// Size of the texture.
/// The output size of the camera.
/// This value can be used to draw a box in the image.
final Size size;
/// A bool which is true if the device has a torch.
final bool hasTorch;
/// The texture id of the capture used internally.
final int? textureId;
/// The texture id of the capture used internally if device is web.
final String? webId;
/// Create a [MobileScannerArguments].
MobileScannerArguments({
this.textureId,
required this.size,
required this.hasTorch,
this.textureId,
this.webId,
});
}
... ...
import 'dart:html';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/objects/barcode.dart';
import 'package:mobile_scanner/src/web/media.dart';
abstract class WebBarcodeReaderBase {
/// Timer used to capture frames to be analyzed
final Duration frameInterval;
final DivElement videoContainer;
const WebBarcodeReaderBase({
required this.videoContainer,
this.frameInterval = const Duration(milliseconds: 200),
});
bool get isStarted;
int get videoWidth;
int get videoHeight;
/// Starts streaming video
Future<void> start({
required CameraFacing cameraFacing,
});
/// Starts scanning QR codes or barcodes
Stream<Barcode?> detectBarcodeContinuously();
/// Stops streaming video
Future<void> stop();
/// Can enable or disable the flash if available
Future<void> toggleTorch({required bool enabled});
/// Determine whether device has flash
bool get hasTorch;
}
mixin InternalStreamCreation on WebBarcodeReaderBase {
/// The video stream.
/// Will be initialized later to see which camera needs to be used.
MediaStream? localMediaStream;
final VideoElement video = VideoElement();
@override
int get videoWidth => video.videoWidth;
@override
int get videoHeight => video.videoHeight;
Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async {
// Check if browser supports multiple camera's and set if supported
final Map? capabilities =
window.navigator.mediaDevices?.getSupportedConstraints();
final Map<String, dynamic> constraints;
if (capabilities != null && capabilities['facingMode'] as bool) {
constraints = {
'video': VideoOptions(
facingMode:
cameraFacing == CameraFacing.front ? 'user' : 'environment',
)
};
} else {
constraints = {'video': true};
}
final stream =
await window.navigator.mediaDevices?.getUserMedia(constraints);
return stream;
}
void prepareVideoElement(VideoElement videoSource);
Future<void> attachStreamToVideo(
MediaStream stream,
VideoElement videoSource,
);
@override
Future<void> stop() async {
try {
// Stop the camera stream
localMediaStream?.getTracks().forEach((track) {
if (track.readyState == 'live') {
track.stop();
}
});
} catch (e) {
debugPrint('Failed to stop stream: $e');
}
video.srcObject = null;
localMediaStream = null;
videoContainer.children = [];
}
}
/// Mixin for libraries that don't have built-in torch support
mixin InternalTorchDetection on InternalStreamCreation {
@override
bool get hasTorch {
// TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533
// final track = _localStream?.getVideoTracks();
// if (track != null) {
// final imageCapture = html.ImageCapture(track.first);
// final photoCapabilities = await imageCapture.getPhotoCapabilities();
// }
return false;
}
@override
Future<void> toggleTorch({required bool enabled}) async {
if (hasTorch) {
final track = localMediaStream?.getVideoTracks();
await track?.first.applyConstraints({
'advanced': [
{'torch': enabled}
]
});
}
}
}
... ...
@JS()
library jsqr;
import 'dart:async';
import 'dart:html';
import 'dart:typed_data';
import 'package:js/js.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/objects/barcode.dart';
import 'package:mobile_scanner/src/web/base.dart';
@JS('jsQR')
external Code? jsQR(dynamic data, int? width, int? height);
... ... @@ -9,4 +16,75 @@ external Code? jsQR(dynamic data, int? width, int? height);
@JS()
class Code {
external String get data;
external Uint8ClampedList get binaryData;
}
class JsQrCodeReader extends WebBarcodeReaderBase
with InternalStreamCreation, InternalTorchDetection {
JsQrCodeReader({required super.videoContainer});
@override
bool get isStarted => localMediaStream != null;
@override
Future<void> start({
required CameraFacing cameraFacing,
}) async {
videoContainer.children = [video];
final stream = await initMediaStream(cameraFacing);
prepareVideoElement(video);
if (stream != null) {
await attachStreamToVideo(stream, video);
}
}
@override
void prepareVideoElement(VideoElement videoSource) {
// required to tell iOS safari we don't want fullscreen
videoSource.setAttribute('playsinline', 'true');
}
@override
Future<void> attachStreamToVideo(
MediaStream stream,
VideoElement videoSource,
) async {
localMediaStream = stream;
videoSource.srcObject = stream;
await videoSource.play();
}
@override
Stream<Barcode?> detectBarcodeContinuously() async* {
yield* Stream.periodic(frameInterval, (_) {
return _captureFrame(video);
}).asyncMap((event) async {
final code = await event;
if (code == null) {
return null;
}
return Barcode(
rawValue: code.data,
rawBytes: Uint8List.fromList(code.binaryData),
format: BarcodeFormat.qrCode,
);
});
}
/// Captures a frame and analyzes it for QR codes
Future<Code?> _captureFrame(VideoElement video) async {
if (localMediaStream == null) return null;
final canvas =
CanvasElement(width: video.videoWidth, height: video.videoHeight);
final ctx = canvas.context2D;
ctx.drawImage(video, 0, 0);
final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!);
final code = jsQR(imgData.data, canvas.width, canvas.height);
return code;
}
}
... ...
@JS()
library qrscanner;
import 'package:js/js.dart';
@JS('QrScanner')
external String scanImage(dynamic data);
@JS()
class QrScanner {
external String get scanImage;
}