Julian Steenbakker
Committed by GitHub

Merge pull request #592 from ryuta46/imp/zoom-scale-improvements

feat: Improvements about zoom scale value and ultra-wide camera
@@ -138,6 +138,7 @@ class MobileScanner( @@ -138,6 +138,7 @@ class MobileScanner(
138 torch: Boolean, 138 torch: Boolean,
139 detectionSpeed: DetectionSpeed, 139 detectionSpeed: DetectionSpeed,
140 torchStateCallback: TorchStateCallback, 140 torchStateCallback: TorchStateCallback,
  141 + zoomScaleStateCallback: ZoomScaleStateCallback,
141 mobileScannerStartedCallback: MobileScannerStartedCallback, 142 mobileScannerStartedCallback: MobileScannerStartedCallback,
142 detectionTimeout: Long 143 detectionTimeout: Long
143 ) { 144 ) {
@@ -201,6 +202,11 @@ class MobileScanner( @@ -201,6 +202,11 @@ class MobileScanner(
201 torchStateCallback(state) 202 torchStateCallback(state)
202 } 203 }
203 204
  205 + // Register the zoom scale listener
  206 + camera!!.cameraInfo.zoomState.observe(activity) { state ->
  207 + zoomScaleStateCallback(state.linearZoom.toDouble())
  208 + }
  209 +
204 210
205 // Enable torch if provided 211 // Enable torch if provided
206 camera!!.cameraControl.enableTorch(torch) 212 camera!!.cameraControl.enableTorch(torch)
@@ -283,4 +289,12 @@ class MobileScanner( @@ -283,4 +289,12 @@ class MobileScanner(
283 camera!!.cameraControl.setLinearZoom(scale.toFloat()) 289 camera!!.cameraControl.setLinearZoom(scale.toFloat())
284 } 290 }
285 291
  292 + /**
  293 + * Reset the zoom rate of the camera.
  294 + */
  295 + fun resetScale() {
  296 + if (camera == null) throw ZoomWhenStopped()
  297 + camera!!.cameraControl.setZoomRatio(1f)
  298 + }
  299 +
286 } 300 }
@@ -6,4 +6,5 @@ typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: Byt @@ -6,4 +6,5 @@ typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: Byt
6 typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit 6 typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
7 typealias MobileScannerErrorCallback = (error: String) -> Unit 7 typealias MobileScannerErrorCallback = (error: String) -> Unit
8 typealias TorchStateCallback = (state: Int) -> Unit 8 typealias TorchStateCallback = (state: Int) -> Unit
  9 +typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit
9 typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit 10 typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
@@ -70,6 +70,10 @@ class MobileScannerHandler( @@ -70,6 +70,10 @@ class MobileScannerHandler(
70 barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) 70 barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
71 } 71 }
72 72
  73 + private val zoomScaleStateCallback: ZoomScaleStateCallback = {zoomScale: Double ->
  74 + barcodeHandler.publishEvent(mapOf("name" to "zoomScaleState", "data" to zoomScale))
  75 + }
  76 +
