blob: fd42cc6956658d98dab5c7411d876a21d36f7ece [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:async/async.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_transform/stream_transform.dart';
import 'analyzer.dart';
import 'camera.dart';
import 'camera_info.dart';
import 'camera_selector.dart';
import 'camera_state.dart';
import 'camerax_library.g.dart';
import 'exposure_state.dart';
import 'fallback_strategy.dart';
import 'image_analysis.dart';
import 'image_capture.dart';
import 'image_proxy.dart';
import 'live_data.dart';
import 'observer.dart';
import 'pending_recording.dart';
import 'plane_proxy.dart';
import 'preview.dart';
import 'process_camera_provider.dart';
import 'quality_selector.dart';
import 'recorder.dart';
import 'recording.dart';
import 'resolution_selector.dart';
import 'resolution_strategy.dart';
import 'surface.dart';
import 'system_services.dart';
import 'use_case.dart';
import 'video_capture.dart';
import 'zoom_state.dart';
/// The Android implementation of [CameraPlatform] that uses the CameraX library.
class AndroidCameraCameraX extends CameraPlatform {
/// Constructs an [AndroidCameraCameraX].
AndroidCameraCameraX() : _shouldCreateDetachedObjectForTesting = false;
/// Constructs an [AndroidCameraCameraX] that is able to set
/// [_shouldCreateDetachedObjectForTesting] to create detached objects
/// for testing purposes only.
@visibleForTesting
AndroidCameraCameraX.forTesting(
{bool shouldCreateDetachedObjectForTesting = false})
: _shouldCreateDetachedObjectForTesting =
shouldCreateDetachedObjectForTesting;
/// Registers this class as the default instance of [CameraPlatform].
static void registerWith() {
CameraPlatform.instance = AndroidCameraCameraX();
}
/// The [ProcessCameraProvider] instance used to access camera functionality.
@visibleForTesting
ProcessCameraProvider? processCameraProvider;
/// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is
/// bound to the lifecycle of the camera it manages.
@visibleForTesting
Camera? camera;
/// The [CameraInfo] instance that corresponds to the [camera] instance.
@visibleForTesting
CameraInfo? cameraInfo;
/// The [LiveData] of the [CameraState] that represents the state of the
/// [camera] instance.
LiveData<CameraState>? liveCameraState;
/// The [Preview] instance that can be configured to present a live camera preview.
@visibleForTesting
Preview? preview;
/// The [VideoCapture] instance that can be instantiated and configured to
/// handle video recording
@visibleForTesting
VideoCapture? videoCapture;
/// The [Recorder] instance handling the current creating a new [PendingRecording].
@visibleForTesting
Recorder? recorder;
/// The [PendingRecording] instance used to create an active [Recording].
@visibleForTesting
PendingRecording? pendingRecording;
/// The [Recording] instance representing the current recording.
@visibleForTesting
Recording? recording;
/// The path at which the video file will be saved for the current [Recording].
@visibleForTesting
String? videoOutputPath;
bool _previewIsPaused = false;
/// The prefix used to create the filename for video recording files.
@visibleForTesting
final String videoPrefix = 'MOV';
/// The [ImageCapture] instance that can be configured to capture a still image.
@visibleForTesting
ImageCapture? imageCapture;
/// The flash mode currently configured for [imageCapture].
int? _currentFlashMode;
/// The [ImageAnalysis] instance that can be configured to analyze individual
/// frames.
ImageAnalysis? imageAnalysis;
/// The [CameraSelector] used to configure the [processCameraProvider] to use
/// the desired camera.
@visibleForTesting
CameraSelector? cameraSelector;
/// The controller we need to broadcast the different camera events.
///
/// It is a `broadcast` because multiple controllers will connect to
/// different stream views of this Controller.
/// This is only exposed for test purposes. It shouldn't be used by clients of
/// the plugin as it may break or change at any time.
@visibleForTesting
final StreamController<CameraEvent> cameraEventStreamController =
StreamController<CameraEvent>.broadcast();
/// The stream of camera events.
Stream<CameraEvent> _cameraEvents(int cameraId) =>
cameraEventStreamController.stream
.where((CameraEvent event) => event.cameraId == cameraId);
/// Conditional used to create detached objects for testing their
/// callback methods.
final bool _shouldCreateDetachedObjectForTesting;
/// The controller we need to stream image data.
@visibleForTesting
StreamController<CameraImageData>? cameraImageDataStreamController;
/// Constant representing the multi-plane Android YUV 420 image format.
///
/// See https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888.
static const int imageFormatYuv420_888 = 35;
/// Constant representing the compressed JPEG image format.
///
/// See https://developer.android.com/reference/android/graphics/ImageFormat#JPEG.
static const int imageFormatJpeg = 256;
/// Error code indicating a [ZoomState] was requested, but one has not been
/// set for the camera in use.
static const String zoomStateNotSetErrorCode = 'zoomStateNotSet';
/// Returns list of all available cameras and their descriptions.
@override
Future<List<CameraDescription>> availableCameras() async {
final List<CameraDescription> cameraDescriptions = <CameraDescription>[];
processCameraProvider ??= await ProcessCameraProvider.getInstance();
final List<CameraInfo> cameraInfos =
await processCameraProvider!.getAvailableCameraInfos();
CameraLensDirection? cameraLensDirection;
int cameraCount = 0;
int? cameraSensorOrientation;
String? cameraName;
for (final CameraInfo cameraInfo in cameraInfos) {
// Determine the lens direction by filtering the CameraInfo
// TODO(gmackall): replace this with call to CameraInfo.getLensFacing when changes containing that method are available
if ((await createCameraSelector(CameraSelector.lensFacingBack)
.filter(<CameraInfo>[cameraInfo]))
.isNotEmpty) {
cameraLensDirection = CameraLensDirection.back;
} else if ((await createCameraSelector(CameraSelector.lensFacingFront)
.filter(<CameraInfo>[cameraInfo]))
.isNotEmpty) {
cameraLensDirection = CameraLensDirection.front;
} else {
//Skip this CameraInfo as its lens direction is unknown
continue;
}
cameraSensorOrientation = await cameraInfo.getSensorRotationDegrees();
cameraName = 'Camera $cameraCount';
cameraCount++;
cameraDescriptions.add(CameraDescription(
name: cameraName,
lensDirection: cameraLensDirection,
sensorOrientation: cameraSensorOrientation));
}
return cameraDescriptions;
}
/// Creates an uninitialized camera instance and returns the camera ID.
///
/// In the CameraX library, cameras are accessed by combining [UseCase]s
/// to an instance of a [ProcessCameraProvider]. Thus, to create an
/// uninitialized camera instance, this method retrieves a
/// [ProcessCameraProvider] instance.
///
/// To return the camera ID, which is equivalent to the ID of the surface texture
/// that a camera preview can be drawn to, a [Preview] instance is configured
/// and bound to the [ProcessCameraProvider] instance.
@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) async {
// Must obtain proper permissions before attempting to access a camera.
await requestCameraPermissions(enableAudio);
// Save CameraSelector that matches cameraDescription.
final int cameraSelectorLensDirection =
_getCameraSelectorLensDirection(cameraDescription.lensDirection);
final bool cameraIsFrontFacing =
cameraSelectorLensDirection == CameraSelector.lensFacingFront;
cameraSelector = createCameraSelector(cameraSelectorLensDirection);
// Start listening for device orientation changes preceding camera creation.
startListeningForDeviceOrientationChange(
cameraIsFrontFacing, cameraDescription.sensorOrientation);
// Determine ResolutionSelector and QualitySelector based on
// resolutionPreset for camera UseCases.
final ResolutionSelector? presetResolutionSelector =
_getResolutionSelectorFromPreset(resolutionPreset);
final QualitySelector? presetQualitySelector =
_getQualitySelectorFromPreset(resolutionPreset);
// Retrieve a fresh ProcessCameraProvider instance.
processCameraProvider ??= await ProcessCameraProvider.getInstance();
processCameraProvider!.unbindAll();
// Configure Preview instance.
final int targetRotation =
_getTargetRotation(cameraDescription.sensorOrientation);
preview = createPreview(
targetRotation: targetRotation,
resolutionSelector: presetResolutionSelector);
final int flutterSurfaceTextureId = await preview!.setSurfaceProvider();
// Configure ImageCapture instance.
imageCapture = createImageCapture(presetResolutionSelector);
// Configure ImageAnalysis instance.
// Defaults to YUV_420_888 image format.
imageAnalysis = createImageAnalysis(presetResolutionSelector);
// Configure VideoCapture and Recorder instances.
recorder = createRecorder(presetQualitySelector);
videoCapture = await createVideoCapture(recorder!);
// Bind configured UseCases to ProcessCameraProvider instance & mark Preview
// instance as bound but not paused. Video capture is bound at first use
// instead of here.
camera = await processCameraProvider!.bindToLifecycle(
cameraSelector!, <UseCase>[preview!, imageCapture!, imageAnalysis!]);
await _updateCameraInfoAndLiveCameraState(flutterSurfaceTextureId);
_previewIsPaused = false;
return flutterSurfaceTextureId;
}
/// Initializes the camera on the device.
///
/// Since initialization of a camera does not directly map as an operation to
/// the CameraX library, this method just retrieves information about the
/// camera and sends a [CameraInitializedEvent].
///
/// [imageFormatGroup] is used to specify the image format used for image
/// streaming, but CameraX currently only supports YUV_420_888 (supported by
/// Flutter) and RGBA (not supported by Flutter). CameraX uses YUV_420_888
/// by default, so [imageFormatGroup] is not used.
@override
Future<void> initializeCamera(
int cameraId, {
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
}) async {
// Configure CameraInitializedEvent to send as representation of a
// configured camera:
// Retrieve preview resolution.
if (preview == null) {
// No camera has been created; createCamera must be called before initializeCamera.
throw CameraException(
'cameraNotFound',
"Camera not found. Please call the 'create' method before calling 'initialize'",
);
}
final ResolutionInfo previewResolutionInfo =
await preview!.getResolutionInfo();
// Retrieve exposure and focus mode configurations:
// TODO(camsim99): Implement support for retrieving exposure mode configuration.
// https://github.com/flutter/flutter/issues/120468
const ExposureMode exposureMode = ExposureMode.auto;
const bool exposurePointSupported = false;
// TODO(camsim99): Implement support for retrieving focus mode configuration.
// https://github.com/flutter/flutter/issues/120467
const FocusMode focusMode = FocusMode.auto;
const bool focusPointSupported = false;
cameraEventStreamController.add(CameraInitializedEvent(
cameraId,
previewResolutionInfo.width.toDouble(),
previewResolutionInfo.height.toDouble(),
exposureMode,
exposurePointSupported,
focusMode,
focusPointSupported));
}
/// Releases the resources of the accessed camera.
///
/// [cameraId] not used.
@override
Future<void> dispose(int cameraId) async {
preview?.releaseFlutterSurfaceTexture();
unawaited(liveCameraState?.removeObservers());
processCameraProvider?.unbindAll();
unawaited(imageAnalysis?.clearAnalyzer());
}
/// The camera has been initialized.
@override
Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraInitializedEvent>();
}
/// The camera's resolution has changed.
///
/// This stream currently has no events being added to it from this plugin.
@override
Stream<CameraResolutionChangedEvent> onCameraResolutionChanged(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraResolutionChangedEvent>();
}
/// The camera started to close.
@override
Stream<CameraClosingEvent> onCameraClosing(int cameraId) {
return _cameraEvents(cameraId).whereType<CameraClosingEvent>();
}
/// The camera experienced an error.
@override
Stream<CameraErrorEvent> onCameraError(int cameraId) {
return StreamGroup.mergeBroadcast<
CameraErrorEvent>(<Stream<CameraErrorEvent>>[
SystemServices.cameraErrorStreamController.stream
.map<CameraErrorEvent>((String errorDescription) {
return CameraErrorEvent(cameraId, errorDescription);
}),
_cameraEvents(cameraId).whereType<CameraErrorEvent>()
]);
}
/// Gets the minimum supported exposure offset for the selected camera in EV units.
///
/// [cameraId] not used.
@override
Future<double> getMinExposureOffset(int cameraId) async {
final ExposureState exposureState = await cameraInfo!.getExposureState();
return exposureState.exposureCompensationRange.minCompensation *
exposureState.exposureCompensationStep;
}
/// Gets the maximum supported exposure offset for the selected camera in EV units.
///
/// [cameraId] not used.
@override
Future<double> getMaxExposureOffset(int cameraId) async {
final ExposureState exposureState = await cameraInfo!.getExposureState();
return exposureState.exposureCompensationRange.maxCompensation *
exposureState.exposureCompensationStep;
}
/// Gets the supported step size for exposure offset for the selected camera in EV units.
///
/// Returns 0 when exposure compensation is not supported.
///
/// [cameraId] not used.
@override
Future<double> getExposureOffsetStepSize(int cameraId) async {
final ExposureState exposureState = await cameraInfo!.getExposureState();
return exposureState.exposureCompensationStep;
}
/// Gets the maximum supported zoom level for the selected camera.
///
/// [cameraId] not used.
@override
Future<double> getMaxZoomLevel(int cameraId) async {
final LiveData<ZoomState> liveZoomState = await cameraInfo!.getZoomState();
final ZoomState? zoomState = await liveZoomState.getValue();
if (zoomState == null) {
throw CameraException(
zoomStateNotSetErrorCode,
'No explicit ZoomState has been set on the LiveData instance for the camera in use.',
);
}
return zoomState.maxZoomRatio;
}
/// Gets the minimum supported zoom level for the selected camera.
///
/// [cameraId] not used.
@override
Future<double> getMinZoomLevel(int cameraId) async {
final LiveData<ZoomState> liveZoomState = await cameraInfo!.getZoomState();
final ZoomState? zoomState = await liveZoomState.getValue();
if (zoomState == null) {
throw CameraException(
zoomStateNotSetErrorCode,
'No explicit ZoomState has been set on the LiveData instance for the camera in use.',
);
}
return zoomState.minZoomRatio;
}
/// The ui orientation changed.
@override
Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() {
return SystemServices.deviceOrientationChangedStreamController.stream;
}
/// Pause the active preview on the current frame for the selected camera.
///
/// [cameraId] not used.
@override
Future<void> pausePreview(int cameraId) async {
_previewIsPaused = true;
await _unbindUseCaseFromLifecycle(preview!);
}
/// Resume the paused preview for the selected camera.
///
/// [cameraId] not used.
@override
Future<void> resumePreview(int cameraId) async {
_previewIsPaused = false;
await _bindPreviewToLifecycle(cameraId);
}
/// Returns a widget showing a live camera preview.
@override
Widget buildPreview(int cameraId) {
return FutureBuilder<void>(
future: _bindPreviewToLifecycle(cameraId),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
// Do nothing while waiting for preview to be bound to lifecyle.
return const SizedBox.shrink();
case ConnectionState.done:
return Texture(textureId: cameraId);
}
});
}
/// Captures an image and returns the file where it was saved.
///
/// [cameraId] is not used.
@override
Future<XFile> takePicture(int cameraId) async {
if (_currentFlashMode != null) {
await imageCapture!.setFlashMode(_currentFlashMode!);
}
final String picturePath = await imageCapture!.takePicture();
return XFile(picturePath);
}
/// Sets the flash mode for the selected camera.
@override
Future<void> setFlashMode(int cameraId, FlashMode mode) async {
switch (mode) {
case FlashMode.off:
_currentFlashMode = ImageCapture.flashModeOff;
break;
case FlashMode.auto:
_currentFlashMode = ImageCapture.flashModeAuto;
break;
case FlashMode.always:
_currentFlashMode = ImageCapture.flashModeOn;
break;
case FlashMode.torch:
// TODO(camsim99): Implement torch mode when CameraControl is wrapped.
break;
}
}
/// Configures and starts a video recording. Returns silently without doing
/// anything if there is currently an active recording.
///
/// Note that the preset resolution is used to configure the recording, but
/// 240p ([ResolutionPreset.low]) is unsupported and will fallback to
/// configure the recording as the next highest available quality.
@override
Future<void> startVideoRecording(int cameraId,
{Duration? maxVideoDuration}) async {
assert(cameraSelector != null);
assert(processCameraProvider != null);
if (recording != null) {
// There is currently an active recording, so do not start a new one.
return;
}
if (!(await processCameraProvider!.isBound(videoCapture!))) {
camera = await processCameraProvider!
.bindToLifecycle(cameraSelector!, <UseCase>[videoCapture!]);
}
videoOutputPath =
await SystemServices.getTempFilePath(videoPrefix, '.temp');
pendingRecording = await recorder!.prepareRecording(videoOutputPath!);
recording = await pendingRecording!.start();
}
/// Stops the video recording and returns the file where it was saved.
/// Throws a CameraException if the recording is currently null, or if the
/// videoOutputPath is null.
///
/// If the videoOutputPath is null the recording objects are cleaned up
/// so starting a new recording is possible.
@override
Future<XFile> stopVideoRecording(int cameraId) async {
if (recording == null) {
throw CameraException(
'videoRecordingFailed',
'Attempting to stop a '
'video recording while no recording is in progress.');
}
if (videoOutputPath == null) {
// Stop the current active recording as we will be unable to complete it
// in this error case.
unawaited(recording!.close());
recording = null;
pendingRecording = null;
throw CameraException(
'INVALID_PATH',
'The platform did not return a path '
'while reporting success. The platform should always '
'return a valid path or report an error.');
}
unawaited(recording!.close());
recording = null;
pendingRecording = null;
return XFile(videoOutputPath!);
}
/// Pause the current video recording if it is not null.
@override
Future<void> pauseVideoRecording(int cameraId) async {
if (recording != null) {
await recording!.pause();
}
}
/// Resume the current video recording if it is not null.
@override
Future<void> resumeVideoRecording(int cameraId) async {
if (recording != null) {
await recording!.resume();
}
}
/// A new streamed frame is available.
///
/// Listening to this stream will start streaming, and canceling will stop.
/// To temporarily stop receiving frames, cancel, then listen again later.
/// Pausing/resuming is not supported, as pausing the stream would cause
/// very high memory usage, and will throw an exception due to the
/// implementation using a broadcast [StreamController], which does not
/// support those operations.
///
/// [cameraId] and [options] are not used.
@override
Stream<CameraImageData> onStreamedFrameAvailable(int cameraId,
{CameraImageStreamOptions? options}) {
cameraImageDataStreamController = StreamController<CameraImageData>(
onListen: () => _onFrameStreamListen(cameraId),
onCancel: _onFrameStreamCancel,
);
return cameraImageDataStreamController!.stream;
}
// Methods for binding UseCases to the lifecycle of the camera controlled
// by a ProcessCameraProvider instance:
/// Binds [preview] instance to the camera lifecycle controlled by the
/// [processCameraProvider].
///
/// [cameraId] used to build [CameraEvent]s should you wish to filter
/// these based on a reference to a cameraId received from calling
/// `createCamera(...)`.
Future<void> _bindPreviewToLifecycle(int cameraId) async {
final bool previewIsBound = await processCameraProvider!.isBound(preview!);
if (previewIsBound || _previewIsPaused) {
// Only bind if preview is not already bound or intentionally paused.
return;
}
camera = await processCameraProvider!
.bindToLifecycle(cameraSelector!, <UseCase>[preview!]);
await _updateCameraInfoAndLiveCameraState(cameraId);
}
/// Configures the [imageAnalysis] instance for image streaming and binds it
/// to camera lifecycle controlled by the [processCameraProvider].
Future<void> _configureAndBindImageAnalysisToLifecycle(int cameraId) async {
// Create Analyzer that can read image data for image streaming.
final WeakReference<AndroidCameraCameraX> weakThis =
WeakReference<AndroidCameraCameraX>(this);
Future<void> analyze(ImageProxy imageProxy) async {
final List<PlaneProxy> planes = await imageProxy.getPlanes();
final List<CameraImagePlane> cameraImagePlanes = <CameraImagePlane>[];
for (final PlaneProxy plane in planes) {
cameraImagePlanes.add(CameraImagePlane(
bytes: plane.buffer,
bytesPerRow: plane.rowStride,
bytesPerPixel: plane.pixelStride));
}
final int format = imageProxy.format;
final CameraImageFormat cameraImageFormat = CameraImageFormat(
_imageFormatGroupFromPlatformData(format),
raw: format);
final CameraImageData cameraImageData = CameraImageData(
format: cameraImageFormat,
planes: cameraImagePlanes,
height: imageProxy.height,
width: imageProxy.width);
weakThis.target!.cameraImageDataStreamController!.add(cameraImageData);
unawaited(imageProxy.close());
}
// shouldCreateDetachedObjectForTesting is used to create an Analyzer
// detached from the native sideonly to test the logic of the Analyzer
// instance that will be used for image streaming.
final Analyzer analyzer = _shouldCreateDetachedObjectForTesting
? Analyzer.detached(analyze: analyze)
: Analyzer(analyze: analyze);
await imageAnalysis!.setAnalyzer(analyzer);
}
/// Unbinds [useCase] from camera lifecycle controlled by the
/// [processCameraProvider].
Future<void> _unbindUseCaseFromLifecycle(UseCase useCase) async {
final bool useCaseIsBound = await processCameraProvider!.isBound(useCase);
if (!useCaseIsBound) {
return;
}
processCameraProvider!.unbind(<UseCase>[useCase]);
}
// Methods for configuring image streaming:
/// The [onListen] callback for the stream controller used for image
/// streaming.
Future<void> _onFrameStreamListen(int cameraId) async {
await _configureAndBindImageAnalysisToLifecycle(cameraId);
}
/// The [onCancel] callback for the stream controller used for image
/// streaming.
///
/// Removes the previously set analyzer on the [imageAnalysis] instance, since
/// image information should no longer be streamed.
FutureOr<void> _onFrameStreamCancel() async {
unawaited(imageAnalysis!.clearAnalyzer());
}
/// Converts between Android ImageFormat constants and [ImageFormatGroup]s.
///
/// See https://developer.android.com/reference/android/graphics/ImageFormat.
ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) {
switch (data) {
case imageFormatYuv420_888: // android.graphics.ImageFormat.YUV_420_888
return ImageFormatGroup.yuv420;
case imageFormatJpeg: // android.graphics.ImageFormat.JPEG
return ImageFormatGroup.jpeg;
}
return ImageFormatGroup.unknown;
}
// Methods concerning camera state:
/// Updates [cameraInfo] to the information corresponding to [camera] and
/// adds observers to the [LiveData] of the [CameraState] of the current
/// [camera], saved as [liveCameraState].
///
/// If a previous [liveCameraState] was stored, existing observers are
/// removed, as well.
Future<void> _updateCameraInfoAndLiveCameraState(int cameraId) async {
cameraInfo = await camera!.getCameraInfo();
await liveCameraState?.removeObservers();
liveCameraState = await cameraInfo!.getCameraState();
await liveCameraState!.observe(_createCameraClosingObserver(cameraId));
}
/// Creates [Observer] of the [CameraState] that will:
///
/// * Send a [CameraClosingEvent] if the [CameraState] indicates that the
/// camera has begun to close.
/// * Send a [CameraErrorEvent] if the [CameraState] indicates that the
/// camera is in error state.
Observer<CameraState> _createCameraClosingObserver(int cameraId) {
final WeakReference<AndroidCameraCameraX> weakThis =
WeakReference<AndroidCameraCameraX>(this);
// Callback method used to implement the behavior described above:
void onChanged(Object stateAsObject) {
// This cast is safe because the Observer implementation ensures
// the type of stateAsObject is the same as the observer this callback
// is attached to.
final CameraState state = stateAsObject as CameraState;
if (state.type == CameraStateType.closing) {
weakThis.target!.cameraEventStreamController
.add(CameraClosingEvent(cameraId));
}
if (state.error != null) {
weakThis.target!.cameraEventStreamController
.add(CameraErrorEvent(cameraId, state.error!.getDescription()));
}
}
// shouldCreateDetachedObjectForTesting is used to create an Observer
// detached from the native side only to test the logic of the Analyzer
// instance that will be used for image streaming.
return _shouldCreateDetachedObjectForTesting
? Observer<CameraState>.detached(onChanged: onChanged)
: Observer<CameraState>(onChanged: onChanged);
}
// Methods for mapping Flutter camera constants to CameraX constants:
/// Returns [CameraSelector] lens direction that maps to specified
/// [CameraLensDirection].
int _getCameraSelectorLensDirection(CameraLensDirection lensDirection) {
switch (lensDirection) {
case CameraLensDirection.front:
return CameraSelector.lensFacingFront;
case CameraLensDirection.back:
return CameraSelector.lensFacingBack;
case CameraLensDirection.external:
return CameraSelector.lensFacingExternal;
}
}
/// Returns [Surface] target rotation constant that maps to specified sensor
/// orientation.
int _getTargetRotation(int sensorOrientation) {
switch (sensorOrientation) {
case 90:
return Surface.ROTATION_90;
case 180:
return Surface.ROTATION_180;
case 270:
return Surface.ROTATION_270;
case 0:
return Surface.ROTATION_0;
default:
throw ArgumentError(
'"$sensorOrientation" is not a valid sensor orientation value');
}
}
/// Returns the [ResolutionSelector] that maps to the specified resolution
/// preset for camera [UseCase]s.
///
/// If the specified [preset] is unavailable, the camera will fall back to the
/// closest lower resolution available.
ResolutionSelector? _getResolutionSelectorFromPreset(
ResolutionPreset? preset) {
const int fallbackRule = ResolutionStrategy.fallbackRuleClosestLower;
Size? boundSize;
ResolutionStrategy? resolutionStrategy;
switch (preset) {
case ResolutionPreset.low:
boundSize = const Size(320, 240);
break;
case ResolutionPreset.medium:
boundSize = const Size(720, 480);
break;
case ResolutionPreset.high:
boundSize = const Size(1280, 720);
break;
case ResolutionPreset.veryHigh:
boundSize = const Size(1920, 1080);
break;
case ResolutionPreset.ultraHigh:
boundSize = const Size(3840, 2160);
break;
case ResolutionPreset.max:
// Automatically set strategy to choose highest available.
resolutionStrategy = _shouldCreateDetachedObjectForTesting
? ResolutionStrategy.detachedHighestAvailableStrategy()
: ResolutionStrategy.highestAvailableStrategy();
break;
case null:
// If no preset is specified, default to CameraX's default behavior
// for each UseCase.
return null;
}
if (_shouldCreateDetachedObjectForTesting) {
resolutionStrategy ??= ResolutionStrategy.detached(
boundSize: boundSize, fallbackRule: fallbackRule);
return ResolutionSelector.detached(
resolutionStrategy: resolutionStrategy);
}
resolutionStrategy ??=
ResolutionStrategy(boundSize: boundSize!, fallbackRule: fallbackRule);
return ResolutionSelector(
resolutionStrategy: ResolutionStrategy(
boundSize: boundSize!, fallbackRule: fallbackRule));
}
/// Returns the [QualitySelector] that maps to the specified resolution
/// preset for the camera used only for video capture.
///
/// If the specified [preset] is unavailable, the camera will fall back to the
/// closest lower resolution available.
QualitySelector? _getQualitySelectorFromPreset(ResolutionPreset? preset) {
VideoQuality? videoQuality;
switch (preset) {
case ResolutionPreset.low:
// 240p is not supported by CameraX.
case ResolutionPreset.medium:
videoQuality = VideoQuality.SD;
break;
case ResolutionPreset.high:
videoQuality = VideoQuality.HD;
break;
case ResolutionPreset.veryHigh:
videoQuality = VideoQuality.FHD;
break;
case ResolutionPreset.ultraHigh:
videoQuality = VideoQuality.UHD;
break;
case ResolutionPreset.max:
videoQuality = VideoQuality.highest;
break;
case null:
// If no preset is specified, default to CameraX's default behavior
// for each UseCase.
return null;
}
// We will choose the next highest video quality if the one desired
// is unavailable.
const VideoResolutionFallbackRule fallbackRule =
VideoResolutionFallbackRule.lowerQualityThan;
final FallbackStrategy fallbackStrategy =
_shouldCreateDetachedObjectForTesting
? FallbackStrategy.detached(
quality: videoQuality, fallbackRule: fallbackRule)
: FallbackStrategy(
quality: videoQuality, fallbackRule: fallbackRule);
return _shouldCreateDetachedObjectForTesting
? QualitySelector.detached(qualityList: <VideoQualityData>[
VideoQualityData(quality: videoQuality)
], fallbackStrategy: fallbackStrategy)
: QualitySelector.from(
quality: VideoQualityData(quality: videoQuality),
fallbackStrategy: fallbackStrategy);
}
// Methods for calls that need to be tested:
/// Requests camera permissions.
@visibleForTesting
Future<void> requestCameraPermissions(bool enableAudio) async {
await SystemServices.requestCameraPermissions(enableAudio);
}
/// Subscribes the plugin as a listener to changes in device orientation.
@visibleForTesting
void startListeningForDeviceOrientationChange(
bool cameraIsFrontFacing, int sensorOrientation) {
SystemServices.startListeningForDeviceOrientationChange(
cameraIsFrontFacing, sensorOrientation);
}
/// Returns a [CameraSelector] based on the specified camera lens direction.
@visibleForTesting
CameraSelector createCameraSelector(int cameraSelectorLensDirection) {
switch (cameraSelectorLensDirection) {
case CameraSelector.lensFacingFront:
return CameraSelector.getDefaultFrontCamera();
case CameraSelector.lensFacingBack:
return CameraSelector.getDefaultBackCamera();
default:
return CameraSelector(lensFacing: cameraSelectorLensDirection);
}
}
/// Returns a [Preview] configured with the specified target rotation and
/// specified [ResolutionSelector].
@visibleForTesting
Preview createPreview(
{required int targetRotation, ResolutionSelector? resolutionSelector}) {
return Preview(
targetRotation: targetRotation, resolutionSelector: resolutionSelector);
}
/// Returns an [ImageCapture] configured with specified flash mode and
/// the specified [ResolutionSelector].
@visibleForTesting
ImageCapture createImageCapture(ResolutionSelector? resolutionSelector) {
return ImageCapture(resolutionSelector: resolutionSelector);
}
/// Returns a [Recorder] for use in video capture configured with the
/// specified [QualitySelector].
@visibleForTesting
Recorder createRecorder(QualitySelector? qualitySelector) {
return Recorder(qualitySelector: qualitySelector);
}
/// Returns a [VideoCapture] associated with the provided [Recorder].
@visibleForTesting
Future<VideoCapture> createVideoCapture(Recorder recorder) async {
return VideoCapture.withOutput(recorder);
}
/// Returns an [ImageAnalysis] configured with the specified
/// [ResolutionSelector].
@visibleForTesting
ImageAnalysis createImageAnalysis(ResolutionSelector? resolutionSelector) {
return ImageAnalysis(resolutionSelector: resolutionSelector);
}
}