Julian Steenbakker
Committed by GitHub

Merge branch 'master' into pause_function

Showing 56 changed files with 916 additions and 632 deletions
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
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"
commit-message:
prefix: "chore"
include: "scope"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
include: "scope"
reviewers:
- "juliansteenbakker"
# version: 2
# updates:
# - package-ecosystem: "github-actions"
# directory: "/"
# schedule:
# 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"
# # commit-message:
# # prefix: "chore"
# # include: "scope"
# - package-ecosystem: "pub"
# directory: "/"
# schedule:
# interval: "weekly"
# commit-message:
# prefix: "chore"
# include: "scope"
# reviewers:
# - "juliansteenbakker"
... ...
... ... @@ -7,7 +7,7 @@
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v4.0.2
- uses: GoogleCloudPlatform/release-please-action@v4.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: simple
... ...
## 5.0.0-beta.2
## 5.1.1
* This release fixes an issue with automatic starts in the examples.
## 5.1.0
This updates reverts a few breaking changes made in v5.0.0 in order to keep things simple.
* The `onDetect` method has been reinstated in the `MobileScanner` widget, but is nullable. You can
still listen to `MobileScannerController.barcodes` directly by passing null to this parameter.
* The `autoStart` attribute has been reinstated in the `MobileScannerController` and defaults to true. However, if you want
to control which camera is used on start, or you want to manage the lifecycle yourself, you should set
autoStart to false and manually call `MobileScannerController.start({CameraFacing? cameraDirection})`.
* The `controller` is no longer required in the `MobileScanner` widget. However if provided, the user should take care
of disposing it.
* [Android] Revert Gradle 8 back to Gradle 7, to be inline with most Flutter plugins and prevent build issues.
* [Android] Revert Kotlin back from 1.9 to 1.7 to be inline with most Flutter plugins. Special 1.9 functionality
has been refactored to be compatible with 1.7.
## 5.0.2
Bugs fixed:
* Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !)
* Fixed an issue that causes the initial torch state to be out of sync.
Improvements:
* Updated the lifeycle code sample to handle not-initialized controllers.
## 5.0.1
Improvements:
* Adjusted the platform checks to use the defaultTargetPlatform API, so that tests can use the correct platform overrides.
## 5.0.0
This major release contains all the changes from the 5.0.0 beta releases, along with the following changes:
Improvements:
- [Android] Remove the Kotlin Standard Library from the dependencies, as it is automatically included in Kotlin 1.4+
## 5.0.0-beta.3
**BREAKING CHANGES:**
* Flutter 3.16.0 is now required.
* Flutter 3.19.0 is now required.
* [iOS] iOS 12.0 is now the minimum supported iOS version.
* [iOS] Adds a Privacy Manifest.
Bugs fixed:
* Fixed an issue where the camera preview and barcode scanner did not work the second time on web.
Improvements:
* [web] Migrates to extension types. (thanks @koji-1009 !)
## 5.0.0-beta.2
Bugs fixed:
* Fixed an issue where the scan window was not updated when its size was changed. (thanks @navaronbracke !)
## 5.0.0-beta.1
**BREAKING CHANGES:**
* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`.
... ... @@ -39,7 +82,6 @@ Bugs fixed:
* [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !)
## 4.0.0
**BREAKING CHANGES:**
* [Android] compileSdk has been upgraded to version 34.
... ...
... ... @@ -7,6 +7,28 @@
A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS.
## Breaking Changes v5.0.0
Version 5.0.0 brings some breaking changes. However, some are reverted in version 5.1.0. Please see the list below for all breaking changes, and Changelog.md for a more detailed list.
* ~~The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand.~~ (Reverted in version 5.1.0)
* ~~A controller is now required for the `MobileScanner` widget.~~ (Reverted in version 5.1.0)
* ~~The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly.~~ (Reverted in version 5.1.0)
* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`.
* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion.
* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type.
* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`.
* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean.
* The `formats` attribute of the `MobileScannerController` is now non-null.
* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`.
* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead.
* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value.
* The `TorchState` enum now provides a new value for unavailable flashlights.
* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`.
* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers.
* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay.
* The torch can no longer be toggled on the web, as this is only available for image tracks and not video tracks. As a result the torch state for the web will always be `TorchState.unavailable`.
* The zoom scale can no longer be modified on the web, as this is only available for image tracks and not video tracks. As a result, the zoom scale will always be `1.0`.
## Features Supported
See the example app for detailed implementation information.
... ... @@ -103,7 +125,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// If the controller is not ready, do not try to start or stop it.
// Permission dialogs can trigger lifecycle changes before the controller is ready.
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
... ... @@ -165,5 +191,5 @@ Future<void> dispose() async {
To display the camera preview, pass the controller to a `MobileScanner` widget.
See the examples for runnable examples of various usages,
See the [examples](example/README.md) for runnable examples of various usages,
such as the basic usage, applying a scan window, or retrieving images from the barcodes.
... ...
... ... @@ -9,7 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'com.android.tools.build:gradle:8.3.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
... ... @@ -32,12 +32,12 @@ android {
compileSdk 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '17'
jvmTarget = '1.8'
}
sourceSets {
... ... @@ -64,8 +64,6 @@ android {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
def useUnbundled = project.findProperty('dev.steenbakker.mobile_scanner.useUnbundled') ?: false
if (useUnbundled.toBoolean()) {
// Dynamically downloaded model via Google Play Services
... ... @@ -75,9 +73,13 @@ dependencies {
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
}
implementation 'androidx.camera:camera-camera2:1.3.1'
implementation 'androidx.camera:camera-lifecycle:1.3.1'
// org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions.
// See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
implementation 'androidx.camera:camera-lifecycle:1.3.3'
implementation 'androidx.camera:camera-camera2:1.3.3'
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.10.0'
testImplementation 'org.mockito:mockito-core:5.12.0'
}
... ...
... ... @@ -19,6 +19,8 @@ import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.TorchState
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
... ... @@ -367,11 +369,22 @@ class MobileScanner(
val height = resolution.height.toDouble()
val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0
// Start with 'unavailable' torch state.
var currentTorchState: Int = -1
camera?.cameraInfo?.let {
if (!it.hasFlashUnit()) {
return@let
}
currentTorchState = it.torchState.value ?: -1
}
mobileScannerStartedCallback(
MobileScannerStartParameters(
if (portrait) width else height,
if (portrait) height else width,
camera?.cameraInfo?.hasFlashUnit() ?: false,
currentTorchState,
textureEntry!!.id(),
numberOfCameras ?: 0
)
... ... @@ -433,13 +446,16 @@ class MobileScanner(
/**
* Toggles the flash light on or off.
*/
fun toggleTorch(enableTorch: Boolean) {
if (camera == null) {
return
}
fun toggleTorch() {
camera?.let {
if (!it.cameraInfo.hasFlashUnit()) {
return@let
}
if (camera?.cameraInfo?.hasFlashUnit() == true) {
camera?.cameraControl?.enableTorch(enableTorch)
when(it.cameraInfo.torchState.value) {
TorchState.OFF -> it.cameraControl.enableTorch(true)
TorchState.ON -> it.cameraControl.enableTorch(false)
}
}
}
... ...
... ... @@ -74,6 +74,7 @@ class MobileScannerHandler(
private var mobileScanner: MobileScanner? = null
private val torchStateCallback: TorchStateCallback = {state: Int ->
// Off = 0, On = 1
barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
}
... ... @@ -121,9 +122,9 @@ class MobileScannerHandler(
}
})
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"pause" -> pause(result)
"stop" -> stop(result)
"toggleTorch" -> toggleTorch(result)
"analyzeImage" -> analyzeImage(call, result)
"setScale" -> setScale(call, result)
"resetScale" -> resetScale(result)
... ... @@ -168,7 +169,8 @@ class MobileScannerHandler(
val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
val detectionSpeed: DetectionSpeed = if (speed == 0) DetectionSpeed.NO_DUPLICATES
else if (speed ==1) DetectionSpeed.NORMAL else DetectionSpeed.UNRESTRICTED
mobileScanner!!.start(
barcodeScannerOptions,
... ... @@ -183,7 +185,7 @@ class MobileScannerHandler(
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit,
"currentTorchState" to it.currentTorchState,
"numberOfCameras" to it.numberOfCameras
))
}
... ... @@ -256,8 +258,8 @@ class MobileScannerHandler(
mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
mobileScanner!!.toggleTorch(call.arguments == 1)
private fun toggleTorch(result: MethodChannel.Result) {
mobileScanner?.toggleTorch()
result.success(null)
}
... ...
... ... @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects
class MobileScannerStartParameters(
val width: Double = 0.0,
val height: Double,
val hasFlashUnit: Boolean,
val currentTorchState: Int,
val id: Long,
val numberOfCameras: Int
)
\ No newline at end of file
... ...
... ... @@ -2,15 +2,66 @@
Demonstrates how to use the mobile_scanner plugin.
## Getting Started
## Run Examples
This project is a starting point for a Flutter application.
1. `git clone https://github.com/juliansteenbakker/mobile_scanner.git`
2. `cd mobile_scanner/examples/lib`
3. `flutter pub get`
4. `flutter run`
A few resources to get you started if this is your first Flutter project:
## Examples Overview
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
### With Controller
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
Scanner widget with control buttons overlay. Shows first detected barcode.
(See ListView example for detecting and displaying multiple barcodes at the same time.)
* Displays Flashlight, SwitchCamera and Start/Stop buttons.
* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera.
* Displays Gallery button to use images as source for analysis.
* Handles changes in AppLifecycleState.
### With ListView
Scanner widget with control buttons overlay. Shows all barcodes detected at the same time in a ListView.
* Displays Flashlight, SwitchCamera and Start/Stop buttons.
* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera.
* Displays Gallery button to use images as source for analysis.
### With Zoom Slider
Scanner widget with control buttons and zoom slider overlay. Shows first detected barcode.
* Displays Flashlight, SwitchCamera and Start/Stop buttons and zoom slider.
* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera, set zoom scale.
* Displays Gallery button to use images as source for analysis.
### With Controller (returning image)
Scanner widget with control buttons overlay. Shows the first detected barcode and the captured image.
* Displays Flashlight, SwitchCamera and Start/Stop buttons.
* Uses `MobileScannerController` to start/stop, toggle flashlight, switch camera.
* Displays captured image that contains the detected barcode.
### With Page View
Scanner widget in one of many pages that can be swiped horizontally. Starts and stops scanner depending on page visibility.
* Shows first detected barcode.
### With Scan Window
Scanner widget with scan window overlay. Barcodes are only detected inside the scan window.
* Draws scan window - a half-transparent overlay with a cut out middle part.
* Draws bounding box around (first) detected barcode. (not working on every device)
### With Overlay
Scanner widget with scan window overlay. Barcodes are only detected inside the scan window.
* Draws scan window - a half-transparent overlay with a cut out middle part that has a border with rounded corners.
* Displays Flashlight, SwitchCamera buttons.
* Uses `MobileScannerController` to toggle flashlight, switch camera.
... ...
... ... @@ -25,16 +25,14 @@ if (flutterVersionName == null) {
android {
namespace "dev.steenbakker.mobile_scanner_example"
compileSdk 34
ndkVersion "25.1.8937393"
// ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '17'
jvmTarget = '1.8'
}
sourceSets {
... ... @@ -62,7 +60,3 @@ android {
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
... ...
buildscript {
ext.kotlin_version = '1.9.22'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
... ...
#Thu May 02 10:24:49 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
... ...
... ... @@ -5,12 +5,21 @@ pluginManagement {
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include ":app"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.22" apply false
}
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"
include ":app"
\ No newline at end of file
... ...
... ... @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
<string>12.0</string>
</dict>
</plist>
... ...
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ... @@ -41,7 +41,7 @@ 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['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end
... ...
... ... @@ -198,6 +198,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */,
BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */,
);
buildRules = (
);
... ... @@ -215,7 +216,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
... ... @@ -317,6 +318,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
... ... @@ -452,7 +470,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -495,7 +513,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
... ... @@ -513,7 +531,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
... ... @@ -529,7 +547,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
... ... @@ -583,7 +601,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
... ... @@ -632,7 +650,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ...
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
... ...
... ... @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
... ... @@ -28,6 +30,8 @@
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photos access to get QR code from photo library</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
... ... @@ -47,9 +51,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
... ...
... ... @@ -16,12 +16,9 @@ class BarcodeScannerWithController extends StatefulWidget {
class _BarcodeScannerWithControllerState
extends State<BarcodeScannerWithController> with WidgetsBindingObserver {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true, useNewCameraSelector: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
autoStart: false,
torchEnabled: true,
useNewCameraSelector: true,
);
Barcode? _barcode;
... ... @@ -63,7 +60,9 @@ class _BarcodeScannerWithControllerState
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
... ...
... ... @@ -15,20 +15,8 @@ class BarcodeScannerListView extends StatefulWidget {
class _BarcodeScannerListViewState extends State<BarcodeScannerListView> {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
@override
void initState() {
super.initState();
controller.start();
}
Widget _buildBarcodesListView() {
return StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
... ...
... ... @@ -18,12 +18,6 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> {
final PageController pageController = PageController();
@override
void initState() {
super.initState();
unawaited(controller.start());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('With PageView')),
... ...
... ... @@ -18,20 +18,10 @@ class _BarcodeScannerReturningImageState
extends State<BarcodeScannerReturningImage> {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
returnImage: true,
);
@override
void initState() {
super.initState();
controller.start();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Returning image')),
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerSimple extends StatefulWidget {
const BarcodeScannerSimple({super.key});
@override
State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState();
}
class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
Barcode? _barcode;
Widget _buildBarcode(Barcode? value) {
if (value == null) {
return const Text(
'Scan something!',
overflow: TextOverflow.fade,
style: TextStyle(color: Colors.white),
);
}
return Text(
value.displayValue ?? 'No display value.',
overflow: TextOverflow.fade,
style: const TextStyle(color: Colors.white),
);
}
void _handleBarcode(BarcodeCapture barcodes) {
if (mounted) {
setState(() {
_barcode = barcodes.barcodes.firstOrNull;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Simple scanner')),
backgroundColor: Colors.black,
body: Stack(
children: [
MobileScanner(
onDetect: _handleBarcode,
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(child: Center(child: _buildBarcode(_barcode))),
],
),
),
),
],
),
);
}
}
... ...
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
... ... @@ -19,13 +17,6 @@ class _BarcodeScannerWithScanWindowState
extends State<BarcodeScannerWithScanWindow> {
final MobileScannerController controller = MobileScannerController();
@override
void initState() {
super.initState();
controller.start();
}
Widget _buildBarcodeOverlay() {
return ValueListenableBuilder(
valueListenable: controller,
... ... @@ -204,7 +195,7 @@ class BarcodeOverlay extends CustomPainter {
final double ratioWidth;
final double ratioHeight;
if (!kIsWeb && Platform.isIOS) {
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
ratioWidth = barcodeSize.width / adjustedSize.destination.width;
ratioHeight = barcodeSize.height / adjustedSize.destination.height;
} else {
... ...
... ... @@ -22,12 +22,6 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> {
double _zoomFactor = 0.0;
@override
void initState() {
super.initState();
controller.start();
}
Widget _buildZoomScaleSlider() {
return ValueListenableBuilder(
valueListenable: controller,
... ...
... ... @@ -3,6 +3,7 @@ import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_listview.dart';
import 'package:mobile_scanner_example/barcode_scanner_pageview.dart';
import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
import 'package:mobile_scanner_example/barcode_scanner_simple.dart';
import 'package:mobile_scanner_example/barcode_scanner_window.dart';
import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
import 'package:mobile_scanner_example/mobile_scanner_overlay.dart';
... ... @@ -31,6 +32,16 @@ class MyHome extends StatelessWidget {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerSimple(),
),
);
},
child: const Text('MobileScanner Simple'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerListView(),
),
);
... ...
... ... @@ -16,12 +16,6 @@ class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> {
);
@override
void initState() {
super.initState();
controller.start();
}
@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
center: MediaQuery.sizeOf(context).center(Offset.zero),
... ...
... ... @@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget {
}
switch (state.torchState) {
case TorchState.auto:
return IconButton(
color: Colors.white,
iconSize: 32.0,
icon: const Icon(Icons.flash_auto),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.off:
return IconButton(
color: Colors.white,
... ...
... ... @@ -259,7 +259,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
... ...
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
... ...
... ... @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1
environment:
sdk: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
... ...
... ... @@ -268,12 +268,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
// as they interact with the hardware camera.
if (torch) {
DispatchQueue.main.async {
do {
try self.toggleTorch(.on)
} catch {
// If the torch does not turn on,
// continue with the capture session anyway.
}
self.turnTorchOn()
}
}
... ... @@ -292,13 +287,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
// as this does not change the configuration of the hardware camera.
let dimensions = CMVideoFormatDescriptionGetDimensions(
device.activeFormat.formatDescription)
let hasTorch = device.hasTorch
completion(
MobileScannerStartParameters(
width: Double(dimensions.height),
height: Double(dimensions.width),
hasTorch: hasTorch,
currentTorchState: device.hasTorch ? device.torchMode.rawValue : -1,
textureId: self.textureId ?? 0
)
)
... ... @@ -355,30 +349,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
textureId = nil
}
/// Set the torch mode.
/// Toggle the torch.
///
/// This method should be called on the main DispatchQueue.
func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
func toggleTorch() {
guard let device = self.device else {
return
}
if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(torch)) {
if (!device.hasTorch || !device.isTorchAvailable) {
return
}
if (device.torchMode != torch) {
var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode
switch(device.torchMode) {
case AVCaptureDevice.TorchMode.auto:
newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on
break;
case AVCaptureDevice.TorchMode.off:
newTorchMode = AVCaptureDevice.TorchMode.on
break;
case AVCaptureDevice.TorchMode.on:
newTorchMode = AVCaptureDevice.TorchMode.off
break;
default:
return;
}
if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) {
return;
}
do {
try device.lockForConfiguration()
device.torchMode = torch
device.torchMode = newTorchMode
device.unlockForConfiguration()
} catch(_) {}
}
/// Turn the torch on.
private func turnTorchOn() {
guard let device = self.device else {
return
}
if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(.on) || device.torchMode == .on) {
return
}
do {
try device.lockForConfiguration()
device.torchMode = .on
device.unlockForConfiguration()
} catch(_) {}
}
// 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
// Off = 0, On = 1, Auto = 2
let state = change?[.newKey] as? Int
torchModeChangeCallback(state)
case "videoZoomFactor":
... ... @@ -490,7 +521,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var currentTorchState: Int = -1
var textureId: Int64 = 0
}
}
... ...
... ... @@ -82,8 +82,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
pause(result)
case "stop":
stop(result)
case "torch":
toggleTorch(call, result)
case "toggleTorch":
toggleTorch(result)
case "analyzeImage":
analyzeImage(call, result)
case "setScale":
... ... @@ -127,7 +127,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
result([
"textureId": parameters.textureId,
"size": ["width": parameters.width, "height": parameters.height],
"torchable": parameters.hasTorch])
"currentTorchState": parameters.currentTorchState,
])
}
}
} catch MobileScannerError.alreadyStarted {
... ... @@ -166,13 +167,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
}
/// Toggles the torch.
private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
do {
try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
result(nil)
} catch {
result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil))
}
private func toggleTorch(_ result: @escaping FlutterResult) {
mobileScanner.toggleTorch()
result(nil)
}
/// Sets the zoomScale.
... ...
... ... @@ -8,31 +8,6 @@ extension CVBuffer {
let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)
return UIImage(cgImage: cgImage!)
}
var image1: UIImage {
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly)
// Get the number of bytes per row for the pixel buffer
let baseAddress = CVPixelBufferGetBaseAddress(self)
// Get the number of bytes per row for the pixel buffer
let bytesPerRow = CVPixelBufferGetBytesPerRow(self)
// Get the pixel buffer width and height
let width = CVPixelBufferGetWidth(self)
let height = CVPixelBufferGetHeight(self)
// Create a device-dependent RGB color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
// Create a bitmap graphics context with the sample buffer data
var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue
bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue
//let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue
let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
// Create a Quartz image from the pixel data in the bitmap graphics context
let quartzImage = context?.makeImage()
// Unlock the pixel buffer
CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly)
// Create an image object from the Quartz image
return UIImage(cgImage: quartzImage!)
}
}
extension UIDeviceOrientation {
... ...
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>
... ...
... ... @@ -4,21 +4,22 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '3.5.6'
s.version = '5.1.1'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
DESC
s.homepage = 'http://example.com'
s.homepage = 'https://github.com/juliansteenbakker/mobile_scanner'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 4.0.0'
s.platform = :ios, '11.0'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 6.0.0'
s.platform = :ios, '12.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' }
s.swift_version = '5.0'
s.resource_bundles = { 'mobile_scanner_privacy' => ['Resources/PrivacyInfo.xcprivacy'] }
end
... ...
/// The state of the flashlight.
enum TorchState {
/// The flashlight turns on automatically in low light conditions.
///
/// This is currently only supported on iOS and MacOS.
auto(2),
/// The flashlight is off.
off(0),
... ... @@ -7,18 +12,20 @@ enum TorchState {
on(1),
/// The flashlight is unavailable.
unavailable(2);
unavailable(-1);
const TorchState(this.rawValue);
factory TorchState.fromRawValue(int value) {
switch (value) {
case -1:
return TorchState.unavailable;
case 0:
return TorchState.off;
case 1:
return TorchState.on;
case 2:
return TorchState.unavailable;
return TorchState.auto;
default:
throw ArgumentError.value(value, 'value', 'Invalid raw value.');
}
... ...
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
... ... @@ -55,7 +55,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
final List<Map<Object?, Object?>> barcodes =
data.cast<Map<Object?, Object?>>();
if (Platform.isMacOS) {
if (defaultTargetPlatform == TargetPlatform.macOS) {
return BarcodeCapture(
raw: event,
barcodes: barcodes
... ... @@ -71,7 +71,8 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
}
if (Platform.isAndroid || Platform.isIOS) {
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS) {
final double? width = event['width'] as double?;
final double? height = event['height'] as double?;
... ... @@ -95,12 +96,29 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
///
/// Throws a [MobileScannerException] if the permission is not granted.
Future<void> _requestCameraPermission() async {
final MobileScannerAuthorizationState authorizationState;
try {
authorizationState = MobileScannerAuthorizationState.fromRawValue(
final MobileScannerAuthorizationState authorizationState =
MobileScannerAuthorizationState.fromRawValue(
await methodChannel.invokeMethod<int>('state') ?? 0,
);
switch (authorizationState) {
// Authorization was already granted, no need to request it again.
case MobileScannerAuthorizationState.authorized:
return;
// Android does not have an undetermined authorization state.
// So if the permission was denied, request it again.
case MobileScannerAuthorizationState.denied:
case MobileScannerAuthorizationState.undetermined:
final bool permissionGranted =
await methodChannel.invokeMethod<bool>('request') ?? false;
if (!permissionGranted) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
}
}
} on PlatformException catch (error) {
// If the permission state is invalid, that is an error.
throw MobileScannerException(
... ... @@ -112,37 +130,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
),
);
}
switch (authorizationState) {
case MobileScannerAuthorizationState.authorized:
return; // Already authorized.
// Android does not have an undetermined authorization state.
// So if the permission was denied, request it again.
case MobileScannerAuthorizationState.denied:
case MobileScannerAuthorizationState.undetermined:
try {
final bool granted =
await methodChannel.invokeMethod<bool>('request') ?? false;
if (granted) {
return; // Authorization was granted.
}
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
} on PlatformException catch (error) {
// If the permission state is invalid, that is an error.
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
}
}
@override
... ... @@ -192,15 +179,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
}
@override
Future<void> setTorchState(TorchState torchState) async {
if (torchState == TorchState.unavailable) {
return;
}
await methodChannel.invokeMethod<void>('torch', torchState.rawValue);
}
@override
Future<void> setZoomScale(double zoomScale) async {
await methodChannel.invokeMethod<void>('setScale', zoomScale);
}
... ... @@ -260,7 +238,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
_textureId = textureId;
final int? numberOfCameras = startResult['numberOfCameras'] as int?;
final bool hasTorch = startResult['torchable'] as bool? ?? false;
final TorchState currentTorchState = TorchState.fromRawValue(
startResult['currentTorchState'] as int? ?? -1,
);
final Map<Object?, Object?>? sizeInfo =
startResult['size'] as Map<Object?, Object?>?;
... ... @@ -278,7 +258,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
_pausing = false;
return MobileScannerViewAttributes(
hasTorch: hasTorch,
currentTorchMode: currentTorchState,
numberOfCameras: numberOfCameras,
size: size,
);
... ... @@ -309,6 +289,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
}
@override
Future<void> toggleTorch() async {
await methodChannel.invokeMethod<void>('toggleTorch');
}
@override
Future<void> updateScanWindow(Rect? window) async {
if (_textureId == null) {
return;
... ...
... ... @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
import 'package:mobile_scanner/src/scan_window_calculation.dart';
... ... @@ -18,7 +19,8 @@ typedef MobileScannerErrorBuilder = Widget Function(
class MobileScanner extends StatefulWidget {
/// Create a new [MobileScanner] using the provided [controller].
const MobileScanner({
required this.controller,
this.controller,
this.onDetect,
this.fit = BoxFit.cover,
this.errorBuilder,
this.overlayBuilder,
... ... @@ -29,7 +31,11 @@ class MobileScanner extends StatefulWidget {
});
/// The controller for the camera preview.
final MobileScannerController controller;
final MobileScannerController? controller;
/// The function that signals when new codes were detected by the [controller].
/// If null, use the controller.barcodes stream directly to capture barcodes.
final void Function(BarcodeCapture barcodes)? onDetect;
/// The error builder for the camera preview.
///
... ... @@ -112,7 +118,10 @@ class MobileScanner extends StatefulWidget {
State<MobileScanner> createState() => _MobileScannerState();
}
class _MobileScannerState extends State<MobileScanner> {
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
late final controller = widget.controller ?? MobileScannerController();
/// The current scan window.
Rect? scanWindow;
... ... @@ -139,7 +148,7 @@ class _MobileScannerState extends State<MobileScanner> {
if (scanWindow == null) {
scanWindow = newScanWindow;
unawaited(widget.controller.updateScanWindow(scanWindow));
unawaited(controller.updateScanWindow(scanWindow));
return;
}
... ... @@ -154,7 +163,7 @@ class _MobileScannerState extends State<MobileScanner> {
if (widget.scanWindowUpdateThreshold == 0.0) {
scanWindow = newScanWindow;
unawaited(widget.controller.updateScanWindow(scanWindow));
unawaited(controller.updateScanWindow(scanWindow));
return;
}
... ... @@ -167,14 +176,14 @@ class _MobileScannerState extends State<MobileScanner> {
dy >= widget.scanWindowUpdateThreshold) {
scanWindow = newScanWindow;
unawaited(widget.controller.updateScanWindow(scanWindow));
unawaited(controller.updateScanWindow(scanWindow));
}
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<MobileScannerState>(
valueListenable: widget.controller,
valueListenable: controller,
builder: (BuildContext context, MobileScannerState value, Widget? child) {
if (!value.isInitialized) {
const Widget defaultPlaceholder = ColoredBox(color: Colors.black);
... ... @@ -234,10 +243,62 @@ class _MobileScannerState extends State<MobileScanner> {
);
}
StreamSubscription? _subscription;
@override
void initState() {
if (widget.onDetect != null) {
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(widget.onDetect);
}
if (controller.autoStart) {
controller.start();
}
super.initState();
}
@override
void dispose() {
super.dispose();
if (_subscription != null) {
_subscription!.cancel();
_subscription = null;
}
if (controller.autoStart) {
controller.stop();
}
// When this widget is unmounted, reset the scan window.
unawaited(widget.controller.updateScanWindow(null));
unawaited(controller.updateScanWindow(null));
// Dispose default controller if not provided by user
if (widget.controller == null) {
controller.dispose();
WidgetsBinding.instance.removeObserver(this);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (widget.controller != null) return;
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(widget.onDetect);
unawaited(controller.start());
case AppLifecycleState.inactive:
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
}
... ...
... ... @@ -17,6 +17,7 @@ import 'package:mobile_scanner/src/objects/start_options.dart';
class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Construct a new [MobileScannerController] instance.
MobileScannerController({
this.autoStart = true,
this.cameraResolution,
this.detectionSpeed = DetectionSpeed.normal,
int detectionTimeoutMs = 250,
... ... @@ -47,6 +48,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Currently only supported on Android.
final Size? cameraResolution;
/// Automatically start the scanner on initialization.
final bool autoStart;
/// The detection speed for the scanner.
///
/// Defaults to [DetectionSpeed.normal].
... ... @@ -177,11 +181,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
_disposeListeners();
final TorchState oldTorchState = value.torchState;
// After the camera stopped, set the torch state to off,
// as the torch state callback is never called when the camera is stopped.
// If the device does not have a torch, do not report "off".
value = value.copyWith(
isRunning: false,
torchState: TorchState.off,
torchState: oldTorchState == TorchState.unavailable
? TorchState.unavailable
: TorchState.off,
);
}
... ... @@ -242,12 +251,14 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
}
/// Start scanning for barcodes.
/// Upon calling this method, the necessary camera permission will be requested.
///
/// The [cameraDirection] can be used to specify the camera direction.
/// If this is null, this defaults to the [facing] value.
///
/// Does nothing if the camera is already running.
/// Upon calling this method, the necessary camera permission will be requested.
///
/// If the permission is denied on iOS, MacOS or Web, there is no way to request it again.
Future<void> start({CameraFacing? cameraDirection}) async {
if (_isDisposed) {
throw const MobileScannerException(
... ... @@ -259,6 +270,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
);
}
// Permission was denied, do nothing.
// When the controller is stopped,
// the error is reset so the permission can be requested again if possible.
if (value.error?.errorCode == MobileScannerErrorCode.permissionDenied) {
return;
}
// Do nothing if the camera is already running.
if (value.isRunning) {
return;
... ... @@ -284,16 +302,18 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
options,
);
value = value.copyWith(
availableCameras: viewAttributes.numberOfCameras,
cameraDirection: effectiveDirection,
isInitialized: true,
isRunning: true,
size: viewAttributes.size,
// If the device has a flashlight, let the platform update the torch state.
// If it does not have one, provide the unavailable state directly.
torchState: viewAttributes.hasTorch ? null : TorchState.unavailable,
);
if (!_isDisposed) {
value = value.copyWith(
availableCameras: viewAttributes.numberOfCameras,
cameraDirection: effectiveDirection,
isInitialized: true,
isRunning: true,
size: viewAttributes.size,
// Provide the current torch state.
// Updates are provided by the `torchStateStream`.
torchState: viewAttributes.currentTorchMode,
);
}
} on MobileScannerException catch (error) {
// The initialization finished with an error.
// To avoid stale values, reset the output size,
... ... @@ -365,6 +385,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
///
/// Does nothing if the device has no torch,
/// or if the camera is not running.
///
/// If the current torch state is [TorchState.auto],
/// the torch is turned on or off depending on its actual current state.
Future<void> toggleTorch() async {
_throwIfNotInitialized();
... ... @@ -378,13 +401,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
return;
}
final TorchState newState =
torchState == TorchState.off ? TorchState.on : TorchState.off;
// Update the torch state to the new state.
// Request the torch state to be switched to the opposite state.
// When the platform has updated the torch state,
// it will send an update through the torch state event stream.
await MobileScannerPlatform.instance.setTorchState(newState);
await MobileScannerPlatform.instance.toggleTorch();
}
/// Update the scan window with the given [window] rectangle.
... ...
... ... @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface {
/// This is only supported on the web.
void setBarcodeLibraryScriptUrl(String scriptUrl) {}
/// Set the torch state of the active camera.
Future<void> setTorchState(TorchState torchState) {
throw UnimplementedError('setTorchState() has not been implemented.');
}
/// Set the zoom scale of the camera.
///
/// The [zoomScale] must be between `0.0` and `1.0` (both inclusive).
... ... @@ -100,6 +95,10 @@ abstract class MobileScannerPlatform extends PlatformInterface {
throw UnimplementedError('pause() has not been implemented.');
}
/// Toggle the torch on the active camera on or off.
Future<void> toggleTorch() {
throw UnimplementedError('toggleTorch() has not been implemented.');
}
/// Update the scan window to the given [window] rectangle.
///
... ...
import 'dart:ui';
import 'package:mobile_scanner/src/enums/torch_state.dart';
/// This class defines the attributes for the mobile scanner view.
class MobileScannerViewAttributes {
const MobileScannerViewAttributes({
required this.hasTorch,
required this.currentTorchMode,
this.numberOfCameras,
required this.size,
});
/// Whether the current active camera has a torch.
final bool hasTorch;
/// The current torch state of the active camera.
final TorchState currentTorchMode;
/// The number of available cameras.
final int? numberOfCameras;
... ...
... ... @@ -38,7 +38,7 @@ class MobileScannerState {
/// The facing direction of the camera.
final CameraFacing cameraDirection;
/// The error that occurred while setting up or using the canera.
/// The error that occurred while setting up or using the camera.
final MobileScannerException? error;
/// Whether the mobile scanner has initialized successfully.
... ...
... ... @@ -62,20 +62,19 @@ abstract class BarcodeReader {
final Completer<void> completer = Completer();
final HTMLScriptElement script =
(document.createElement('script') as HTMLScriptElement)
..id = scriptId
..async = true
..defer = false
..type = 'application/javascript'
..lang = 'javascript'
..crossOrigin = 'anonymous'
..src = alternateScriptUrl ?? scriptUrl
..onload = (JSAny _) {
if (!completer.isCompleted) {
completer.complete();
}
}.toJS;
final HTMLScriptElement script = HTMLScriptElement()
..id = scriptId
..async = true
..defer = false
..type = 'application/javascript'
..lang = 'javascript'
..crossOrigin = 'anonymous'
..src = alternateScriptUrl ?? scriptUrl
..onload = (JSAny _) {
if (!completer.isCompleted) {
completer.complete();
}
}.toJS;
script.onerror = (JSAny _) {
if (!completer.isCompleted) {
... ...
... ... @@ -9,12 +9,10 @@ import 'dart:js_interop';
///
/// Object literals can be made using [jsify].
@JS('Map')
@staticInterop
class JSMap<K extends JSAny, V extends JSAny> {
extension type JSMap<K extends JSAny, V extends JSAny>._(JSObject _)
implements JSObject {
external factory JSMap();
}
extension JSMapExtension<K extends JSAny, V extends JSAny> on JSMap<K, V> {
external V? get(K key);
external JSVoid set(K key, V? value);
}
... ...
import 'dart:js_interop';
import 'package:mobile_scanner/src/web/media_track_extension.dart';
import 'package:web/web.dart';
/// This class represents a delegate that manages the constraints for a [MediaStreamTrack].
... ... @@ -9,25 +10,36 @@ final class MediaTrackConstraintsDelegate {
/// Get the settings for the given [mediaStream].
MediaTrackSettings? getSettings(MediaStream? mediaStream) {
final List<JSAny?>? tracks = mediaStream?.getVideoTracks().toDart;
final List<MediaStreamTrack>? tracks = mediaStream?.getVideoTracks().toDart;
if (tracks == null || tracks.isEmpty) {
return null;
}
final MediaStreamTrack? track = tracks.first as MediaStreamTrack?;
final MediaStreamTrack track = tracks.first;
if (track == null) {
return null;
final MediaTrackCapabilities capabilities;
if (track.getCapabilitiesNullable != null) {
capabilities = track.getCapabilities();
} else {
capabilities = MediaTrackCapabilities();
}
final MediaTrackSettings settings = track.getSettings();
final JSArray<JSString>? facingModes = capabilities.facingModeNullable;
if (facingModes == null || facingModes.toDart.isEmpty) {
return MediaTrackSettings(
width: settings.width,
height: settings.height,
);
}
return MediaTrackSettings(
width: settings.width,
height: settings.height,
facingMode: settings.facingMode,
aspectRatio: settings.aspectRatio,
);
}
}
... ...
import 'dart:js_interop';
import 'package:web/web.dart';
/// This extension provides nullable properties for [MediaStreamTrack],
/// for cases where the properties are not supported by all browsers.
extension NullableMediaStreamTrackCapabilities on MediaStreamTrack {
/// The `getCapabilities` function is not supported on Firefox.
@JS('getCapabilities')
external JSFunction? get getCapabilitiesNullable;
}
/// This extension provides nullable properties for [MediaTrackCapabilities],
/// for cases where the properties are not supported by all browsers.
extension NullableMediaTrackCapabilities on MediaTrackCapabilities {
/// The `facingMode` property is not supported on Safari.
@JS('facingMode')
external JSArray<JSString>? get facingModeNullable;
}
... ...
... ... @@ -15,6 +15,7 @@ import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/start_options.dart';
import 'package:mobile_scanner/src/web/barcode_reader.dart';
import 'package:mobile_scanner/src/web/media_track_extension.dart';
import 'package:mobile_scanner/src/web/zxing/zxing_barcode_reader.dart';
import 'package:web/web.dart';
... ... @@ -27,7 +28,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
String? _alternateScriptUrl;
/// The internal barcode reader.
final BarcodeReader _barcodeReader = ZXingBarcodeReader();
BarcodeReader? _barcodeReader;
/// The stream controller for the barcode stream.
final StreamController<BarcodeCapture> _barcodesController =
... ... @@ -37,21 +38,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
StreamSubscription<Object?>? _barcodesSubscription;
/// The container div element for the camera view.
///
/// This container element is used by the barcode reader.
HTMLDivElement? _divElement;
late HTMLDivElement _divElement;
/// This [Completer] is used to prevent additional calls to the [start] method.
///
/// To handle lifecycle changes properly,
/// the scanner is stopped when the application is inactive,
/// and restarted when the application gains focus.
/// The flag that keeps track of whether a permission request is in progress.
///
/// However, when the camera permission is requested,
/// the application is put in the inactive state due to the permission popup gaining focus.
/// Thus, as long as the permission status is not known,
/// any calls to the [start] method are ignored.
Completer<void>? _cameraPermissionCompleter;
/// On the web, a permission request triggers a dialog, that in turn triggers a lifecycle change.
/// While the permission request is in progress, any attempts at (re)starting the camera should be ignored.
bool _permissionRequestInProgress = false;
/// The stream controller for the media track settings stream.
///
... ... @@ -62,18 +55,19 @@ class MobileScannerWeb extends MobileScannerPlatform {
final StreamController<MediaTrackSettings> _settingsController =
StreamController.broadcast();
/// The view type for the platform view factory.
static const String _viewType = 'MobileScannerWeb';
/// The texture ID for the camera view.
int _textureId = 1;
/// The video element for the camera view.
late HTMLVideoElement _videoElement;
/// Get the view type for the platform view factory.
String _getViewType(int textureId) => 'mobile-scanner-view-$textureId';
static void registerWith(Registrar registrar) {
MobileScannerPlatform.instance = MobileScannerWeb();
}
bool get _hasPendingPermissionRequest {
return _cameraPermissionCompleter != null &&
!_cameraPermissionCompleter!.isCompleted;
}
@override
Stream<BarcodeCapture?> get barcodesStream => _barcodesController.stream;
... ... @@ -85,6 +79,33 @@ class MobileScannerWeb extends MobileScannerPlatform {
Stream<double> get zoomScaleStateStream =>
_settingsController.stream.map((_) => 1.0);
/// Create the [HTMLVideoElement] along with its parent container [HTMLDivElement].
HTMLVideoElement _createVideoElement(int textureId) {
final HTMLVideoElement videoElement = HTMLVideoElement();
videoElement.style
..height = '100%'
..width = '100%'
..objectFit = 'cover'
..transformOrigin = 'center'
..pointerEvents = 'none';
// Attach the video element to its parent container
// and setup the PlatformView factory for this `textureId`.
_divElement = HTMLDivElement()
..style.objectFit = 'cover'
..style.height = '100%'
..style.width = '100%'
..append(videoElement);
ui_web.platformViewRegistry.registerViewFactory(
_getViewType(textureId),
(_) => _divElement,
);
return videoElement;
}
void _handleMediaTrackSettingsChange(MediaTrackSettings settings) {
if (_settingsController.isClosed) {
return;
... ... @@ -93,6 +114,40 @@ class MobileScannerWeb extends MobileScannerPlatform {
_settingsController.add(settings);
}
/// Flip the [videoElement] horizontally,
/// if the [videoStream] indicates that is facing the user.
void _maybeFlipVideoPreview(
HTMLVideoElement videoElement,
MediaStream videoStream,
) {
final List<MediaStreamTrack> tracks = videoStream.getVideoTracks().toDart;
if (tracks.isEmpty) {
return;
}
final MediaStreamTrack videoTrack = tracks.first;
final MediaTrackCapabilities capabilities;
if (videoTrack.getCapabilitiesNullable != null) {
capabilities = videoTrack.getCapabilities();
} else {
capabilities = MediaTrackCapabilities();
}
final JSArray<JSString>? facingModes = capabilities.facingModeNullable;
// TODO: this is an empty array on MacOS Chrome, where there is no facing mode, but one, user facing camera.
// Facing mode is not supported by this track, do nothing.
if (facingModes == null || facingModes.toDart.isEmpty) {
return;
}
if (videoTrack.getSettings().facingMode == 'user') {
videoElement.style.transform = 'scaleX(-1)';
}
}
/// Prepare a [MediaStream] for the video output.
///
/// This method requests permission to use the camera.
... ... @@ -102,7 +157,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
Future<MediaStream> _prepareVideoStream(
CameraFacing cameraDirection,
) async {
if ((window.navigator.mediaDevices as JSAny?).isUndefinedOrNull) {
if (window.navigator.mediaDevices.isUndefinedOrNull) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.unsupported,
errorDetails: MobileScannerErrorDetails(
... ... @@ -117,7 +172,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
final MediaStreamConstraints constraints;
if ((capabilities as JSAny).isUndefinedOrNull || !capabilities.facingMode) {
if (capabilities.isUndefinedOrNull || !capabilities.facingMode) {
constraints = MediaStreamConstraints(video: true.toJS);
} else {
final String facingMode = switch (cameraDirection) {
... ... @@ -126,43 +181,24 @@ class MobileScannerWeb extends MobileScannerPlatform {
};
constraints = MediaStreamConstraints(
video: MediaTrackConstraintSet(facingMode: facingMode.toJS) as JSAny,
video: MediaTrackConstraintSet(
facingMode: facingMode.toJS,
),
);
}
try {
// Retrieving the video track requests the camera permission.
// If the completer is not null, the permission was never requested before.
_cameraPermissionCompleter ??= Completer<void>();
_permissionRequestInProgress = true;
final MediaStream? videoStream = await window.navigator.mediaDevices
.getUserMedia(constraints)
.toDart as MediaStream?;
// At this point the permission is granted.
if (!_cameraPermissionCompleter!.isCompleted) {
_cameraPermissionCompleter!.complete();
}
// Retrieving the media devices requests the camera permission.
final MediaStream videoStream =
await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
if (videoStream == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message:
'Could not create a video stream from the camera with the given options. '
'The browser might not support the given constraints.',
),
);
}
_permissionRequestInProgress = false;
return videoStream;
} on DOMException catch (error, stackTrace) {
// At this point the permission request completed, although with an error,
// but the error is irrelevant for the completer.
if (!_cameraPermissionCompleter!.isCompleted) {
_cameraPermissionCompleter!.complete();
}
_permissionRequestInProgress = false;
final String errorMessage = error.toString();
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
... ... @@ -192,11 +228,11 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Widget buildCameraView() {
if (!_barcodeReader.isScanning) {
return const SizedBox();
if (_barcodeReader?.isScanning ?? false) {
return HtmlElementView(viewType: _getViewType(_textureId));
}
return const HtmlElementView(viewType: _viewType);
return const SizedBox();
}
@override
... ... @@ -213,14 +249,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
@override
Future<void> setTorchState(TorchState torchState) {
throw UnsupportedError(
'Setting the torch state is not supported for video tracks on the web.\n'
'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks',
);
}
@override
Future<void> setZoomScale(double zoomScale) {
throw UnsupportedError(
'Setting the zoom scale is not supported for video tracks on the web.\n'
... ... @@ -233,7 +261,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
// If the permission request has not yet completed,
// the camera view is not ready yet.
// Prevent the permission popup from triggering a restart of the scanner.
if (_hasPendingPermissionRequest) {
if (_permissionRequestInProgress) {
throw PermissionRequestPendingException();
}
... ... @@ -242,23 +270,13 @@ class MobileScannerWeb extends MobileScannerPlatform {
await stop();
}
await _barcodeReader.maybeLoadLibrary(
_barcodeReader = ZXingBarcodeReader();
await _barcodeReader?.maybeLoadLibrary(
alternateScriptUrl: _alternateScriptUrl,
);
// Setup the view factory & container element.
if (_divElement == null) {
_divElement = (document.createElement('div') as HTMLDivElement)
..style.width = '100%'
..style.height = '100%';
ui_web.platformViewRegistry.registerViewFactory(
_viewType,
(int id) => _divElement!,
);
}
if (_barcodeReader.isScanning) {
if (_barcodeReader?.isScanning ?? false) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
... ... @@ -280,25 +298,19 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
// Listen for changes to the media track settings.
_barcodeReader.setMediaTrackSettingsListener(
_barcodeReader?.setMediaTrackSettingsListener(
_handleMediaTrackSettingsChange,
);
final HTMLVideoElement videoElement;
_textureId += 1; // Request a new texture.
// Attach the video element to the DOM, through its parent container.
// If a video element is already present, reuse it.
if (_divElement!.children.length == 0) {
videoElement = document.createElement('video') as HTMLVideoElement;
_videoElement = _createVideoElement(_textureId);
_divElement!.appendChild(videoElement);
} else {
videoElement = _divElement!.children.item(0)! as HTMLVideoElement;
}
_maybeFlipVideoPreview(_videoElement, videoStream);
await _barcodeReader.start(
await _barcodeReader?.start(
startOptions,
videoElement: videoElement,
videoElement: _videoElement,
videoStream: videoStream,
);
} catch (error, stackTrace) {
... ... @@ -312,7 +324,7 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
try {
_barcodesSubscription = _barcodeReader.detectBarcodes().listen(
_barcodesSubscription = _barcodeReader?.detectBarcodes().listen(
(BarcodeCapture barcode) {
if (_barcodesController.isClosed) {
return;
... ... @@ -322,15 +334,17 @@ class MobileScannerWeb extends MobileScannerPlatform {
},
);
final bool hasTorch = await _barcodeReader.hasTorch();
final bool hasTorch = await _barcodeReader?.hasTorch() ?? false;
if (hasTorch && startOptions.torchEnabled) {
await _barcodeReader.setTorchState(TorchState.on);
await _barcodeReader?.setTorchState(TorchState.on);
}
return MobileScannerViewAttributes(
hasTorch: hasTorch,
size: _barcodeReader.videoSize,
// The torch of a media stream is not available for video tracks.
// See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks
currentTorchMode: TorchState.unavailable,
size: _barcodeReader?.videoSize ?? Size.zero,
);
} catch (error, stackTrace) {
throw MobileScannerException(
... ... @@ -352,15 +366,20 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Future<void> stop() async {
if (_barcodesController.isClosed) {
return;
}
// Ensure the barcode scanner is stopped, by cancelling the subscription.
await _barcodesSubscription?.cancel();
_barcodesSubscription = null;
await _barcodeReader.stop();
await _barcodeReader?.stop();
_barcodeReader = null;
}
@override
Future<void> toggleTorch() {
throw UnsupportedError(
'Setting the torch state is not supported for video tracks on the web.\n'
'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks',
);
}
@override
... ... @@ -372,31 +391,8 @@ class MobileScannerWeb extends MobileScannerPlatform {
@override
Future<void> dispose() async {
if (_barcodesController.isClosed) {
return;
}
// The `_barcodesController` and `_settingsController`
// are not closed, as these have the same lifetime as the plugin.
await stop();
await _barcodesController.close();
await _settingsController.close();
// Finally, remove the video element from the DOM.
try {
final HTMLCollection? divChildren = _divElement?.children;
// Since the exact element is unknown, remove all children.
// In practice, there should only be one child, the single video element.
if (divChildren != null && divChildren.length > 0) {
for (int i = 0; i < divChildren.length; i++) {
final Node? child = divChildren.item(i);
if (child != null) {
_divElement?.removeChild(child);
}
}
}
} catch (_) {
// The video element was no longer a child of the container element.
}
}
}
... ...
... ... @@ -11,96 +11,68 @@ import 'package:mobile_scanner/src/web/zxing/result_point.dart';
///
/// See also: https://github.com/zxing-js/library/blob/master/src/core/Result.ts
@JS()
@anonymous
@staticInterop
abstract class Result {}
extension ResultExt on Result {
extension type Result(JSObject _) implements JSObject {
@JS('barcodeFormat')
external JSNumber? get _barcodeFormat;
external int? get _barcodeFormat;
@JS('text')
external JSString? get _text;
/// Get the text of the result.
external String? get text;
@JS('rawBytes')
external JSUint8Array? get _rawBytes;
@JS('resultPoints')
external JSArray? get _resultPoints;
external JSArray<ResultPoint>? get _resultPoints;
@JS('timestamp')
external JSNumber? get _timestamp;
/// Get the timestamp of the result.
external int? get timestamp;
/// Get the barcode format of the result.
///
/// See also https://github.com/zxing-js/library/blob/master/src/core/BarcodeFormat.ts
BarcodeFormat get barcodeFormat {
switch (_barcodeFormat?.toDartInt) {
case 0:
return BarcodeFormat.aztec;
case 1:
return BarcodeFormat.codabar;
case 2:
return BarcodeFormat.code39;
case 3:
return BarcodeFormat.code93;
case 4:
return BarcodeFormat.code128;
case 5:
return BarcodeFormat.dataMatrix;
case 6:
return BarcodeFormat.ean8;
case 7:
return BarcodeFormat.ean13;
case 8:
return BarcodeFormat.itf;
case 9:
// Maxicode
return BarcodeFormat.unknown;
case 10:
return BarcodeFormat.pdf417;
case 11:
return BarcodeFormat.qrCode;
case 12:
// RSS 14
return BarcodeFormat.unknown;
case 13:
// RSS EXPANDED
return BarcodeFormat.unknown;
case 14:
return BarcodeFormat.upcA;
case 15:
return BarcodeFormat.upcE;
case 16:
// UPC/EAN extension
return BarcodeFormat.unknown;
default:
return BarcodeFormat.unknown;
}
return switch (_barcodeFormat) {
0 => BarcodeFormat.aztec,
1 => BarcodeFormat.codabar,
2 => BarcodeFormat.code39,
3 => BarcodeFormat.code93,
4 => BarcodeFormat.code128,
5 => BarcodeFormat.dataMatrix,
6 => BarcodeFormat.ean8,
7 => BarcodeFormat.ean13,
8 => BarcodeFormat.itf,
// Maxicode
9 => BarcodeFormat.unknown,
10 => BarcodeFormat.pdf417,
11 => BarcodeFormat.qrCode,
// RSS 14
12 => BarcodeFormat.unknown,
// RSS EXPANDED
13 => BarcodeFormat.unknown,
14 => BarcodeFormat.upcA,
15 => BarcodeFormat.upcE,
// UPC/EAN extension
16 => BarcodeFormat.unknown,
_ => BarcodeFormat.unknown
};
}
/// Get the raw bytes of the result.
Uint8List? get rawBytes => _rawBytes?.toDart;
/// Get the corner points of the result.
List<Offset> get resultPoints {
final JSArray? points = _resultPoints;
final JSArray<ResultPoint>? points = _resultPoints;
if (points == null) {
return [];
return const [];
}
return points.toDart.cast<ResultPoint>().map((point) {
return points.toDart.map((point) {
return Offset(point.x, point.y);
}).toList();
}
/// Get the raw bytes of the result.
Uint8List? get rawBytes => _rawBytes?.toDart;
/// Get the text of the result.
String? get text => _text?.toDart;
/// Get the timestamp of the result.
int? get timestamp => _timestamp?.toDartInt;
/// Convert this result to a [Barcode].
Barcode get toBarcode {
return Barcode(
... ...
... ... @@ -4,20 +4,10 @@ import 'dart:js_interop';
///
/// See also: https://github.com/zxing-js/library/blob/master/src/core/ResultPoint.ts
@JS()
@anonymous
@staticInterop
abstract class ResultPoint {}
extension ResultPointExt on ResultPoint {
@JS('x')
external JSNumber get _x;
@JS('y')
external JSNumber get _y;
extension type ResultPoint(JSObject _) implements JSObject {
/// The x coordinate of the point.
double get x => _x.toDartDouble;
external double get x;
/// The y coordinate of the point.
double get y => _y.toDartDouble;
external double get y;
}
... ...
... ... @@ -12,8 +12,6 @@ import 'package:mobile_scanner/src/web/zxing/result.dart';
import 'package:mobile_scanner/src/web/zxing/zxing_browser_multi_format_reader.dart';
import 'package:web/web.dart' as web;
// TODO: remove the JSAny casts once upgraded to a package:web version that restores "implements JSAny"
/// A barcode reader implementation that uses the ZXing library.
final class ZXingBarcodeReader extends BarcodeReader {
ZXingBarcodeReader();
... ... @@ -48,42 +46,6 @@ final class ZXingBarcodeReader extends BarcodeReader {
@override
String get scriptUrl => 'https://unpkg.com/@zxing/library@0.19.1';
/// Get the barcode format from the ZXing library, for the given [format].
static int getZXingBarcodeFormat(BarcodeFormat format) {
switch (format) {
case BarcodeFormat.aztec:
return 0;
case BarcodeFormat.codabar:
return 1;
case BarcodeFormat.code39:
return 2;
case BarcodeFormat.code93:
return 3;
case BarcodeFormat.code128:
return 4;
case BarcodeFormat.dataMatrix:
return 5;
case BarcodeFormat.ean8:
return 6;
case BarcodeFormat.ean13:
return 7;
case BarcodeFormat.itf:
return 8;
case BarcodeFormat.pdf417:
return 10;
case BarcodeFormat.qrCode:
return 11;
case BarcodeFormat.upcA:
return 14;
case BarcodeFormat.upcE:
return 15;
case BarcodeFormat.unknown:
case BarcodeFormat.all:
default:
return -1;
}
}
JSMap? _createReaderHints(List<BarcodeFormat> formats) {
if (formats.isEmpty || formats.contains(BarcodeFormat.all)) {
return null;
... ... @@ -96,8 +58,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
hints.set(
2.toJS,
[
for (final BarcodeFormat format in formats)
getZXingBarcodeFormat(format).toJS,
for (final BarcodeFormat format in formats) format.toJS,
].toJS,
);
... ... @@ -114,9 +75,9 @@ final class ZXingBarcodeReader extends BarcodeReader {
web.MediaStream videoStream,
) async {
final JSPromise? result = _reader?.attachStreamToVideo.callAsFunction(
_reader as JSAny?,
videoStream as JSAny,
videoElement as JSAny,
_reader,
videoStream,
videoElement,
) as JSPromise?;
await result?.toDart;
... ... @@ -135,8 +96,8 @@ final class ZXingBarcodeReader extends BarcodeReader {
controller.onListen = () {
_reader?.decodeContinuously.callAsFunction(
_reader as JSAny?,
_reader?.videoElement as JSAny?,
_reader,
_reader?.videoElement,
(Result? result, JSAny? error) {
if (controller.isClosed || result == null) {
return;
... ... @@ -155,8 +116,8 @@ final class ZXingBarcodeReader extends BarcodeReader {
// when the stream subscription returned by this method is cancelled in `MobileScannerWeb.stop()`.
// This avoids both leaving the barcode scanner running and a memory leak for the stream subscription.
controller.onCancel = () async {
_reader?.stopContinuousDecode.callAsFunction(_reader as JSAny?);
_reader?.reset.callAsFunction(_reader as JSAny?);
_reader?.stopContinuousDecode.callAsFunction(_reader);
_reader?.reset.callAsFunction(_reader);
await controller.close();
};
... ... @@ -185,7 +146,7 @@ final class ZXingBarcodeReader extends BarcodeReader {
_reader = ZXingBrowserMultiFormatReader(
_createReaderHints(formats),
detectionTimeoutMs.toJS,
detectionTimeoutMs,
);
await _prepareVideoElement(videoElement, videoStream);
... ... @@ -199,8 +160,32 @@ final class ZXingBarcodeReader extends BarcodeReader {
@override
Future<void> stop() async {
_onMediaTrackSettingsChanged = null;
_reader?.stopContinuousDecode.callAsFunction(_reader as JSAny?);
_reader?.reset.callAsFunction(_reader as JSAny?);
_reader?.stopContinuousDecode.callAsFunction(_reader);
_reader?.reset.callAsFunction(_reader);
_reader = null;
}
}
extension on BarcodeFormat {
/// Get the barcode format from the ZXing library.
JSNumber get toJS {
final int zxingFormat = switch (this) {
BarcodeFormat.aztec => 0,
BarcodeFormat.codabar => 1,
BarcodeFormat.code39 => 2,
BarcodeFormat.code93 => 3,
BarcodeFormat.code128 => 4,
BarcodeFormat.dataMatrix => 5,
BarcodeFormat.ean8 => 6,
BarcodeFormat.ean13 => 7,
BarcodeFormat.itf => 8,
BarcodeFormat.pdf417 => 10,
BarcodeFormat.qrCode => 11,
BarcodeFormat.upcA => 14,
BarcodeFormat.upcE => 15,
BarcodeFormat.unknown || BarcodeFormat.all || _ => -1,
};
return zxingFormat.toJS;
}
}
... ...
... ... @@ -7,8 +7,7 @@ import 'package:web/web.dart';
///
/// See https://github.com/zxing-js/library/blob/master/src/browser/BrowserMultiFormatReader.ts
@JS('ZXing.BrowserMultiFormatReader')
@staticInterop
class ZXingBrowserMultiFormatReader {
extension type ZXingBrowserMultiFormatReader._(JSObject _) implements JSObject {
/// Construct a new `ZXing.BrowserMultiFormatReader`.
///
/// The [hints] are the configuration options for the reader.
... ... @@ -17,11 +16,9 @@ class ZXingBrowserMultiFormatReader {
/// See also: https://github.com/zxing-js/library/blob/master/src/core/DecodeHintType.ts
external factory ZXingBrowserMultiFormatReader(
JSMap? hints,
JSNumber? timeBetweenScansMillis,
int timeBetweenScansMillis,
);
}
extension ZXingBrowserMultiFormatReaderExt on ZXingBrowserMultiFormatReader {
/// Attach a [MediaStream] to a [HTMLVideoElement].
///
/// This function accepts a [MediaStream] and a [HTMLVideoElement] as arguments,
... ...
... ... @@ -67,8 +67,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
requestPermission(call, result)
case "start":
start(call, result)
case "torch":
toggleTorch(call, result)
case "toggleTorch":
toggleTorch(result)
case "setScale":
setScale(call, result)
case "resetScale":
... ... @@ -298,12 +298,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
// Turn on the torch if requested.
if (torch) {
do {
try self.toggleTorchInternal(.on)
} catch {
// If the torch could not be turned on,
// continue the capture session.
}
self.turnTorchOn()
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
... ... @@ -336,17 +331,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
captureSession!.startRunning()
let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)]
let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]
let answer: [String : Any?] = [
"textureId": textureId,
"size": size,
"currentTorchState": device.hasTorch ? device.torchMode.rawValue : -1,
]
result(answer)
}
// TODO: this method should be removed when iOS and MacOS share their implementation.
private func toggleTorchInternal(_ torch: AVCaptureDevice.TorchMode) throws {
private func toggleTorchInternal() {
guard let device = self.device else {
return
}
if (!device.hasTorch || !device.isTorchModeSupported(torch)) {
if (!device.hasTorch) {
return
}
... ... @@ -355,12 +355,57 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
return
}
}
var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode
switch(device.torchMode) {
case AVCaptureDevice.TorchMode.auto:
if #available(macOS 10.15, *) {
newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on
}
break;
case AVCaptureDevice.TorchMode.off:
newTorchMode = AVCaptureDevice.TorchMode.on
break;
case AVCaptureDevice.TorchMode.on:
newTorchMode = AVCaptureDevice.TorchMode.off
break;
default:
return;
}
if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) {
return;
}
if (device.torchMode != torch) {
do {
try device.lockForConfiguration()
device.torchMode = torch
device.torchMode = newTorchMode
device.unlockForConfiguration()
} catch(_) {}
}
/// Turn the torch on.
private func turnTorchOn() {
guard let device = self.device else {
return
}
if (!device.hasTorch || !device.isTorchModeSupported(.on) || device.torchMode == .on) {
return
}
if #available(macOS 15.0, *) {
if(!device.isTorchAvailable) {
return
}
}
do {
try device.lockForConfiguration()
device.torchMode = .on
device.unlockForConfiguration()
} catch(_) {}
}
/// Reset the zoom scale.
... ... @@ -375,15 +420,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
result(nil)
}
private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let requestedTorchMode: AVCaptureDevice.TorchMode = call.arguments as! Int == 1 ? .on : .off
do {
try self.toggleTorchInternal(requestedTorchMode)
result(nil)
} catch {
result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil))
}
private func toggleTorch(_ result: @escaping FlutterResult) {
self.toggleTorchInternal()
result(nil)
}
// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
... ... @@ -439,7 +478,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
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
// Off = 0, On = 1, Auto = 2
let state = change?[.newKey] as? Int
let event: [String: Any?] = ["name": "torchState", "data": state]
sink?(event)
... ...
... ... @@ -4,14 +4,14 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '3.5.6'
s.version = '5.1.1'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
DESC
s.homepage = 'http://example.com'
s.homepage = 'https://github.com/juliansteenbakker/mobile_scanner'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.author = { 'Julian Steenbakker' => 'juliansteenbakker@outlook.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
... ...
name: mobile_scanner
description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
version: 5.0.0-beta.2
version: 5.1.1
repository: https://github.com/juliansteenbakker/mobile_scanner
screenshots:
... ... @@ -16,16 +16,16 @@ screenshots:
path: example/screenshots/overlay.png
environment:
sdk: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
plugin_platform_interface: ^2.0.2
web: ^0.4.0
plugin_platform_interface: ^2.0.2
web: ^0.5.1
dev_dependencies:
flutter_test:
... ...
... ... @@ -7,7 +7,8 @@ void main() {
const values = <int, TorchState>{
0: TorchState.off,
1: TorchState.on,
2: TorchState.unavailable,
2: TorchState.auto,
-1: TorchState.unavailable,
};
for (final MapEntry<int, TorchState> entry in values.entries) {
... ... @@ -18,7 +19,7 @@ void main() {
});
test('invalid raw value throws argument error', () {
const int negative = -1;
const int negative = -2;
const int outOfRange = 3;
expect(() => TorchState.fromRawValue(negative), throwsArgumentError);
... ... @@ -27,9 +28,10 @@ void main() {
test('can be converted to raw value', () {
const values = <TorchState, int>{
TorchState.unavailable: -1,
TorchState.off: 0,
TorchState.on: 1,
TorchState.unavailable: 2,
TorchState.auto: 2,
};
for (final MapEntry<TorchState, int> entry in values.entries) {
... ...