73 init { 77 init {
74 methodChannel = MethodChannel(binaryMessenger, 78 methodChannel = MethodChannel(binaryMessenger,
75 "dev.steenbakker.mobile_scanner/scanner/method") 79 "dev.steenbakker.mobile_scanner/scanner/method")
@@ -115,6 +119,7 @@ class MobileScannerHandler( @@ -115,6 +119,7 @@ class MobileScannerHandler(
115 "stop" -> stop(result) 119 "stop" -> stop(result)
116 "analyzeImage" -> analyzeImage(call, result) 120 "analyzeImage" -> analyzeImage(call, result)
117 "setScale" -> setScale(call, result) 121 "setScale" -> setScale(call, result)
  122 + "resetScale" -> resetScale(call, result)
118 "updateScanWindow" -> updateScanWindow(call) 123 "updateScanWindow" -> updateScanWindow(call)
119 else -> result.notImplemented() 124 else -> result.notImplemented()
120 } 125 }
@@ -152,7 +157,7 @@ class MobileScannerHandler( @@ -152,7 +157,7 @@ class MobileScannerHandler(
152 val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} 157 val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
153 158
154 try { 159 try {
155 - mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = { 160 + mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, zoomScaleStateCallback, mobileScannerStartedCallback = {
156 result.success(mapOf( 161 result.success(mapOf(
157 "textureId" to it.id, 162 "textureId" to it.id,
158 "size" to mapOf("width" to it.width, "height" to it.height), 163 "size" to mapOf("width" to it.width, "height" to it.height),
@@ -229,6 +234,15 @@ class MobileScannerHandler( @@ -229,6 +234,15 @@ class MobileScannerHandler(
229 } 234 }
230 } 235 }
231 236
  237 + private fun resetScale(call: MethodCall, result: MethodChannel.Result) {
  238 + try {
  239 + mobileScanner!!.resetScale()
  240 + result.success(null)
  241 + } catch (e: ZoomWhenStopped) {
  242 + result.error("MobileScanner", "Called resetScale() while stopped!", null)
  243 + }
  244 + }
  245 +
232 private fun updateScanWindow(call: MethodCall) { 246 private fun updateScanWindow(call: MethodCall) {
233 mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect") 247 mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect")
234 } 248 }
@@ -13,6 +13,7 @@ import MLKitBarcodeScanning @@ -13,6 +13,7 @@ import MLKitBarcodeScanning
13 13
14 typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) 14 typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
15 typealias TorchModeChangeCallback = ((Int?) -> ()) 15 typealias TorchModeChangeCallback = ((Int?) -> ())
  16 +typealias ZoomScaleChangeCallback = ((Double?) -> ())
16 17
17 public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { 18 public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
18 /// Capture session of the camera 19 /// Capture session of the camera
@@ -36,6 +37,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -36,6 +37,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
36 /// When torch mode is changes, this callback will be called 37 /// When torch mode is changes, this callback will be called
37 let torchModeChangeCallback: TorchModeChangeCallback 38 let torchModeChangeCallback: TorchModeChangeCallback
38 39
  40 + /// When zoom scale is changes, this callback will be called
  41 + let zoomScaleChangeCallback: ZoomScaleChangeCallback
  42 +
39 /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. 43 /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
40 private let registry: FlutterTextureRegistry? 44 private let registry: FlutterTextureRegistry?
41 45
@@ -47,10 +51,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -47,10 +51,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
47 51
48 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates 52 var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
49 53
50 - init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback) { 54 + var standardZoomFactor: CGFloat = 1
  55 +
  56 + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) {
51 self.registry = registry 57 self.registry = registry
52 self.mobileScannerCallback = mobileScannerCallback 58 self.mobileScannerCallback = mobileScannerCallback
53 self.torchModeChangeCallback = torchModeChangeCallback 59 self.torchModeChangeCallback = torchModeChangeCallback
  60 + self.zoomScaleChangeCallback = zoomScaleChangeCallback
54 super.init() 61 super.init()
55 } 62 }
56 63
@@ -133,6 +140,21 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -133,6 +140,21 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
133 } 140 }
134 141
135 device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) 142 device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
  143 + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor), options: .new, context: nil)
  144 +
  145 + // Check the zoom factor at switching from ultra wide camera to wide camera.
  146 + standardZoomFactor = 1
  147 + if #available(iOS 13.0, *) {
  148 + for (index, actualDevice) in device.constituentDevices.enumerated() {
  149 + if (actualDevice.deviceType != .builtInUltraWideCamera) {
  150 + if index > 0 && index <= device.virtualDeviceSwitchOverVideoZoomFactors.count {
  151 + standardZoomFactor = CGFloat(truncating: device.virtualDeviceSwitchOverVideoZoomFactors[index - 1])
  152 + }
  153 + break
  154 + }
  155 + }
  156 + }
  157 +
136 do { 158 do {
137 try device.lockForConfiguration() 159 try device.lockForConfiguration()
138 if device.isFocusModeSupported(.continuousAutoFocus) { 160 if device.isFocusModeSupported(.continuousAutoFocus) {
@@ -181,6 +203,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -181,6 +203,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
181 } catch { 203 } catch {
182 print("Failed to set initial torch state.") 204 print("Failed to set initial torch state.")
183 } 205 }
  206 +
  207 + do {
  208 + try resetScale()
  209 + } catch {
  210 + print("Failed to reset zoom scale")
  211 + }
  212 +
184 let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) 213 let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
185 214
186 return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) 215 return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
@@ -199,6 +228,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -199,6 +228,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
199 captureSession.removeOutput(output) 228 captureSession.removeOutput(output)
200 } 229 }
201 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) 230 device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
  231 + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor))
202 registry?.unregisterTexture(textureId) 232 registry?.unregisterTexture(textureId)
203 textureId = nil 233 textureId = nil
204 captureSession = nil 234 captureSession = nil
@@ -228,6 +258,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -228,6 +258,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
228 // off = 0; on = 1; auto = 2; 258 // off = 0; on = 1; auto = 2;
229 let state = change?[.newKey] as? Int 259 let state = change?[.newKey] as? Int
230 torchModeChangeCallback(state) 260 torchModeChangeCallback(state)
  261 + case "videoZoomFactor":
  262 + let zoomFactor = change?[.newKey] as? CGFloat ?? 1
  263 + let zoomScale = (zoomFactor - 1) / 4
  264 + zoomScaleChangeCallback(Double(zoomScale))
