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(
torch: Boolean,
detectionSpeed: DetectionSpeed,
torchStateCallback: TorchStateCallback,
zoomScaleStateCallback: ZoomScaleStateCallback,
mobileScannerStartedCallback: MobileScannerStartedCallback,
detectionTimeout: Long
) {
... ... @@ -201,6 +202,11 @@ class MobileScanner(
torchStateCallback(state)
}
// Register the zoom scale listener
camera!!.cameraInfo.zoomState.observe(activity) { state ->
zoomScaleStateCallback(state.linearZoom.toDouble())
}
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
... ... @@ -283,4 +289,12 @@ class MobileScanner(
camera!!.cameraControl.setLinearZoom(scale.toFloat())
}
/**
* Reset the zoom rate of the camera.
*/
fun resetScale() {
if (camera == null) throw ZoomWhenStopped()
camera!!.cameraControl.setZoomRatio(1f)
}
}
... ...
... ... @@ -6,4 +6,5 @@ typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: Byt
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
typealias TorchStateCallback = (state: Int) -> Unit
typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit
typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
\ No newline at end of file
... ...
... ... @@ -70,6 +70,10 @@ class MobileScannerHandler(
barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state))
}
private val zoomScaleStateCallback: ZoomScaleStateCallback = {zoomScale: Double ->
barcodeHandler.publishEvent(mapOf("name" to "zoomScaleState", "data" to zoomScale))
}
init {
methodChannel = MethodChannel(binaryMessenger,
"dev.steenbakker.mobile_scanner/scanner/method")
... ... @@ -115,6 +119,7 @@ class MobileScannerHandler(
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"setScale" -> setScale(call, result)
"resetScale" -> resetScale(call, result)
"updateScanWindow" -> updateScanWindow(call)
else -> result.notImplemented()
}
... ... @@ -152,7 +157,7 @@ class MobileScannerHandler(
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = {
mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, zoomScaleStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
... ... @@ -229,6 +234,15 @@ class MobileScannerHandler(
}
}
private fun resetScale(call: MethodCall, result: MethodChannel.Result) {
try {
mobileScanner!!.resetScale()
result.success(null)
} catch (e: ZoomWhenStopped) {
result.error("MobileScanner", "Called resetScale() while stopped!", null)
}
}
private fun updateScanWindow(call: MethodCall) {
mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect")
}
... ...
... ... @@ -13,6 +13,7 @@ import MLKitBarcodeScanning
typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
typealias TorchModeChangeCallback = ((Int?) -> ())
typealias ZoomScaleChangeCallback = ((Double?) -> ())
public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
/// Capture session of the camera
... ... @@ -36,6 +37,9 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
/// When torch mode is changes, this callback will be called
let torchModeChangeCallback: TorchModeChangeCallback
/// When zoom scale is changes, this callback will be called
let zoomScaleChangeCallback: ZoomScaleChangeCallback
/// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
private let registry: FlutterTextureRegistry?
... ... @@ -47,10 +51,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback) {
var standardZoomFactor: CGFloat = 1
init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback, torchModeChangeCallback: @escaping TorchModeChangeCallback, zoomScaleChangeCallback: @escaping ZoomScaleChangeCallback) {
self.registry = registry
self.mobileScannerCallback = mobileScannerCallback
self.torchModeChangeCallback = torchModeChangeCallback
self.zoomScaleChangeCallback = zoomScaleChangeCallback
super.init()
}
... ... @@ -133,6 +140,21 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor), options: .new, context: nil)
// Check the zoom factor at switching from ultra wide camera to wide camera.
standardZoomFactor = 1
if #available(iOS 13.0, *) {
for (index, actualDevice) in device.constituentDevices.enumerated() {
if (actualDevice.deviceType != .builtInUltraWideCamera) {
if index > 0 && index <= device.virtualDeviceSwitchOverVideoZoomFactors.count {
standardZoomFactor = CGFloat(truncating: device.virtualDeviceSwitchOverVideoZoomFactors[index - 1])
}
break
}
}
}
do {
try device.lockForConfiguration()
if device.isFocusModeSupported(.continuousAutoFocus) {
... ... @@ -181,6 +203,13 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
} catch {
print("Failed to set initial torch state.")
}
do {
try resetScale()
} catch {
print("Failed to reset zoom scale")
}
let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
... ... @@ -199,6 +228,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
captureSession.removeOutput(output)
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.videoZoomFactor))
registry?.unregisterTexture(textureId)
textureId = nil
captureSession = nil
... ... @@ -228,6 +258,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
// off = 0; on = 1; auto = 2;
let state = change?[.newKey] as? Int
torchModeChangeCallback(state)
case "videoZoomFactor":
let zoomFactor = change?[.newKey] as? CGFloat ?? 1
let zoomScale = (zoomFactor - 1) / 4
zoomScaleChangeCallback(Double(zoomScale))
default:
break
}
... ... @@ -252,7 +286,7 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
actualScale = min(maxZoomFactor, actualScale)
// Limit to 1.0 scale
device.ramp(toVideoZoomFactor: actualScale, withRate: 5)
device.videoZoomFactor = actualScale
device.unlockForConfiguration()
} catch {
throw MobileScannerError.zoomError(error)
... ... @@ -260,6 +294,22 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
/// Reset the zoom factor of the camera
func resetScale() throws {
if (device == nil) {
throw MobileScannerError.zoomWhenStopped
}
do {
try device.lockForConfiguration()
device.videoZoomFactor = standardZoomFactor
device.unlockForConfiguration()
} catch {
throw MobileScannerError.zoomError(error)
}
}
/// Analyze a single image
func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
let image = VisionImage(image: image)
... ...
... ... @@ -57,6 +57,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
}
}, torchModeChangeCallback: { torchState in
barcodeHandler.publishEvent(["name": "torchState", "data": torchState])
}, zoomScaleChangeCallback: { zoomScale in
barcodeHandler.publishEvent(["name": "zoomScaleState", "data": zoomScale])
})
self.barcodeHandler = barcodeHandler
super.init()
... ... @@ -85,6 +87,8 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
analyzeImage(call, result)
case "setScale":
setScale(call, result)
case "resetScale":
resetScale(call, result)
case "updateScanWindow":
updateScanWindow(call, result)
default:
... ... @@ -187,7 +191,28 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
}
result(nil)
}
/// Reset the zoomScale
private func resetScale(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
do {
try mobileScanner.resetScale()
} catch MobileScannerError.zoomWhenStopped {
result(FlutterError(code: "MobileScanner",
message: "Called resetScale() while stopped!",
details: nil))
} catch MobileScannerError.zoomError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
details: error))
} catch {
result(FlutterError(code: "MobileScanner",
message: "Error while zooming.",
details: nil))
}
result(nil)
}
/// Toggles the torch
func updateScanWindow(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let scanWindowData: Array? = (call.arguments as? [String: Any])?["rect"] as? [CGFloat]
... ...
... ... @@ -85,6 +85,9 @@ class MobileScannerController {
late final ValueNotifier<CameraFacing> cameraFacingState =
ValueNotifier(facing);
/// A notifier that provides zoomScale.
final ValueNotifier<double> zoomScaleState = ValueNotifier(0.0);
bool isStarting = false;
/// A notifier that provides availability of the Torch (Flash)
... ... @@ -318,6 +321,11 @@ class MobileScannerController {
await _methodChannel.invokeMethod('setScale', zoomScale);
}
/// Reset the zoomScale of the camera to use standard scale 1x.
Future<void> resetZoomScale() async {
await _methodChannel.invokeMethod('resetScale');
}
/// Disposes the MobileScannerController and closes all listeners.
///
/// If you call this, you cannot use this controller object anymore.
... ... @@ -337,6 +345,9 @@ class MobileScannerController {
final state = TorchState.values[data as int? ?? 0];
torchState.value = state;
break;
case 'zoomScaleState':
zoomScaleState.value = data as double? ?? 0.0;
break;
case 'barcode':
if (data == null) return;
final parsed = (data as List)
... ...