Committed by
GitHub
Merge pull request #1048 from navaronbracke/fix_ios_torch_mode
fix: provide correct initial torch state
Showing
25 changed files
with
253 additions
and
146 deletions
| @@ -2,6 +2,10 @@ | @@ -2,6 +2,10 @@ | ||
| 2 | 2 | ||
| 3 | Bugs fixed: | 3 | Bugs fixed: |
| 4 | * Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !) | 4 | * Fixed a crash when the controller is disposed while it is still starting. [#1036](https://github.com/juliansteenbakker/mobile_scanner/pull/1036) (thanks @EArminjon !) |
| 5 | +* Fixed an issue that causes the initial torch state to be out of sync. | ||
| 6 | + | ||
| 7 | +Improvements: | ||
| 8 | +* Updated the lifeycle code sample to handle not-initialized controllers. | ||
| 5 | 9 | ||
| 6 | ## 5.0.1 | 10 | ## 5.0.1 |
| 7 | 11 |
| @@ -103,7 +103,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { | @@ -103,7 +103,11 @@ class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { | ||
| 103 | 103 | ||
| 104 | @override | 104 | @override |
| 105 | void didChangeAppLifecycleState(AppLifecycleState state) { | 105 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 106 | - super.didChangeAppLifecycleState(state); | 106 | + // If the controller is not ready, do not try to start or stop it. |
| 107 | + // Permission dialogs can trigger lifecycle changes before the controller is ready. | ||
| 108 | + if (!controller.value.isInitialized) { | ||
| 109 | + return; | ||
| 110 | + } | ||
| 107 | 111 | ||
| 108 | switch (state) { | 112 | switch (state) { |
| 109 | case AppLifecycleState.detached: | 113 | case AppLifecycleState.detached: |
| @@ -19,6 +19,7 @@ import androidx.camera.core.ExperimentalGetImage | @@ -19,6 +19,7 @@ import androidx.camera.core.ExperimentalGetImage | ||
| 19 | import androidx.camera.core.ImageAnalysis | 19 | import androidx.camera.core.ImageAnalysis |
| 20 | import androidx.camera.core.ImageProxy | 20 | import androidx.camera.core.ImageProxy |
| 21 | import androidx.camera.core.Preview | 21 | import androidx.camera.core.Preview |
| 22 | +import androidx.camera.core.TorchState | ||
| 22 | import androidx.camera.core.resolutionselector.AspectRatioStrategy | 23 | import androidx.camera.core.resolutionselector.AspectRatioStrategy |
| 23 | import androidx.camera.core.resolutionselector.ResolutionSelector | 24 | import androidx.camera.core.resolutionselector.ResolutionSelector |
| 24 | import androidx.camera.core.resolutionselector.ResolutionStrategy | 25 | import androidx.camera.core.resolutionselector.ResolutionStrategy |
| @@ -368,11 +369,22 @@ class MobileScanner( | @@ -368,11 +369,22 @@ class MobileScanner( | ||
| 368 | val height = resolution.height.toDouble() | 369 | val height = resolution.height.toDouble() |
| 369 | val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0 | 370 | val portrait = (camera?.cameraInfo?.sensorRotationDegrees ?: 0) % 180 == 0 |
| 370 | 371 | ||
| 372 | + // Start with 'unavailable' torch state. | ||
| 373 | + var currentTorchState: Int = -1 | ||
| 374 | + | ||
| 375 | + camera?.cameraInfo?.let { | ||
| 376 | + if (!it.hasFlashUnit()) { | ||
| 377 | + return@let | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + currentTorchState = it.torchState.value ?: -1 | ||
| 381 | + } | ||
| 382 | + | ||
| 371 | mobileScannerStartedCallback( | 383 | mobileScannerStartedCallback( |
| 372 | MobileScannerStartParameters( | 384 | MobileScannerStartParameters( |
| 373 | if (portrait) width else height, | 385 | if (portrait) width else height, |
| 374 | if (portrait) height else width, | 386 | if (portrait) height else width, |
| 375 | - camera?.cameraInfo?.hasFlashUnit() ?: false, | 387 | + currentTorchState, |
| 376 | textureEntry!!.id(), | 388 | textureEntry!!.id(), |
| 377 | numberOfCameras ?: 0 | 389 | numberOfCameras ?: 0 |
| 378 | ) | 390 | ) |
| @@ -411,13 +423,16 @@ class MobileScanner( | @@ -411,13 +423,16 @@ class MobileScanner( | ||
| 411 | /** | 423 | /** |
| 412 | * Toggles the flash light on or off. | 424 | * Toggles the flash light on or off. |
| 413 | */ | 425 | */ |
| 414 | - fun toggleTorch(enableTorch: Boolean) { | ||
| 415 | - if (camera == null) { | ||
| 416 | - return | 426 | + fun toggleTorch() { |
| 427 | + camera?.let { | ||
| 428 | + if (!it.cameraInfo.hasFlashUnit()) { | ||
| 429 | + return@let | ||
| 417 | } | 430 | } |
| 418 | 431 | ||
| 419 | - if (camera?.cameraInfo?.hasFlashUnit() == true) { | ||
| 420 | - camera?.cameraControl?.enableTorch(enableTorch) | 432 | + when(it.cameraInfo.torchState.value) { |
| 433 | + TorchState.OFF -> it.cameraControl.enableTorch(true) | ||
| 434 | + TorchState.ON -> it.cameraControl.enableTorch(false) | ||
| 435 | + } | ||
| 421 | } | 436 | } |
| 422 | } | 437 | } |
| 423 | 438 |
| @@ -74,6 +74,7 @@ class MobileScannerHandler( | @@ -74,6 +74,7 @@ class MobileScannerHandler( | ||
| 74 | private var mobileScanner: MobileScanner? = null | 74 | private var mobileScanner: MobileScanner? = null |
| 75 | 75 | ||
| 76 | private val torchStateCallback: TorchStateCallback = {state: Int -> | 76 | private val torchStateCallback: TorchStateCallback = {state: Int -> |
| 77 | + // Off = 0, On = 1 | ||
| 77 | barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) | 78 | barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) |
| 78 | } | 79 | } |
| 79 | 80 | ||
| @@ -121,8 +122,8 @@ class MobileScannerHandler( | @@ -121,8 +122,8 @@ class MobileScannerHandler( | ||
| 121 | } | 122 | } |
| 122 | }) | 123 | }) |
| 123 | "start" -> start(call, result) | 124 | "start" -> start(call, result) |
| 124 | - "torch" -> toggleTorch(call, result) | ||
| 125 | "stop" -> stop(result) | 125 | "stop" -> stop(result) |
| 126 | + "toggleTorch" -> toggleTorch(result) | ||
| 126 | "analyzeImage" -> analyzeImage(call, result) | 127 | "analyzeImage" -> analyzeImage(call, result) |
| 127 | "setScale" -> setScale(call, result) | 128 | "setScale" -> setScale(call, result) |
| 128 | "resetScale" -> resetScale(result) | 129 | "resetScale" -> resetScale(result) |
| @@ -167,7 +168,7 @@ class MobileScannerHandler( | @@ -167,7 +168,7 @@ class MobileScannerHandler( | ||
| 167 | val position = | 168 | val position = |
| 168 | if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | 169 | if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA |
| 169 | 170 | ||
| 170 | - val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} | 171 | + val detectionSpeed: DetectionSpeed = DetectionSpeed.entries.first { it.intValue == speed} |
| 171 | 172 | ||
| 172 | mobileScanner!!.start( | 173 | mobileScanner!!.start( |
| 173 | barcodeScannerOptions, | 174 | barcodeScannerOptions, |
| @@ -182,7 +183,7 @@ class MobileScannerHandler( | @@ -182,7 +183,7 @@ class MobileScannerHandler( | ||
| 182 | result.success(mapOf( | 183 | result.success(mapOf( |
| 183 | "textureId" to it.id, | 184 | "textureId" to it.id, |
| 184 | "size" to mapOf("width" to it.width, "height" to it.height), | 185 | "size" to mapOf("width" to it.width, "height" to it.height), |
| 185 | - "torchable" to it.hasFlashUnit, | 186 | + "currentTorchState" to it.currentTorchState, |
| 186 | "numberOfCameras" to it.numberOfCameras | 187 | "numberOfCameras" to it.numberOfCameras |
| 187 | )) | 188 | )) |
| 188 | } | 189 | } |
| @@ -243,8 +244,8 @@ class MobileScannerHandler( | @@ -243,8 +244,8 @@ class MobileScannerHandler( | ||
| 243 | mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) | 244 | mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) |
| 244 | } | 245 | } |
| 245 | 246 | ||
| 246 | - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | ||
| 247 | - mobileScanner!!.toggleTorch(call.arguments == 1) | 247 | + private fun toggleTorch(result: MethodChannel.Result) { |
| 248 | + mobileScanner?.toggleTorch() | ||
| 248 | result.success(null) | 249 | result.success(null) |
| 249 | } | 250 | } |
| 250 | 251 |
| @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects | @@ -3,7 +3,7 @@ package dev.steenbakker.mobile_scanner.objects | ||
| 3 | class MobileScannerStartParameters( | 3 | class MobileScannerStartParameters( |
| 4 | val width: Double = 0.0, | 4 | val width: Double = 0.0, |
| 5 | val height: Double, | 5 | val height: Double, |
| 6 | - val hasFlashUnit: Boolean, | 6 | + val currentTorchState: Int, |
| 7 | val id: Long, | 7 | val id: Long, |
| 8 | val numberOfCameras: Int | 8 | val numberOfCameras: Int |
| 9 | ) | 9 | ) |
| @@ -198,6 +198,7 @@ | @@ -198,6 +198,7 @@ | ||
| 198 | 9705A1C41CF9048500538489 /* Embed Frameworks */, | 198 | 9705A1C41CF9048500538489 /* Embed Frameworks */, |
| 199 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, | 199 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, |
| 200 | 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */, | 200 | 3DBCC0215D7BED1D9A756EA3 /* [CP] Embed Pods Frameworks */, |
| 201 | + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */, | ||
| 201 | ); | 202 | ); |
| 202 | buildRules = ( | 203 | buildRules = ( |
| 203 | ); | 204 | ); |
| @@ -215,7 +216,7 @@ | @@ -215,7 +216,7 @@ | ||
| 215 | isa = PBXProject; | 216 | isa = PBXProject; |
| 216 | attributes = { | 217 | attributes = { |
| 217 | BuildIndependentTargetsInParallel = YES; | 218 | BuildIndependentTargetsInParallel = YES; |
| 218 | - LastUpgradeCheck = 1430; | 219 | + LastUpgradeCheck = 1510; |
| 219 | ORGANIZATIONNAME = ""; | 220 | ORGANIZATIONNAME = ""; |
| 220 | TargetAttributes = { | 221 | TargetAttributes = { |
| 221 | 331C8080294A63A400263BE5 = { | 222 | 331C8080294A63A400263BE5 = { |
| @@ -317,6 +318,23 @@ | @@ -317,6 +318,23 @@ | ||
| 317 | shellPath = /bin/sh; | 318 | shellPath = /bin/sh; |
| 318 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; | 319 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; |
| 319 | }; | 320 | }; |
| 321 | + BB0C8EA8DA81A75DE53F052F /* [CP] Copy Pods Resources */ = { | ||
| 322 | + isa = PBXShellScriptBuildPhase; | ||
| 323 | + buildActionMask = 2147483647; | ||
| 324 | + files = ( | ||
| 325 | + ); | ||
| 326 | + inputFileListPaths = ( | ||
| 327 | + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", | ||
| 328 | + ); | ||
| 329 | + name = "[CP] Copy Pods Resources"; | ||
| 330 | + outputFileListPaths = ( | ||
| 331 | + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", | ||
| 332 | + ); | ||
| 333 | + runOnlyForDeploymentPostprocessing = 0; | ||
| 334 | + shellPath = /bin/sh; | ||
| 335 | + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | ||
| 336 | + showEnvVarsInLog = 0; | ||
| 337 | + }; | ||
| 320 | C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = { | 338 | C7DE006A696F551C4E067E41 /* [CP] Check Pods Manifest.lock */ = { |
| 321 | isa = PBXShellScriptBuildPhase; | 339 | isa = PBXShellScriptBuildPhase; |
| 322 | buildActionMask = 2147483647; | 340 | buildActionMask = 2147483647; |
| @@ -495,7 +513,7 @@ | @@ -495,7 +513,7 @@ | ||
| 495 | CURRENT_PROJECT_VERSION = 1; | 513 | CURRENT_PROJECT_VERSION = 1; |
| 496 | GENERATE_INFOPLIST_FILE = YES; | 514 | GENERATE_INFOPLIST_FILE = YES; |
| 497 | MARKETING_VERSION = 1.0; | 515 | MARKETING_VERSION = 1.0; |
| 498 | - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; | 516 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; |
| 499 | PRODUCT_NAME = "$(TARGET_NAME)"; | 517 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 500 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; | 518 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; |
| 501 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | 519 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
| @@ -513,7 +531,7 @@ | @@ -513,7 +531,7 @@ | ||
| 513 | CURRENT_PROJECT_VERSION = 1; | 531 | CURRENT_PROJECT_VERSION = 1; |
| 514 | GENERATE_INFOPLIST_FILE = YES; | 532 | GENERATE_INFOPLIST_FILE = YES; |
| 515 | MARKETING_VERSION = 1.0; | 533 | MARKETING_VERSION = 1.0; |
| 516 | - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; | 534 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; |
| 517 | PRODUCT_NAME = "$(TARGET_NAME)"; | 535 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 518 | SWIFT_VERSION = 5.0; | 536 | SWIFT_VERSION = 5.0; |
| 519 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; | 537 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
| @@ -529,7 +547,7 @@ | @@ -529,7 +547,7 @@ | ||
| 529 | CURRENT_PROJECT_VERSION = 1; | 547 | CURRENT_PROJECT_VERSION = 1; |
| 530 | GENERATE_INFOPLIST_FILE = YES; | 548 | GENERATE_INFOPLIST_FILE = YES; |
| 531 | MARKETING_VERSION = 1.0; | 549 | MARKETING_VERSION = 1.0; |
| 532 | - PRODUCT_BUNDLE_IDENTIFIER = com.example.mobile-scanner.RunnerTests; | 550 | + PRODUCT_BUNDLE_IDENTIFIER = "com.example.mobile-scanner.RunnerTests"; |
| 533 | PRODUCT_NAME = "$(TARGET_NAME)"; | 551 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 534 | SWIFT_VERSION = 5.0; | 552 | SWIFT_VERSION = 5.0; |
| 535 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; | 553 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
| @@ -2,6 +2,8 @@ | @@ -2,6 +2,8 @@ | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 3 | <plist version="1.0"> | 3 | <plist version="1.0"> |
| 4 | <dict> | 4 | <dict> |
| 5 | + <key>CADisableMinimumFrameDurationOnPhone</key> | ||
| 6 | + <true/> | ||
| 5 | <key>CFBundleDevelopmentRegion</key> | 7 | <key>CFBundleDevelopmentRegion</key> |
| 6 | <string>$(DEVELOPMENT_LANGUAGE)</string> | 8 | <string>$(DEVELOPMENT_LANGUAGE)</string> |
| 7 | <key>CFBundleDisplayName</key> | 9 | <key>CFBundleDisplayName</key> |
| @@ -28,6 +30,8 @@ | @@ -28,6 +30,8 @@ | ||
| 28 | <string>This app needs camera access to scan QR codes</string> | 30 | <string>This app needs camera access to scan QR codes</string> |
| 29 | <key>NSPhotoLibraryUsageDescription</key> | 31 | <key>NSPhotoLibraryUsageDescription</key> |
| 30 | <string>This app needs photos access to get QR code from photo library</string> | 32 | <string>This app needs photos access to get QR code from photo library</string> |
| 33 | + <key>UIApplicationSupportsIndirectInputEvents</key> | ||
| 34 | + <true/> | ||
| 31 | <key>UILaunchStoryboardName</key> | 35 | <key>UILaunchStoryboardName</key> |
| 32 | <string>LaunchScreen</string> | 36 | <string>LaunchScreen</string> |
| 33 | <key>UIMainStoryboardFile</key> | 37 | <key>UIMainStoryboardFile</key> |
| @@ -47,9 +51,5 @@ | @@ -47,9 +51,5 @@ | ||
| 47 | </array> | 51 | </array> |
| 48 | <key>UIViewControllerBasedStatusBarAppearance</key> | 52 | <key>UIViewControllerBasedStatusBarAppearance</key> |
| 49 | <false/> | 53 | <false/> |
| 50 | - <key>CADisableMinimumFrameDurationOnPhone</key> | ||
| 51 | - <true/> | ||
| 52 | - <key>UIApplicationSupportsIndirectInputEvents</key> | ||
| 53 | - <true/> | ||
| 54 | </dict> | 54 | </dict> |
| 55 | </plist> | 55 | </plist> |
| @@ -63,7 +63,9 @@ class _BarcodeScannerWithControllerState | @@ -63,7 +63,9 @@ class _BarcodeScannerWithControllerState | ||
| 63 | 63 | ||
| 64 | @override | 64 | @override |
| 65 | void didChangeAppLifecycleState(AppLifecycleState state) { | 65 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 66 | - super.didChangeAppLifecycleState(state); | 66 | + if (!controller.value.isInitialized) { |
| 67 | + return; | ||
| 68 | + } | ||
| 67 | 69 | ||
| 68 | switch (state) { | 70 | switch (state) { |
| 69 | case AppLifecycleState.detached: | 71 | case AppLifecycleState.detached: |
| @@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget { | @@ -138,6 +138,15 @@ class ToggleFlashlightButton extends StatelessWidget { | ||
| 138 | } | 138 | } |
| 139 | 139 | ||
| 140 | switch (state.torchState) { | 140 | switch (state.torchState) { |
| 141 | + case TorchState.auto: | ||
| 142 | + return IconButton( | ||
| 143 | + color: Colors.white, | ||
| 144 | + iconSize: 32.0, | ||
| 145 | + icon: const Icon(Icons.flash_auto), | ||
| 146 | + onPressed: () async { | ||
| 147 | + await controller.toggleTorch(); | ||
| 148 | + }, | ||
| 149 | + ); | ||
| 141 | case TorchState.off: | 150 | case TorchState.off: |
| 142 | return IconButton( | 151 | return IconButton( |
| 143 | color: Colors.white, | 152 | color: Colors.white, |
| @@ -259,7 +259,7 @@ | @@ -259,7 +259,7 @@ | ||
| 259 | isa = PBXProject; | 259 | isa = PBXProject; |
| 260 | attributes = { | 260 | attributes = { |
| 261 | LastSwiftUpdateCheck = 0920; | 261 | LastSwiftUpdateCheck = 0920; |
| 262 | - LastUpgradeCheck = 1430; | 262 | + LastUpgradeCheck = 1510; |
| 263 | ORGANIZATIONNAME = ""; | 263 | ORGANIZATIONNAME = ""; |
| 264 | TargetAttributes = { | 264 | TargetAttributes = { |
| 265 | 331C80D4294CF70F00263BE5 = { | 265 | 331C80D4294CF70F00263BE5 = { |
| @@ -259,12 +259,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -259,12 +259,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 259 | // as they interact with the hardware camera. | 259 | // as they interact with the hardware camera. |
| 260 | if (torch) { | 260 | if (torch) { |
| 261 | DispatchQueue.main.async { | 261 | DispatchQueue.main.async { |
| 262 | - do { | ||
| 263 | - try self.toggleTorch(.on) | ||
| 264 | - } catch { | ||
| 265 | - // If the torch does not turn on, | ||
| 266 | - // continue with the capture session anyway. | ||
| 267 | - } | 262 | + self.turnTorchOn() |
| 268 | } | 263 | } |
| 269 | } | 264 | } |
| 270 | 265 | ||
| @@ -283,13 +278,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -283,13 +278,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 283 | // as this does not change the configuration of the hardware camera. | 278 | // as this does not change the configuration of the hardware camera. |
| 284 | let dimensions = CMVideoFormatDescriptionGetDimensions( | 279 | let dimensions = CMVideoFormatDescriptionGetDimensions( |
| 285 | device.activeFormat.formatDescription) | 280 | device.activeFormat.formatDescription) |
| 286 | - let hasTorch = device.hasTorch | ||
| 287 | 281 | ||
| 288 | completion( | 282 | completion( |
| 289 | MobileScannerStartParameters( | 283 | MobileScannerStartParameters( |
| 290 | width: Double(dimensions.height), | 284 | width: Double(dimensions.height), |
| 291 | height: Double(dimensions.width), | 285 | height: Double(dimensions.width), |
| 292 | - hasTorch: hasTorch, | 286 | + currentTorchState: device.hasTorch ? device.torchMode.rawValue : -1, |
| 293 | textureId: self.textureId ?? 0 | 287 | textureId: self.textureId ?? 0 |
| 294 | ) | 288 | ) |
| 295 | ) | 289 | ) |
| @@ -324,30 +318,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -324,30 +318,67 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 324 | device = nil | 318 | device = nil |
| 325 | } | 319 | } |
| 326 | 320 | ||
| 327 | - /// Set the torch mode. | 321 | + /// Toggle the torch. |
| 328 | /// | 322 | /// |
| 329 | /// This method should be called on the main DispatchQueue. | 323 | /// This method should be called on the main DispatchQueue. |
| 330 | - func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { | 324 | + func toggleTorch() { |
| 331 | guard let device = self.device else { | 325 | guard let device = self.device else { |
| 332 | return | 326 | return |
| 333 | } | 327 | } |
| 334 | 328 | ||
| 335 | - if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(torch)) { | 329 | + if (!device.hasTorch || !device.isTorchAvailable) { |
| 336 | return | 330 | return |
| 337 | } | 331 | } |
| 338 | 332 | ||
| 339 | - if (device.torchMode != torch) { | 333 | + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode |
| 334 | + | ||
| 335 | + switch(device.torchMode) { | ||
| 336 | + case AVCaptureDevice.TorchMode.auto: | ||
| 337 | + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on | ||
| 338 | + break; | ||
| 339 | + case AVCaptureDevice.TorchMode.off: | ||
| 340 | + newTorchMode = AVCaptureDevice.TorchMode.on | ||
| 341 | + break; | ||
| 342 | + case AVCaptureDevice.TorchMode.on: | ||
| 343 | + newTorchMode = AVCaptureDevice.TorchMode.off | ||
| 344 | + break; | ||
| 345 | + default: | ||
| 346 | + return; | ||
| 347 | + } | ||
| 348 | + | ||
| 349 | + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) { | ||
| 350 | + return; | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + do { | ||
| 340 | try device.lockForConfiguration() | 354 | try device.lockForConfiguration() |
| 341 | - device.torchMode = torch | 355 | + device.torchMode = newTorchMode |
| 342 | device.unlockForConfiguration() | 356 | device.unlockForConfiguration() |
| 357 | + } catch(_) {} | ||
| 343 | } | 358 | } |
| 359 | + | ||
| 360 | + /// Turn the torch on. | ||
| 361 | + private func turnTorchOn() { | ||
| 362 | + guard let device = self.device else { | ||
| 363 | + return | ||
| 364 | + } | ||
| 365 | + | ||
| 366 | + if (!device.hasTorch || !device.isTorchAvailable || !device.isTorchModeSupported(.on) || device.torchMode == .on) { | ||
| 367 | + return | ||
| 368 | + } | ||
| 369 | + | ||
| 370 | + do { | ||
| 371 | + try device.lockForConfiguration() | ||
| 372 | + device.torchMode = .on | ||
| 373 | + device.unlockForConfiguration() | ||
| 374 | + } catch(_) {} | ||
| 344 | } | 375 | } |
| 345 | 376 | ||
| 346 | // Observer for torch state | 377 | // Observer for torch state |
| 347 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 378 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 348 | switch keyPath { | 379 | switch keyPath { |
| 349 | case "torchMode": | 380 | case "torchMode": |
| 350 | - // off = 0; on = 1; auto = 2 | 381 | + // Off = 0, On = 1, Auto = 2 |
| 351 | let state = change?[.newKey] as? Int | 382 | let state = change?[.newKey] as? Int |
| 352 | torchModeChangeCallback(state) | 383 | torchModeChangeCallback(state) |
| 353 | case "videoZoomFactor": | 384 | case "videoZoomFactor": |
| @@ -459,7 +490,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | @@ -459,7 +490,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega | ||
| 459 | struct MobileScannerStartParameters { | 490 | struct MobileScannerStartParameters { |
| 460 | var width: Double = 0.0 | 491 | var width: Double = 0.0 |
| 461 | var height: Double = 0.0 | 492 | var height: Double = 0.0 |
| 462 | - var hasTorch = false | 493 | + var currentTorchState: Int = -1 |
| 463 | var textureId: Int64 = 0 | 494 | var textureId: Int64 = 0 |
| 464 | } | 495 | } |
| 465 | } | 496 | } |
| @@ -80,8 +80,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -80,8 +80,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 80 | start(call, result) | 80 | start(call, result) |
| 81 | case "stop": | 81 | case "stop": |
| 82 | stop(result) | 82 | stop(result) |
| 83 | - case "torch": | ||
| 84 | - toggleTorch(call, result) | 83 | + case "toggleTorch": |
| 84 | + toggleTorch(result) | ||
| 85 | case "analyzeImage": | 85 | case "analyzeImage": |
| 86 | analyzeImage(call, result) | 86 | analyzeImage(call, result) |
| 87 | case "setScale": | 87 | case "setScale": |
| @@ -125,7 +125,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -125,7 +125,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 125 | result([ | 125 | result([ |
| 126 | "textureId": parameters.textureId, | 126 | "textureId": parameters.textureId, |
| 127 | "size": ["width": parameters.width, "height": parameters.height], | 127 | "size": ["width": parameters.width, "height": parameters.height], |
| 128 | - "torchable": parameters.hasTorch]) | 128 | + "currentTorchState": parameters.currentTorchState, |
| 129 | + ]) | ||
| 129 | } | 130 | } |
| 130 | } | 131 | } |
| 131 | } catch MobileScannerError.alreadyStarted { | 132 | } catch MobileScannerError.alreadyStarted { |
| @@ -156,13 +157,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -156,13 +157,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 156 | } | 157 | } |
| 157 | 158 | ||
| 158 | /// Toggles the torch. | 159 | /// Toggles the torch. |
| 159 | - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 160 | - do { | ||
| 161 | - try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off) | 160 | + private func toggleTorch(_ result: @escaping FlutterResult) { |
| 161 | + mobileScanner.toggleTorch() | ||
| 162 | result(nil) | 162 | result(nil) |
| 163 | - } catch { | ||
| 164 | - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) | ||
| 165 | - } | ||
| 166 | } | 163 | } |
| 167 | 164 | ||
| 168 | /// Sets the zoomScale. | 165 | /// Sets the zoomScale. |
| @@ -8,31 +8,6 @@ extension CVBuffer { | @@ -8,31 +8,6 @@ extension CVBuffer { | ||
| 8 | let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) | 8 | let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) |
| 9 | return UIImage(cgImage: cgImage!) | 9 | return UIImage(cgImage: cgImage!) |
| 10 | } | 10 | } |
| 11 | - | ||
| 12 | - var image1: UIImage { | ||
| 13 | - // Lock the base address of the pixel buffer | ||
| 14 | - CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly) | ||
| 15 | - // Get the number of bytes per row for the pixel buffer | ||
| 16 | - let baseAddress = CVPixelBufferGetBaseAddress(self) | ||
| 17 | - // Get the number of bytes per row for the pixel buffer | ||
| 18 | - let bytesPerRow = CVPixelBufferGetBytesPerRow(self) | ||
| 19 | - // Get the pixel buffer width and height | ||
| 20 | - let width = CVPixelBufferGetWidth(self) | ||
| 21 | - let height = CVPixelBufferGetHeight(self) | ||
| 22 | - // Create a device-dependent RGB color space | ||
| 23 | - let colorSpace = CGColorSpaceCreateDeviceRGB() | ||
| 24 | - // Create a bitmap graphics context with the sample buffer data | ||
| 25 | - var bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue | ||
| 26 | - bitmapInfo |= CGImageAlphaInfo.premultipliedFirst.rawValue & CGBitmapInfo.alphaInfoMask.rawValue | ||
| 27 | - //let bitmapInfo: UInt32 = CGBitmapInfo.alphaInfoMask.rawValue | ||
| 28 | - let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) | ||
| 29 | - // Create a Quartz image from the pixel data in the bitmap graphics context | ||
| 30 | - let quartzImage = context?.makeImage() | ||
| 31 | - // Unlock the pixel buffer | ||
| 32 | - CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly) | ||
| 33 | - // Create an image object from the Quartz image | ||
| 34 | - return UIImage(cgImage: quartzImage!) | ||
| 35 | - } | ||
| 36 | } | 11 | } |
| 37 | 12 | ||
| 38 | extension UIDeviceOrientation { | 13 | extension UIDeviceOrientation { |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | # | 4 | # |
| 5 | Pod::Spec.new do |s| | 5 | Pod::Spec.new do |s| |
| 6 | s.name = 'mobile_scanner' | 6 | s.name = 'mobile_scanner' |
| 7 | - s.version = '5.0.0' | 7 | + s.version = '5.0.2' |
| 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' | 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' |
| 9 | s.description = <<-DESC | 9 | s.description = <<-DESC |
| 10 | An universal scanner for Flutter based on MLKit. | 10 | An universal scanner for Flutter based on MLKit. |
| 1 | /// The state of the flashlight. | 1 | /// The state of the flashlight. |
| 2 | enum TorchState { | 2 | enum TorchState { |
| 3 | + /// The flashlight turns on automatically in low light conditions. | ||
| 4 | + /// | ||
| 5 | + /// This is currently only supported on iOS and MacOS. | ||
| 6 | + auto(2), | ||
| 7 | + | ||
| 3 | /// The flashlight is off. | 8 | /// The flashlight is off. |
| 4 | off(0), | 9 | off(0), |
| 5 | 10 | ||
| @@ -7,18 +12,20 @@ enum TorchState { | @@ -7,18 +12,20 @@ enum TorchState { | ||
| 7 | on(1), | 12 | on(1), |
| 8 | 13 | ||
| 9 | /// The flashlight is unavailable. | 14 | /// The flashlight is unavailable. |
| 10 | - unavailable(2); | 15 | + unavailable(-1); |
| 11 | 16 | ||
| 12 | const TorchState(this.rawValue); | 17 | const TorchState(this.rawValue); |
| 13 | 18 | ||
| 14 | factory TorchState.fromRawValue(int value) { | 19 | factory TorchState.fromRawValue(int value) { |
| 15 | switch (value) { | 20 | switch (value) { |
| 21 | + case -1: | ||
| 22 | + return TorchState.unavailable; | ||
| 16 | case 0: | 23 | case 0: |
| 17 | return TorchState.off; | 24 | return TorchState.off; |
| 18 | case 1: | 25 | case 1: |
| 19 | return TorchState.on; | 26 | return TorchState.on; |
| 20 | case 2: | 27 | case 2: |
| 21 | - return TorchState.unavailable; | 28 | + return TorchState.auto; |
| 22 | default: | 29 | default: |
| 23 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 30 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); |
| 24 | } | 31 | } |
| @@ -178,15 +178,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -178,15 +178,6 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 178 | } | 178 | } |
| 179 | 179 | ||
| 180 | @override | 180 | @override |
| 181 | - Future<void> setTorchState(TorchState torchState) async { | ||
| 182 | - if (torchState == TorchState.unavailable) { | ||
| 183 | - return; | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - await methodChannel.invokeMethod<void>('torch', torchState.rawValue); | ||
| 187 | - } | ||
| 188 | - | ||
| 189 | - @override | ||
| 190 | Future<void> setZoomScale(double zoomScale) async { | 181 | Future<void> setZoomScale(double zoomScale) async { |
| 191 | await methodChannel.invokeMethod<void>('setScale', zoomScale); | 182 | await methodChannel.invokeMethod<void>('setScale', zoomScale); |
| 192 | } | 183 | } |
| @@ -246,7 +237,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -246,7 +237,9 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 246 | _textureId = textureId; | 237 | _textureId = textureId; |
| 247 | 238 | ||
| 248 | final int? numberOfCameras = startResult['numberOfCameras'] as int?; | 239 | final int? numberOfCameras = startResult['numberOfCameras'] as int?; |
| 249 | - final bool hasTorch = startResult['torchable'] as bool? ?? false; | 240 | + final TorchState currentTorchState = TorchState.fromRawValue( |
| 241 | + startResult['currentTorchState'] as int? ?? -1, | ||
| 242 | + ); | ||
| 250 | 243 | ||
| 251 | final Map<Object?, Object?>? sizeInfo = | 244 | final Map<Object?, Object?>? sizeInfo = |
| 252 | startResult['size'] as Map<Object?, Object?>?; | 245 | startResult['size'] as Map<Object?, Object?>?; |
| @@ -262,7 +255,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -262,7 +255,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 262 | } | 255 | } |
| 263 | 256 | ||
| 264 | return MobileScannerViewAttributes( | 257 | return MobileScannerViewAttributes( |
| 265 | - hasTorch: hasTorch, | 258 | + currentTorchMode: currentTorchState, |
| 266 | numberOfCameras: numberOfCameras, | 259 | numberOfCameras: numberOfCameras, |
| 267 | size: size, | 260 | size: size, |
| 268 | ); | 261 | ); |
| @@ -280,6 +273,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | @@ -280,6 +273,11 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 280 | } | 273 | } |
| 281 | 274 | ||
| 282 | @override | 275 | @override |
| 276 | + Future<void> toggleTorch() async { | ||
| 277 | + await methodChannel.invokeMethod<void>('toggleTorch'); | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + @override | ||
| 283 | Future<void> updateScanWindow(Rect? window) async { | 281 | Future<void> updateScanWindow(Rect? window) async { |
| 284 | if (_textureId == null) { | 282 | if (_textureId == null) { |
| 285 | return; | 283 | return; |
| @@ -281,9 +281,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -281,9 +281,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 281 | isInitialized: true, | 281 | isInitialized: true, |
| 282 | isRunning: true, | 282 | isRunning: true, |
| 283 | size: viewAttributes.size, | 283 | size: viewAttributes.size, |
| 284 | - // If the device has a flashlight, let the platform update the torch state. | ||
| 285 | - // If it does not have one, provide the unavailable state directly. | ||
| 286 | - torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, | 284 | + // Provide the current torch state. |
| 285 | + // Updates are provided by the `torchStateStream`. | ||
| 286 | + torchState: viewAttributes.currentTorchMode, | ||
| 287 | ); | 287 | ); |
| 288 | } | 288 | } |
| 289 | } on MobileScannerException catch (error) { | 289 | } on MobileScannerException catch (error) { |
| @@ -322,11 +322,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -322,11 +322,16 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 322 | 322 | ||
| 323 | _disposeListeners(); | 323 | _disposeListeners(); |
| 324 | 324 | ||
| 325 | + final TorchState oldTorchState = value.torchState; | ||
| 326 | + | ||
| 325 | // After the camera stopped, set the torch state to off, | 327 | // After the camera stopped, set the torch state to off, |
| 326 | // as the torch state callback is never called when the camera is stopped. | 328 | // as the torch state callback is never called when the camera is stopped. |
| 329 | + // If the device does not have a torch, do not report "off". | ||
| 327 | value = value.copyWith( | 330 | value = value.copyWith( |
| 328 | isRunning: false, | 331 | isRunning: false, |
| 329 | - torchState: TorchState.off, | 332 | + torchState: oldTorchState == TorchState.unavailable |
| 333 | + ? TorchState.unavailable | ||
| 334 | + : TorchState.off, | ||
| 330 | ); | 335 | ); |
| 331 | 336 | ||
| 332 | await MobileScannerPlatform.instance.stop(); | 337 | await MobileScannerPlatform.instance.stop(); |
| @@ -362,6 +367,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -362,6 +367,9 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 362 | /// | 367 | /// |
| 363 | /// Does nothing if the device has no torch, | 368 | /// Does nothing if the device has no torch, |
| 364 | /// or if the camera is not running. | 369 | /// or if the camera is not running. |
| 370 | + /// | ||
| 371 | + /// If the current torch state is [TorchState.auto], | ||
| 372 | + /// the torch is turned on or off depending on its actual current state. | ||
| 365 | Future<void> toggleTorch() async { | 373 | Future<void> toggleTorch() async { |
| 366 | _throwIfNotInitialized(); | 374 | _throwIfNotInitialized(); |
| 367 | 375 | ||
| @@ -375,13 +383,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | @@ -375,13 +383,10 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 375 | return; | 383 | return; |
| 376 | } | 384 | } |
| 377 | 385 | ||
| 378 | - final TorchState newState = | ||
| 379 | - torchState == TorchState.off ? TorchState.on : TorchState.off; | ||
| 380 | - | ||
| 381 | - // Update the torch state to the new state. | 386 | + // Request the torch state to be switched to the opposite state. |
| 382 | // When the platform has updated the torch state, | 387 | // When the platform has updated the torch state, |
| 383 | // it will send an update through the torch state event stream. | 388 | // it will send an update through the torch state event stream. |
| 384 | - await MobileScannerPlatform.instance.setTorchState(newState); | 389 | + await MobileScannerPlatform.instance.toggleTorch(); |
| 385 | } | 390 | } |
| 386 | 391 | ||
| 387 | /// Update the scan window with the given [window] rectangle. | 392 | /// Update the scan window with the given [window] rectangle. |
| @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { | @@ -67,11 +67,6 @@ abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 67 | /// This is only supported on the web. | 67 | /// This is only supported on the web. |
| 68 | void setBarcodeLibraryScriptUrl(String scriptUrl) {} | 68 | void setBarcodeLibraryScriptUrl(String scriptUrl) {} |
| 69 | 69 | ||
| 70 | - /// Set the torch state of the active camera. | ||
| 71 | - Future<void> setTorchState(TorchState torchState) { | ||
| 72 | - throw UnimplementedError('setTorchState() has not been implemented.'); | ||
| 73 | - } | ||
| 74 | - | ||
| 75 | /// Set the zoom scale of the camera. | 70 | /// Set the zoom scale of the camera. |
| 76 | /// | 71 | /// |
| 77 | /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). | 72 | /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). |
| @@ -95,6 +90,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { | @@ -95,6 +90,11 @@ abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 95 | throw UnimplementedError('stop() has not been implemented.'); | 90 | throw UnimplementedError('stop() has not been implemented.'); |
| 96 | } | 91 | } |
| 97 | 92 | ||
| 93 | + /// Toggle the torch on the active camera on or off. | ||
| 94 | + Future<void> toggleTorch() { | ||
| 95 | + throw UnimplementedError('toggleTorch() has not been implemented.'); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | /// Update the scan window to the given [window] rectangle. | 98 | /// Update the scan window to the given [window] rectangle. |
| 99 | /// | 99 | /// |
| 100 | /// Any barcodes that do not intersect with the given [window] will be ignored. | 100 | /// Any barcodes that do not intersect with the given [window] will be ignored. |
| 1 | import 'dart:ui'; | 1 | import 'dart:ui'; |
| 2 | 2 | ||
| 3 | +import 'package:mobile_scanner/src/enums/torch_state.dart'; | ||
| 4 | + | ||
| 3 | /// This class defines the attributes for the mobile scanner view. | 5 | /// This class defines the attributes for the mobile scanner view. |
| 4 | class MobileScannerViewAttributes { | 6 | class MobileScannerViewAttributes { |
| 5 | const MobileScannerViewAttributes({ | 7 | const MobileScannerViewAttributes({ |
| 6 | - required this.hasTorch, | 8 | + required this.currentTorchMode, |
| 7 | this.numberOfCameras, | 9 | this.numberOfCameras, |
| 8 | required this.size, | 10 | required this.size, |
| 9 | }); | 11 | }); |
| 10 | 12 | ||
| 11 | - /// Whether the current active camera has a torch. | ||
| 12 | - final bool hasTorch; | 13 | + /// The current torch state of the active camera. |
| 14 | + final TorchState currentTorchMode; | ||
| 13 | 15 | ||
| 14 | /// The number of available cameras. | 16 | /// The number of available cameras. |
| 15 | final int? numberOfCameras; | 17 | final int? numberOfCameras; |
| @@ -186,17 +186,17 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -186,17 +186,17 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 186 | } | 186 | } |
| 187 | 187 | ||
| 188 | try { | 188 | try { |
| 189 | - // Retrieving the media devices requests the camera permission. | ||
| 190 | _permissionRequestInProgress = true; | 189 | _permissionRequestInProgress = true; |
| 191 | 190 | ||
| 191 | + // Retrieving the media devices requests the camera permission. | ||
| 192 | final MediaStream videoStream = | 192 | final MediaStream videoStream = |
| 193 | await window.navigator.mediaDevices.getUserMedia(constraints).toDart; | 193 | await window.navigator.mediaDevices.getUserMedia(constraints).toDart; |
| 194 | 194 | ||
| 195 | - // At this point the permission is granted. | ||
| 196 | _permissionRequestInProgress = false; | 195 | _permissionRequestInProgress = false; |
| 197 | 196 | ||
| 198 | return videoStream; | 197 | return videoStream; |
| 199 | } on DOMException catch (error, stackTrace) { | 198 | } on DOMException catch (error, stackTrace) { |
| 199 | + _permissionRequestInProgress = false; | ||
| 200 | final String errorMessage = error.toString(); | 200 | final String errorMessage = error.toString(); |
| 201 | 201 | ||
| 202 | MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; | 202 | MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; |
| @@ -209,10 +209,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -209,10 +209,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 209 | errorCode = MobileScannerErrorCode.permissionDenied; | 209 | errorCode = MobileScannerErrorCode.permissionDenied; |
| 210 | } | 210 | } |
| 211 | 211 | ||
| 212 | - // At this point the permission request completed, although with an error, | ||
| 213 | - // but the error is irrelevant. | ||
| 214 | - _permissionRequestInProgress = false; | ||
| 215 | - | ||
| 216 | throw MobileScannerException( | 212 | throw MobileScannerException( |
| 217 | errorCode: errorCode, | 213 | errorCode: errorCode, |
| 218 | errorDetails: MobileScannerErrorDetails( | 214 | errorDetails: MobileScannerErrorDetails( |
| @@ -251,14 +247,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -251,14 +247,6 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 251 | } | 247 | } |
| 252 | 248 | ||
| 253 | @override | 249 | @override |
| 254 | - Future<void> setTorchState(TorchState torchState) { | ||
| 255 | - throw UnsupportedError( | ||
| 256 | - 'Setting the torch state is not supported for video tracks on the web.\n' | ||
| 257 | - 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks', | ||
| 258 | - ); | ||
| 259 | - } | ||
| 260 | - | ||
| 261 | - @override | ||
| 262 | Future<void> setZoomScale(double zoomScale) { | 250 | Future<void> setZoomScale(double zoomScale) { |
| 263 | throw UnsupportedError( | 251 | throw UnsupportedError( |
| 264 | 'Setting the zoom scale is not supported for video tracks on the web.\n' | 252 | 'Setting the zoom scale is not supported for video tracks on the web.\n' |
| @@ -346,7 +334,9 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -346,7 +334,9 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 346 | } | 334 | } |
| 347 | 335 | ||
| 348 | return MobileScannerViewAttributes( | 336 | return MobileScannerViewAttributes( |
| 349 | - hasTorch: hasTorch, | 337 | + // The torch of a media stream is not available for video tracks. |
| 338 | + // See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks | ||
| 339 | + currentTorchMode: TorchState.unavailable, | ||
| 350 | size: _barcodeReader?.videoSize ?? Size.zero, | 340 | size: _barcodeReader?.videoSize ?? Size.zero, |
| 351 | ); | 341 | ); |
| 352 | } catch (error, stackTrace) { | 342 | } catch (error, stackTrace) { |
| @@ -371,6 +361,14 @@ class MobileScannerWeb extends MobileScannerPlatform { | @@ -371,6 +361,14 @@ class MobileScannerWeb extends MobileScannerPlatform { | ||
| 371 | } | 361 | } |
| 372 | 362 | ||
| 373 | @override | 363 | @override |
| 364 | + Future<void> toggleTorch() { | ||
| 365 | + throw UnsupportedError( | ||
| 366 | + 'Setting the torch state is not supported for video tracks on the web.\n' | ||
| 367 | + 'See https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#instance_properties_of_video_tracks', | ||
| 368 | + ); | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + @override | ||
| 374 | Future<void> updateScanWindow(Rect? window) { | 372 | Future<void> updateScanWindow(Rect? window) { |
| 375 | // A scan window is not supported on the web, | 373 | // A scan window is not supported on the web, |
| 376 | // because the scanner does not expose size information for the barcodes. | 374 | // because the scanner does not expose size information for the barcodes. |
| @@ -59,8 +59,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -59,8 +59,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 59 | requestPermission(call, result) | 59 | requestPermission(call, result) |
| 60 | case "start": | 60 | case "start": |
| 61 | start(call, result) | 61 | start(call, result) |
| 62 | - case "torch": | ||
| 63 | - toggleTorch(call, result) | 62 | + case "toggleTorch": |
| 63 | + toggleTorch(result) | ||
| 64 | case "setScale": | 64 | case "setScale": |
| 65 | setScale(call, result) | 65 | setScale(call, result) |
| 66 | case "resetScale": | 66 | case "resetScale": |
| @@ -288,12 +288,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -288,12 +288,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 288 | 288 | ||
| 289 | // Turn on the torch if requested. | 289 | // Turn on the torch if requested. |
| 290 | if (torch) { | 290 | if (torch) { |
| 291 | - do { | ||
| 292 | - try self.toggleTorchInternal(.on) | ||
| 293 | - } catch { | ||
| 294 | - // If the torch could not be turned on, | ||
| 295 | - // continue the capture session. | ||
| 296 | - } | 291 | + self.turnTorchOn() |
| 297 | } | 292 | } |
| 298 | 293 | ||
| 299 | device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | 294 | device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) |
| @@ -326,17 +321,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -326,17 +321,22 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 326 | captureSession!.startRunning() | 321 | captureSession!.startRunning() |
| 327 | let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | 322 | let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) |
| 328 | let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)] | 323 | let size = ["width": Double(dimensions.width), "height": Double(dimensions.height)] |
| 329 | - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch] | 324 | + |
| 325 | + let answer: [String : Any?] = [ | ||
| 326 | + "textureId": textureId, | ||
| 327 | + "size": size, | ||
| 328 | + "currentTorchState": device.hasTorch ? device.torchMode.rawValue : -1, | ||
| 329 | + ] | ||
| 330 | result(answer) | 330 | result(answer) |
| 331 | } | 331 | } |
| 332 | 332 | ||
| 333 | // TODO: this method should be removed when iOS and MacOS share their implementation. | 333 | // TODO: this method should be removed when iOS and MacOS share their implementation. |
| 334 | - private func toggleTorchInternal(_ torch: AVCaptureDevice.TorchMode) throws { | 334 | + private func toggleTorchInternal() { |
| 335 | guard let device = self.device else { | 335 | guard let device = self.device else { |
| 336 | return | 336 | return |
| 337 | } | 337 | } |
| 338 | 338 | ||
| 339 | - if (!device.hasTorch || !device.isTorchModeSupported(torch)) { | 339 | + if (!device.hasTorch) { |
| 340 | return | 340 | return |
| 341 | } | 341 | } |
| 342 | 342 | ||
| @@ -346,13 +346,58 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -346,13 +346,58 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 346 | } | 346 | } |
| 347 | } | 347 | } |
| 348 | 348 | ||
| 349 | - if (device.torchMode != torch) { | 349 | + var newTorchMode: AVCaptureDevice.TorchMode = device.torchMode |
| 350 | + | ||
| 351 | + switch(device.torchMode) { | ||
| 352 | + case AVCaptureDevice.TorchMode.auto: | ||
| 353 | + if #available(macOS 10.15, *) { | ||
| 354 | + newTorchMode = device.isTorchActive ? AVCaptureDevice.TorchMode.off : AVCaptureDevice.TorchMode.on | ||
| 355 | + } | ||
| 356 | + break; | ||
| 357 | + case AVCaptureDevice.TorchMode.off: | ||
| 358 | + newTorchMode = AVCaptureDevice.TorchMode.on | ||
| 359 | + break; | ||
| 360 | + case AVCaptureDevice.TorchMode.on: | ||
| 361 | + newTorchMode = AVCaptureDevice.TorchMode.off | ||
| 362 | + break; | ||
| 363 | + default: | ||
| 364 | + return; | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + if (!device.isTorchModeSupported(newTorchMode) || device.torchMode == newTorchMode) { | ||
| 368 | + return; | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + do { | ||
| 350 | try device.lockForConfiguration() | 372 | try device.lockForConfiguration() |
| 351 | - device.torchMode = torch | 373 | + device.torchMode = newTorchMode |
| 352 | device.unlockForConfiguration() | 374 | device.unlockForConfiguration() |
| 375 | + } catch(_) {} | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + /// Turn the torch on. | ||
| 379 | + private func turnTorchOn() { | ||
| 380 | + guard let device = self.device else { | ||
| 381 | + return | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + if (!device.hasTorch || !device.isTorchModeSupported(.on) || device.torchMode == .on) { | ||
| 385 | + return | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + if #available(macOS 15.0, *) { | ||
| 389 | + if(!device.isTorchAvailable) { | ||
| 390 | + return | ||
| 353 | } | 391 | } |
| 354 | } | 392 | } |
| 355 | 393 | ||
| 394 | + do { | ||
| 395 | + try device.lockForConfiguration() | ||
| 396 | + device.torchMode = .on | ||
| 397 | + device.unlockForConfiguration() | ||
| 398 | + } catch(_) {} | ||
| 399 | + } | ||
| 400 | + | ||
| 356 | /// Reset the zoom scale. | 401 | /// Reset the zoom scale. |
| 357 | private func resetScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 402 | private func resetScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 358 | // The zoom scale is not yet supported on MacOS. | 403 | // The zoom scale is not yet supported on MacOS. |
| @@ -365,15 +410,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -365,15 +410,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 365 | result(nil) | 410 | result(nil) |
| 366 | } | 411 | } |
| 367 | 412 | ||
| 368 | - private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 369 | - let requestedTorchMode: AVCaptureDevice.TorchMode = call.arguments as! Int == 1 ? .on : .off | ||
| 370 | - | ||
| 371 | - do { | ||
| 372 | - try self.toggleTorchInternal(requestedTorchMode) | 413 | + private func toggleTorch(_ result: @escaping FlutterResult) { |
| 414 | + self.toggleTorchInternal() | ||
| 373 | result(nil) | 415 | result(nil) |
| 374 | - } catch { | ||
| 375 | - result(FlutterError(code: "MobileScanner", message: error.localizedDescription, details: nil)) | ||
| 376 | - } | ||
| 377 | } | 416 | } |
| 378 | 417 | ||
| 379 | // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 418 | // func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| @@ -410,7 +449,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | @@ -410,7 +449,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, | ||
| 410 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 449 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 411 | switch keyPath { | 450 | switch keyPath { |
| 412 | case "torchMode": | 451 | case "torchMode": |
| 413 | - // off = 0 on = 1 auto = 2 | 452 | + // Off = 0, On = 1, Auto = 2 |
| 414 | let state = change?[.newKey] as? Int | 453 | let state = change?[.newKey] as? Int |
| 415 | let event: [String: Any?] = ["name": "torchState", "data": state] | 454 | let event: [String: Any?] = ["name": "torchState", "data": state] |
| 416 | sink?(event) | 455 | sink?(event) |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | # | 4 | # |
| 5 | Pod::Spec.new do |s| | 5 | Pod::Spec.new do |s| |
| 6 | s.name = 'mobile_scanner' | 6 | s.name = 'mobile_scanner' |
| 7 | - s.version = '5.0.0' | 7 | + s.version = '5.0.2' |
| 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' | 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' |
| 9 | s.description = <<-DESC | 9 | s.description = <<-DESC |
| 10 | An universal scanner for Flutter based on MLKit. | 10 | An universal scanner for Flutter based on MLKit. |
| @@ -7,7 +7,8 @@ void main() { | @@ -7,7 +7,8 @@ void main() { | ||
| 7 | const values = <int, TorchState>{ | 7 | const values = <int, TorchState>{ |
| 8 | 0: TorchState.off, | 8 | 0: TorchState.off, |
| 9 | 1: TorchState.on, | 9 | 1: TorchState.on, |
| 10 | - 2: TorchState.unavailable, | 10 | + 2: TorchState.auto, |
| 11 | + -1: TorchState.unavailable, | ||
| 11 | }; | 12 | }; |
| 12 | 13 | ||
| 13 | for (final MapEntry<int, TorchState> entry in values.entries) { | 14 | for (final MapEntry<int, TorchState> entry in values.entries) { |
| @@ -18,7 +19,7 @@ void main() { | @@ -18,7 +19,7 @@ void main() { | ||
| 18 | }); | 19 | }); |
| 19 | 20 | ||
| 20 | test('invalid raw value throws argument error', () { | 21 | test('invalid raw value throws argument error', () { |
| 21 | - const int negative = -1; | 22 | + const int negative = -2; |
| 22 | const int outOfRange = 3; | 23 | const int outOfRange = 3; |
| 23 | 24 | ||
| 24 | expect(() => TorchState.fromRawValue(negative), throwsArgumentError); | 25 | expect(() => TorchState.fromRawValue(negative), throwsArgumentError); |
| @@ -27,9 +28,10 @@ void main() { | @@ -27,9 +28,10 @@ void main() { | ||
| 27 | 28 | ||
| 28 | test('can be converted to raw value', () { | 29 | test('can be converted to raw value', () { |
| 29 | const values = <TorchState, int>{ | 30 | const values = <TorchState, int>{ |
| 31 | + TorchState.unavailable: -1, | ||
| 30 | TorchState.off: 0, | 32 | TorchState.off: 0, |
| 31 | TorchState.on: 1, | 33 | TorchState.on: 1, |
| 32 | - TorchState.unavailable: 2, | 34 | + TorchState.auto: 2, |
| 33 | }; | 35 | }; |
| 34 | 36 | ||
| 35 | for (final MapEntry<TorchState, int> entry in values.entries) { | 37 | for (final MapEntry<TorchState, int> entry in values.entries) { |
-
Please register or login to post a comment