231 default: 265 default:
232 break 266 break
233 } 267 }
@@ -252,7 +286,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -252,7 +286,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
252 actualScale = min(maxZoomFactor, actualScale) 286 actualScale = min(maxZoomFactor, actualScale)
253 287
254 // Limit to 1.0 scale 288 // Limit to 1.0 scale
255 - device.ramp(toVideoZoomFactor: actualScale, withRate: 5) 289 + device.videoZoomFactor = actualScale
256 device.unlockForConfiguration() 290 device.unlockForConfiguration()
257 } catch { 291 } catch {
258 throw MobileScannerError.zoomError(error) 292 throw MobileScannerError.zoomError(error)
@@ -260,6 +294,22 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega @@ -260,6 +294,22 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
260 294
261 } 295 }
262 296
  297 + /// Reset the zoom factor of the camera
  298 + func resetScale() throws {
  299 + if (device == nil) {
  300 + throw MobileScannerError.zoomWhenStopped
  301 + }
  302 +
  303 + do {
  304 + try device.lockForConfiguration()
  305 + device.videoZoomFactor = standardZoomFactor
  306 + device.unlockForConfiguration()
  307 + } catch {
  308 + throw MobileScannerError.zoomError(error)
  309 + }
  310 + }
  311 +
  312 +
263 /// Analyze a single image 313 /// Analyze a single image
264 func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { 314 func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
265 let image = VisionImage(image: image) 315 let image = VisionImage(image: image)
@@ -57,6 +57,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -57,6 +57,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
57 } 57 }
58 }, torchModeChangeCallback: { torchState in 58 }, torchModeChangeCallback: { torchState in
59 barcodeHandler.publishEvent(["name": "torchState", "data": torchState]) 59 barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
  60 + }, zoomScaleChangeCallback: { zoomScale in
  61 + barcodeHandler.publishEvent(["name": "zoomScaleState", "data": zoomScale])
60 }) 62 })
61 self.barcodeHandler = barcodeHandler 63 self.barcodeHandler = barcodeHandler
62 super.init() 64 super.init()
@@ -85,6 +87,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -85,6 +87,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
85 analyzeImage(call, result) 87 analyzeImage(call, result)
86 case "setScale": 88 case "setScale":
87 setScale(call, result) 89 setScale(call, result)
  90 + case "resetScale":
  91 + resetScale(call, result)
88 case "updateScanWindow": 92 case "updateScanWindow":
89 updateScanWindow(call, result) 93 updateScanWindow(call, result)
90 default: 94 default:
@@ -187,7 +191,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { @@ -187,7 +191,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
187 } 191 }
188 result(nil) 192 result(nil)
189 } 193 }
190 - 194 +
  195 + /// Reset the zoomScale
  196 + private func resetScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
  197 + do {
  198 + try mobileScanner.resetScale()
  199 + } catch MobileScannerError.zoomWhenStopped {
  200 + result(FlutterError(code: "MobileScanner",
  201 + message: "Called resetScale() while stopped!",
  202 + details: nil))
  203 + } catch MobileScannerError.zoomError(let error) {
  204 + result(FlutterError(code: "MobileScanner",
  205 + message: "Error while zooming.",
  206 + details: error))
  207 + } catch {
  208 + result(FlutterError(code: "MobileScanner",
  209 + message: "Error while zooming.",
  210 + details: nil))
  211 + }
  212 + result(nil)
  213 + }
  214 +
  215 +
191 /// Toggles the torch 216 /// Toggles the torch
192 func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { 217 func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
193 let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat] 218 let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat]
@@ -85,6 +85,9 @@ class MobileScannerController { @@ -85,6 +85,9 @@ class MobileScannerController {
85 late final ValueNotifier<CameraFacing> cameraFacingState = 85 late final ValueNotifier<CameraFacing> cameraFacingState =
86 ValueNotifier(facing); 86 ValueNotifier(facing);
87 87
  88 + /// A notifier that provides zoomScale.
  89 + final ValueNotifier<double> zoomScaleState = ValueNotifier(0.0);
  90 +
88 bool isStarting = false; 91 bool isStarting = false;
89 92
90 /// A notifier that provides availability of the Torch (Flash) 93 /// A notifier that provides availability of the Torch (Flash)
@@ -318,6 +321,11 @@ class MobileScannerController { @@ -318,6 +321,11 @@ class MobileScannerController {
318 await _methodChannel.invokeMethod('setScale', zoomScale); 321 await _methodChannel.invokeMethod('setScale', zoomScale);
319 } 322 }
320 323
  324 + /// Reset the zoomScale of the camera to use standard scale 1x.
  325 + Future<void> resetZoomScale() async {
  326 + await _methodChannel.invokeMethod('resetScale');
  327 + }
  328 +
321 /// Disposes the MobileScannerController and closes all listeners. 329 /// Disposes the MobileScannerController and closes all listeners.
322 /// 330 ///
323 /// If you call this, you cannot use this controller object anymore. 331 /// If you call this, you cannot use this controller object anymore.
@@ -337,6 +345,9 @@ class MobileScannerController { @@ -337,6 +345,9 @@ class MobileScannerController {
337 final state = TorchState.values[data as int? ?? 0]; 345 final state = TorchState.values[data as int? ?? 0];
338 torchState.value = state; 346 torchState.value = state;
339 break; 347 break;
  348 + case 'zoomScaleState':
  349 + zoomScaleState.value = data as double? ?? 0.0;
  350 + break;
340 case 'barcode': 351 case 'barcode':
341 if (data == null) return; 352 if (data == null) return;
342 final parsed = (data as List) 353 final parsed = (data as List)