Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle more facing directions #1328

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
## NEXT

**BREAKING CHANGES:**

* The initial state of the `MobileScannerState` camera facing direction is changed to `CameraFacing.unknown`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done to keep the actual camera state in sync with the value


Improvements:
* [Android] Turn off logging for CameraX, except for the `Log.ERROR` logging level.
* Added `CameraFacing.external` and `CameraFacing.unknown` enum values.

## 7.0.0-beta.6

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ExperimentalLensFacing
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
Expand Down Expand Up @@ -278,6 +279,17 @@ class MobileScanner(
}
}

@ExperimentalLensFacing
private fun getCameraLensFacing(camera: Camera?): Int? {
return when(camera?.cameraInfo?.lensFacing) {
CameraSelector.LENS_FACING_BACK -> 1
CameraSelector.LENS_FACING_FRONT -> 0
CameraSelector.LENS_FACING_EXTERNAL -> 2
CameraSelector.LENS_FACING_UNKNOWN -> null
else -> null
}
}

private fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
Expand Down Expand Up @@ -318,6 +330,7 @@ class MobileScanner(
/**
* Start barcode scanning by initializing the camera and barcode scanner.
*/
@ExperimentalLensFacing
@ExperimentalGetImage
fun start(
barcodeScannerOptions: BarcodeScannerOptions?,
Expand All @@ -343,6 +356,7 @@ class MobileScanner(
// TODO: resume here for seamless transition
// if (isPaused) {
// resumeCamera()
// val cameraDirection = getCameraLensFacing(camera)
// mobileScannerStartedCallback(
// MobileScannerStartParameters(
// if (portrait) width else height,
Expand All @@ -352,7 +366,8 @@ class MobileScanner(
// surfaceProducer!!.handlesCropAndRotation(),
// currentTorchState,
// surfaceProducer!!.id(),
// numberOfCameras ?: 0
// numberOfCameras ?: 0,
// cameraDirection
// )
// )
// return
Expand Down Expand Up @@ -465,6 +480,7 @@ class MobileScanner(
val height = resolution.height.toDouble()
val sensorRotationDegrees = camera?.cameraInfo?.sensorRotationDegrees ?: 0
val portrait = sensorRotationDegrees % 180 == 0
val cameraDirection = getCameraLensFacing(camera)

// Start with 'unavailable' torch state.
var currentTorchState: Int = -1
Expand All @@ -488,7 +504,8 @@ class MobileScanner(
surfaceProducer!!.handlesCropAndRotation(),
currentTorchState,
surfaceProducer!!.id(),
numberOfCameras ?: 0
numberOfCameras ?: 0,
cameraDirection,
)
)
}, executor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.os.Looper
import android.util.Size
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ExperimentalLensFacing
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import dev.steenbakker.mobile_scanner.objects.BarcodeFormats
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
Expand Down Expand Up @@ -114,6 +115,7 @@ class MobileScannerHandler(
}
}

@ExperimentalLensFacing
@ExperimentalGetImage
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
Expand Down Expand Up @@ -146,6 +148,7 @@ class MobileScannerHandler(
}
}

@ExperimentalLensFacing
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
val torch: Boolean = call.argument<Boolean>("torch") ?: false
Expand Down Expand Up @@ -191,7 +194,8 @@ class MobileScannerHandler(
"isPreviewPreTransformed" to it.isPreviewPreTransformed,
"sensorOrientation" to it.sensorOrientation,
"currentTorchState" to it.currentTorchState,
"numberOfCameras" to it.numberOfCameras
"numberOfCameras" to it.numberOfCameras,
"cameraDirection" to it.cameraDirection
))
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ class MobileScannerStartParameters(
val isPreviewPreTransformed: Boolean,
val currentTorchState: Int,
val id: Long,
val numberOfCameras: Int
val numberOfCameras: Int,
val cameraDirection: Int?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,18 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler,
let answer: [String : Any?]

if let device = self.device {
let cameraDirection: Int? = switch(device.position) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact: MacOS reports .unspecified for the front facing FaceTime webcam. Probably because there is only 1 camera?

case .back: 1
case .unspecified: nil
case .front: 0
@unknown default: nil
}

answer = [
"textureId": self.textureId,
"size": size,
"currentTorchState": device.hasTorch ? device.torchMode.rawValue : -1,
"cameraDirection": cameraDirection,
]
} else {
answer = [
Expand Down
4 changes: 4 additions & 0 deletions example/lib/scanner_button_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ class SwitchCameraButton extends StatelessWidget {
icon = const Icon(Icons.camera_front);
case CameraFacing.back:
icon = const Icon(Icons.camera_rear);
case CameraFacing.external:
icon = const Icon(Icons.usb);
case CameraFacing.unknown:
icon = const Icon(Icons.device_unknown);
}

return IconButton(
Expand Down
28 changes: 21 additions & 7 deletions lib/src/enums/camera_facing.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
/// The facing of a camera.
enum CameraFacing {
/// Front facing camera.
/// The camera is a front facing camera.
///
/// This type of camera always faces the user.
front(0),

/// Back facing camera.
back(1);
/// The camera is a back facing camera.
///
/// This type of camera always faces away from the user.
back(1),

/// The camera is an external camera.
///
/// For example a USB-camera.
external(2),

/// The camera facing direction is unknown.
unknown(-1);

const CameraFacing(this.rawValue);

factory CameraFacing.fromRawValue(int value) {
factory CameraFacing.fromRawValue(int? value) {
switch (value) {
case 0:
return CameraFacing.front;
return front;
case 1:
return CameraFacing.back;
return back;
case 2:
return external;
default:
throw ArgumentError.value(value, 'value', 'Invalid raw value.');
return unknown;
}
}

Expand Down
7 changes: 6 additions & 1 deletion lib/src/method_channel/mobile_scanner_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
Expand Down Expand Up @@ -307,13 +308,16 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
}

final CameraFacing cameraDirection =
CameraFacing.fromRawValue(startResult['cameraDirection'] as int?);

_textureId = textureId;

if (defaultTargetPlatform == TargetPlatform.android) {
_surfaceProducerDelegate =
AndroidSurfaceProducerDelegate.fromConfiguration(
startResult,
startOptions.cameraDirection,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To stay consistent with the camera state, we should take the camera direction from the start result

cameraDirection,
);
_surfaceProducerDelegate?.startListeningToDeviceOrientation(
deviceOrientationChangedStream,
Expand All @@ -337,6 +341,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
_pausing = false;

return MobileScannerViewAttributes(
cameraDirection: cameraDirection,
currentTorchMode: currentTorchState,
numberOfCameras: numberOfCameras,
size: size,
Expand Down
54 changes: 38 additions & 16 deletions lib/src/mobile_scanner_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
detectionTimeoutMs >= 0,
'The detection timeout must be greater than or equal to 0.',
),
super(MobileScannerState.uninitialized(facing));
assert(
facing != CameraFacing.unknown,
'CameraFacing.unknown is not a valid camera direction.',
),
super(const MobileScannerState.uninitialized());

/// The desired resolution for the camera.
///
Expand Down Expand Up @@ -303,15 +307,22 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
);
}

if (cameraDirection == CameraFacing.unknown) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: 'CameraFacing.unknown is not a valid camera direction.',
),
);
}

// Do nothing if the camera is already running.
if (value.isRunning) {
return;
}

final CameraFacing effectiveDirection = cameraDirection ?? facing;

final StartOptions options = StartOptions(
cameraDirection: effectiveDirection,
cameraDirection: cameraDirection ?? facing,
cameraResolution: cameraResolution,
detectionSpeed: detectionSpeed,
detectionTimeoutMs: detectionTimeoutMs,
Expand All @@ -333,7 +344,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
if (!_isDisposed) {
value = value.copyWith(
availableCameras: viewAttributes.numberOfCameras,
cameraDirection: effectiveDirection,
cameraDirection: viewAttributes.cameraDirection,
isInitialized: true,
isRunning: true,
size: viewAttributes.size,
Expand All @@ -351,11 +362,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
}

// The initialization finished with an error.
// To avoid stale values, reset the output size,
// torch state and zoom scale to the defaults.
// To avoid stale values, reset the camera direction,
// output size, torch state and zoom scale to the defaults.
if (!_isDisposed) {
value = value.copyWith(
cameraDirection: facing,
cameraDirection: CameraFacing.unknown,
isInitialized: true,
isRunning: false,
error: error,
Expand Down Expand Up @@ -392,11 +403,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {

/// Switch between the front and back camera.
///
/// Does nothing if the device has less than 2 cameras.
/// Does nothing if the device has less than 2 cameras,
/// or if the current camera is an external camera.
Future<void> switchCamera() async {
_throwIfNotInitialized();

final int? availableCameras = value.availableCameras;
final CameraFacing cameraDirection = value.cameraDirection;

// Do nothing if the amount of cameras is less than 2 cameras.
// If the the current platform does not provide the amount of cameras,
Expand All @@ -405,15 +418,24 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
return;
}

await stop();
// If the camera direction is not known,
// or if the camera is an external camera, do not allow switching cameras.
if (cameraDirection == CameraFacing.unknown ||
cameraDirection == CameraFacing.external) {
return;
}

final CameraFacing cameraDirection = value.cameraDirection;
await stop();

await start(
cameraDirection: cameraDirection == CameraFacing.front
? CameraFacing.back
: CameraFacing.front,
);
switch (value.cameraDirection) {
case CameraFacing.front:
return start(cameraDirection: CameraFacing.back);
case CameraFacing.back:
return start(cameraDirection: CameraFacing.front);
case CameraFacing.external:
case CameraFacing.unknown:
return;
}
}

/// Switches the flashlight on or off.
Expand Down
7 changes: 6 additions & 1 deletion lib/src/mobile_scanner_view_attributes.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import 'dart:ui';

import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';

/// This class defines the attributes for the mobile scanner view.
class MobileScannerViewAttributes {
/// Construct a new [MobileScannerViewAttributes] instance.
const MobileScannerViewAttributes({
required this.cameraDirection,
required this.currentTorchMode,
this.numberOfCameras,
required this.size,
this.numberOfCameras,
});

/// The direction of the active camera.
final CameraFacing cameraDirection;

/// The current torch state of the active camera.
final TorchState currentTorchMode;

Expand Down
4 changes: 2 additions & 2 deletions lib/src/objects/mobile_scanner_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ class MobileScannerState {
});

/// Create a new [MobileScannerState] instance that is uninitialized.
const MobileScannerState.uninitialized(CameraFacing facing)
const MobileScannerState.uninitialized()
: this(
availableCameras: null,
cameraDirection: facing,
cameraDirection: CameraFacing.unknown,
isInitialized: false,
isRunning: false,
size: Size.zero,
Expand Down
Loading
Loading