Navaron Bracke
Committed by GitHub

Merge pull request #1048 from navaronbracke/fix_ios_torch_mode

fix: provide correct initial torch state
... ... @@ -2,6 +2,10 @@
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
... ...
... ... @@ -103,7 +103,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:
... ...
... ... @@ -19,6 +19,7 @@ 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
... ... @@ -368,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
)
... ... @@ -411,13 +423,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,8 +122,8 @@ class MobileScannerHandler(
}
})
"start" -> start(call, result)
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"toggleTorch" -> toggleTorch(result)
"analyzeImage" -> analyzeImage(call, result)
"setScale" -> setScale(call, result)
"resetScale" -> resetScale(result)
... ... @@ -167,7 +168,7 @@ 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 = DetectionSpeed.entries.first { it.intValue == speed}
mobileScanner!!.start(
barcodeScannerOptions,
... ... @@ -182,7 +183,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
))
}
... ... @@ -243,8 +244,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
... ...
... ... @@ -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;
... ... @@ -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";
... ...
<?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>
... ...
... ... @@ -63,7 +63,9 @@ class _BarcodeScannerWithControllerState
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (!controller.value.isInitialized) {
return;
}
switch (state) {
case AppLifecycleState.detached:
... ...
... ... @@ -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"
... ...
... ... @@ -259,12 +259,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()
}
}
... ... @@ -283,13 +278,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
)
)
... ... @@ -324,30 +318,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
device = 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":
... ... @@ -459,7 +490,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
}
}
... ...
... ... @@ -80,8 +80,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
start(call, result)
case "stop":
stop(result)
case "torch":
toggleTorch(call, result)
case "toggleTorch":
toggleTorch(result)
case "analyzeImage":
analyzeImage(call, result)
case "setScale":
... ... @@ -125,7 +125,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 {
... ... @@ -156,13 +157,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 {
... ...
... ... @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '5.0.0'
s.version = '5.0.2'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
... ...
/// 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.');
}
... ...
... ... @@ -178,15 +178,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);
}
... ... @@ -246,7 +237,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?>?;
... ... @@ -262,7 +255,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
}
return MobileScannerViewAttributes(
hasTorch: hasTorch,
currentTorchMode: currentTorchState,
numberOfCameras: numberOfCameras,
size: size,
);
... ... @@ -280,6 +273,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;
... ...
... ... @@ -281,9 +281,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
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,
// Provide the current torch state.
// Updates are provided by the `torchStateStream`.
torchState: viewAttributes.currentTorchMode,
);
}
} on MobileScannerException catch (error) {
... ... @@ -322,11 +322,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,
);
await MobileScannerPlatform.instance.stop();
... ... @@ -362,6 +367,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();
... ... @@ -375,13 +383,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).
... ... @@ -95,6 +90,11 @@ abstract class MobileScannerPlatform extends PlatformInterface {
throw UnimplementedError('stop() 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.
///
/// Any barcodes that do not intersect with the given [window] will be ignored.
... ...
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;
... ...
... ... @@ -186,17 +186,17 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
try {
// Retrieving the media devices requests the camera permission.
_permissionRequestInProgress = true;
// Retrieving the media devices requests the camera permission.
final MediaStream videoStream =
await window.navigator.mediaDevices.getUserMedia(constraints).toDart;
// At this point the permission is granted.
_permissionRequestInProgress = false;
return videoStream;
} on DOMException catch (error, stackTrace) {
_permissionRequestInProgress = false;
final String errorMessage = error.toString();
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
... ... @@ -209,10 +209,6 @@ class MobileScannerWeb extends MobileScannerPlatform {
errorCode = MobileScannerErrorCode.permissionDenied;
}
// At this point the permission request completed, although with an error,
// but the error is irrelevant.
_permissionRequestInProgress = false;
throw MobileScannerException(
errorCode: errorCode,
errorDetails: MobileScannerErrorDetails(
... ... @@ -251,14 +247,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'
... ... @@ -346,7 +334,9 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
return MobileScannerViewAttributes(
hasTorch: hasTorch,
// 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) {
... ... @@ -371,6 +361,14 @@ class MobileScannerWeb extends MobileScannerPlatform {
}
@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
Future<void> updateScanWindow(Rect? window) {
// A scan window is not supported on the web,
// because the scanner does not expose size information for the barcodes.
... ...
... ... @@ -59,8 +59,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":
... ... @@ -288,12 +288,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)
... ... @@ -326,17 +321,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
}
... ... @@ -345,12 +345,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.
... ... @@ -365,15 +410,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) {
... ... @@ -410,7 +449,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,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '5.0.0'
s.version = '5.0.2'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
... ...
... ... @@ -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) {
... ...