diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 06f41a68a146..9e6c5a901fc9 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -9,5 +9,6 @@ * Bump CameraX version to 1.3.0-alpha03 and Kotlin version to 1.8.0. * Changes instance manager to allow the separate creation of identical objects. * Adds Preview and Surface classes, along with other methods needed to implement camera preview. -* Adds implementation of availableCameras() +* Adds implementation of availableCameras(). +* Implements camera preview, createCamera, initializeCamera, onCameraError, onDeviceOrientationChanged, and onCameraInitialized. * Adds integration test to plugin. diff --git a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart new file mode 100644 index 000000000000..b1b5e9d4ceb9 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -0,0 +1,957 @@ +// 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 'dart:collection'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'camera_image.dart'; + +/// Signature for a callback receiving the a camera image. +/// +/// This is used by [CameraController.startImageStream]. +// TODO(stuartmorgan): Fix this naming the next time there's a breaking change +// to this package. +// ignore: camel_case_types +typedef onLatestImageAvailable = Function(CameraImage image); + +/// Completes with a list of available cameras. +/// +/// May throw a [CameraException]. +Future> availableCameras() async { + return CameraPlatform.instance.availableCameras(); +} + +// TODO(stuartmorgan): Remove this once the package requires 2.10, where the +// dart:async `unawaited` accepts a nullable future. +void _unawaited(Future? future) {} + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + required this.isInitialized, + this.errorDescription, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required bool isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }) : _isRecordingPaused = isRecordingPaused; + + /// Creates a new camera controller state for an uninitialized controller. + const CameraValue.uninitialized() + : this( + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: false, + focusMode: FocusMode.auto, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: false, + ); + + /// True after [CameraController.initialize] has completed successfully. + final bool isInitialized; + + /// True when a picture capture request has been sent but as not yet returned. + final bool isTakingPicture; + + /// True when the camera is recording (not the same as previewing). + final bool isRecordingVideo; + + /// True when images from the camera are being streamed. + final bool isStreamingImages; + + final bool _isRecordingPaused; + + /// True when the preview widget has been paused manually. + final bool isPreviewPaused; + + /// Set to the orientation the preview was paused in, if it is currently paused. + final DeviceOrientation? previewPauseOrientation; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + + /// Description of an error state. + /// + /// This is null while the controller is not in an error state. + /// When [hasError] is true this contains the error description. + final String? errorDescription; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// Convenience getter for `previewSize.width / previewSize.height`. + /// + /// Can only be called when [initialize] is done. + double get aspectRatio => previewSize!.width / previewSize!.height; + + /// Whether the controller is in an error state. + /// + /// When true [errorDescription] describes the error. + bool get hasError => errorDescription != null; + + /// The flash mode the camera is currently set to. + final FlashMode flashMode; + + /// The exposure mode the camera is currently set to. + final ExposureMode exposureMode; + + /// The focus mode the camera is currently set to. + final FocusMode focusMode; + + /// Whether setting the exposure point is supported. + final bool exposurePointSupported; + + /// Whether setting the focus point is supported. + final bool focusPointSupported; + + /// The current device UI orientation. + final DeviceOrientation deviceOrientation; + + /// The currently locked capture orientation. + final DeviceOrientation? lockedCaptureOrientation; + + /// Whether the capture orientation is currently locked. + bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + + /// The orientation of the currently running video recording. + final DeviceOrientation? recordingOrientation; + + /// Creates a modified copy of the object. + /// + /// Explicitly specified fields get the specified value, all other fields get + /// the same value of the current object. + CameraValue copyWith({ + bool? isInitialized, + bool? isRecordingVideo, + bool? isTakingPicture, + bool? isStreamingImages, + String? errorDescription, + Size? previewSize, + bool? isRecordingPaused, + FlashMode? flashMode, + ExposureMode? exposureMode, + FocusMode? focusMode, + bool? exposurePointSupported, + bool? focusPointSupported, + DeviceOrientation? deviceOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, + }) { + return CameraValue( + isInitialized: isInitialized ?? this.isInitialized, + errorDescription: errorDescription, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, + focusPointSupported: focusPointSupported ?? this.focusPointSupported, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, + ); + } + + @override + String toString() { + return '${objectRuntimeType(this, 'CameraValue')}(' + 'isRecordingVideo: $isRecordingVideo, ' + 'isInitialized: $isInitialized, ' + 'errorDescription: $errorDescription, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'exposurePointSupported: $exposurePointSupported, ' + 'focusPointSupported: $focusPointSupported, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// Controls a device camera. +/// +/// Use [availableCameras] to get a list of available cameras. +/// +/// Before using a [CameraController] a call to [initialize] must complete. +/// +/// To show the camera preview on the screen use a [CameraPreview] widget. +class CameraController extends ValueNotifier { + /// Creates a new camera controller in an uninitialized state. + CameraController( + this.description, + this.resolutionPreset, { + this.enableAudio = true, + this.imageFormatGroup, + }) : super(const CameraValue.uninitialized()); + + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + + /// The resolution this controller is targeting. + /// + /// This resolution preset is not guaranteed to be available on the device, + /// if unavailable a lower resolution will be used. + /// + /// See also: [ResolutionPreset]. + final ResolutionPreset resolutionPreset; + + /// Whether to include audio when recording a video. + final bool enableAudio; + + /// The [ImageFormatGroup] describes the output of the raw image format. + /// + /// When null the imageFormat will fallback to the platforms default. + final ImageFormatGroup? imageFormatGroup; + + /// The id of a camera that hasn't been initialized. + @visibleForTesting + static const int kUninitializedCameraId = -1; + int _cameraId = kUninitializedCameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? + _deviceOrientationSubscription; + + /// Checks whether [CameraController.dispose] has completed successfully. + /// + /// This is a no-op when asserts are disabled. + void debugCheckIsDisposed() { + assert(_isDisposed); + } + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + /// + /// Throws a [CameraException] if the initialization fails. + Future initialize() async { + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + 'initialize was called on a disposed CameraController', + ); + } + try { + final Completer initializeCompleter = + Completer(); + + _deviceOrientationSubscription = CameraPlatform.instance + .onDeviceOrientationChanged() + .listen((DeviceOrientationChangedEvent event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + + _cameraId = await CameraPlatform.instance.createCamera( + description, + resolutionPreset, + enableAudio: enableAudio, + ); + + _unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((CameraInitializedEvent event) { + initializeCompleter.complete(event); + })); + + await CameraPlatform.instance.initializeCamera( + _cameraId, + imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown, + ); + + value = value.copyWith( + isInitialized: true, + previewSize: await initializeCompleter.future + .then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.exposureMode), + focusMode: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.focusMode), + exposurePointSupported: await initializeCompleter.future.then( + (CameraInitializedEvent event) => event.exposurePointSupported), + focusPointSupported: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.focusPointSupported), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + /// + /// Use of this method is optional, but it may be called for performance + /// reasons on iOS. + /// + /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. + /// If video recording is intended, calling this early eliminates this delay + /// that would otherwise be experienced when video recording is started. + /// This operation is a no-op on Android and Web. + /// + /// Throws a [CameraException] if the prepare fails. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + _throwIfNotInitialized('takePicture'); + if (value.isTakingPicture) { + throw CameraException( + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ); + } + try { + value = value.copyWith(isTakingPicture: true); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } on PlatformException catch (e) { + value = value.copyWith(isTakingPicture: false); + throw CameraException(e.code, e.message); + } + } + + /// Start streaming images from platform camera. + /// + /// Settings for capturing images on iOS and Android is set to always use the + /// latest image available from the camera and will drop all other images. + /// + /// When running continuously with [CameraPreview] widget, this function runs + /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can + /// have significant frame rate drops for [CameraPreview] on lower end + /// devices. + /// + /// Throws a [CameraException] if image streaming or video recording has + /// already started. + /// + /// The `startImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + /// + // TODO(bmparr): Add settings for resolution and fps. + Future startImageStream(onLatestImageAvailable onAvailable) async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + _throwIfNotInitialized('startImageStream'); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ); + } + + try { + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }); + value = value.copyWith(isStreamingImages: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stop streaming images from platform camera. + /// + /// Throws a [CameraException] if image streaming was not started or video + /// recording was started. + /// + /// The `stopImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + Future stopImageStream() async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + _throwIfNotInitialized('stopImageStream'); + if (!value.isStreamingImages) { + throw CameraException( + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ); + } + + try { + value = value.copyWith(isStreamingImages: false); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Start a video recording. + /// + /// You may optionally pass an [onAvailable] callback to also have the + /// video frames streamed to this callback. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async { + _throwIfNotInitialized('startVideoRecording'); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ); + } + + Function(CameraImageData image)? streamCallback; + if (onAvailable != null) { + streamCallback = (CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }; + } + + try { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation), + isStreamingImages: onAvailable != null); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + _throwIfNotInitialized('stopVideoRecording'); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'stopVideoRecording was called when no video is recording.', + ); + } + + if (value.isStreamingImages) { + stopImageStream(); + } + + try { + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const Optional.absent(), + ); + return file; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + _throwIfNotInitialized('pauseVideoRecording'); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'pauseVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + _throwIfNotInitialized('resumeVideoRecording'); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'resumeVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + _throwIfNotInitialized('buildPreview'); + try { + return CameraPlatform.instance.buildPreview(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported zoom level for the selected camera. + Future getMaxZoomLevel() { + _throwIfNotInitialized('getMaxZoomLevel'); + try { + return CameraPlatform.instance.getMaxZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported zoom level for the selected camera. + Future getMinZoomLevel() { + _throwIfNotInitialized('getMinZoomLevel'); + try { + return CameraPlatform.instance.getMinZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between 1.0 and the maximum supported + /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException` + /// when an illegal zoom level is suplied. + Future setZoomLevel(double zoom) { + _throwIfNotInitialized('setZoomLevel'); + try { + return CameraPlatform.instance.setZoomLevel(_cameraId, zoom); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + try { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + try { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure point for automatically determining the exposure value. + /// + /// Supplying a `null` value will reset the exposure point to it's default + /// value. + Future setExposurePoint(Offset? point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + + try { + await CameraPlatform.instance.setExposurePoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset() async { + _throwIfNotInitialized('getMinExposureOffset'); + try { + return CameraPlatform.instance.getMinExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset() async { + _throwIfNotInitialized('getMaxExposureOffset'); + try { + return CameraPlatform.instance.getMaxExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize() async { + _throwIfNotInitialized('getExposureOffsetStepSize'); + try { + return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(double offset) async { + _throwIfNotInitialized('setExposureOffset'); + // Check if offset is in range + final List range = await Future.wait( + >[getMinExposureOffset(), getMaxExposureOffset()]); + if (offset < range[0] || offset > range[1]) { + throw CameraException( + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ); + } + + // Round to the closest step if needed + final double stepSize = await getExposureOffsetStepSize(); + if (stepSize > 0) { + final double inv = 1.0 / stepSize; + double roundedOffset = (offset * inv).roundToDouble() / inv; + if (roundedOffset > range[1]) { + roundedOffset = (offset * inv).floorToDouble() / inv; + } else if (roundedOffset < range[0]) { + roundedOffset = (offset * inv).ceilToDouble() / inv; + } + offset = roundedOffset; + } + + try { + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation? orientation]) async { + try { + await CameraPlatform.instance.lockCaptureOrientation( + _cameraId, orientation ?? value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: Optional.of( + orientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(FocusMode mode) async { + try { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + try { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus point for automatically determining the focus value. + /// + /// Supplying a `null` value will reset the focus point to it's default + /// value. + Future setFocusPoint(Offset? point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + try { + await CameraPlatform.instance.setFocusPoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _unawaited(_deviceOrientationSubscription?.cancel()); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + void _throwIfNotInitialized(String functionName) { + if (!value.isInitialized) { + throw CameraException( + 'Uninitialized CameraController', + '$functionName() was called on an uninitialized CameraController.', + ); + } + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + '$functionName() was called on a disposed CameraController.', + ); + } + } + + @override + void removeListener(VoidCallback listener) { + // Prevent ValueListenableBuilder in CameraPreview widget from causing an + // exception to be thrown by attempting to remove its own listener after + // the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } +} + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/camera_image.dart b/packages/camera/camera_android_camerax/example/lib/camera_image.dart new file mode 100644 index 000000000000..bfcad6626dd6 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_image.dart @@ -0,0 +1,177 @@ +// 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. + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +// TODO(stuartmorgan): Remove all of these classes in a breaking change, and +// vend the platform interface versions directly. See +// https://github.com/flutter/flutter/issues/104188 + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by the +/// format of the Image. +class Plane { + Plane._fromPlatformInterface(CameraImagePlane plane) + : bytes = plane.bytes, + bytesPerPixel = plane.bytesPerPixel, + bytesPerRow = plane.bytesPerRow, + height = plane.height, + width = plane.width; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. + Plane._fromPlatformData(Map data) + : bytes = data['bytes'] as Uint8List, + bytesPerPixel = data['bytesPerPixel'] as int?, + bytesPerRow = data['bytesPerRow'] as int, + height = data['height'] as int?, + width = data['width'] as int?; + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The distance between adjacent pixel samples on Android, in bytes. + /// + /// Will be `null` on iOS. + final int? bytesPerPixel; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// Height of the pixel buffer on iOS. + /// + /// Will be `null` on Android + final int? height; + + /// Width of the pixel buffer on iOS. + /// + /// Will be `null` on Android. + final int? width; +} + +/// Describes how pixels are represented in an image. +class ImageFormat { + ImageFormat._fromPlatformInterface(CameraImageFormat format) + : group = format.group, + raw = format.raw; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. + ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the Android or iOS platform. + /// + /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. + /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc + final dynamic raw; +} + +// Only used by the deprecated codepath that's kept to avoid breaking changes. +// Never called by the plugin itself. +ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { + if (defaultTargetPlatform == TargetPlatform.android) { + switch (rawFormat) { + // android.graphics.ImageFormat.YUV_420_888 + case 35: + return ImageFormatGroup.yuv420; + // android.graphics.ImageFormat.JPEG + case 256: + return ImageFormatGroup.jpeg; + } + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (rawFormat) { + // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + case 875704438: + return ImageFormatGroup.yuv420; + // kCVPixelFormatType_32BGRA + case 1111970369: + return ImageFormatGroup.bgra8888; + } + } + + return ImageFormatGroup.unknown; +} + +/// A single complete image buffer from the platform camera. +/// +/// This class allows for direct application access to the pixel data of an +/// Image through one or more [Uint8List]. Each buffer is encapsulated in a +/// [Plane] that describes the layout of the pixel data in that plane. The +/// [CameraImage] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on iOS, we treat 1-dimensional +/// images as single planar images. +class CameraImage { + /// Creates a [CameraImage] from the platform interface version. + CameraImage.fromPlatformInterface(CameraImageData data) + : format = ImageFormat._fromPlatformInterface(data.format), + height = data.height, + width = data.width, + planes = List.unmodifiable(data.planes.map( + (CameraImagePlane plane) => Plane._fromPlatformInterface(plane))), + lensAperture = data.lensAperture, + sensorExposureTime = data.sensorExposureTime, + sensorSensitivity = data.sensorSensitivity; + + /// Creates a [CameraImage] from method channel data. + @Deprecated('Use fromPlatformInterface instead') + CameraImage.fromPlatformData(Map data) + : format = ImageFormat._fromPlatformData(data['format']), + height = data['height'] as int, + width = data['width'] as int, + lensAperture = data['lensAperture'] as double?, + sensorExposureTime = data['sensorExposureTime'] as int?, + sensorSensitivity = data['sensorSensitivity'] as double?, + planes = List.unmodifiable((data['planes'] as List) + .map((dynamic planeData) => + Plane._fromPlatformData(planeData as Map))); + + /// Format of the image provided. + /// + /// Determines the number of planes needed to represent the image, and + /// the general layout of the pixel data in each [Uint8List]. + final ImageFormat format; + + /// Height of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the height + /// of the largest-resolution plane. + final int height; + + /// Width of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the width + /// of the largest-resolution plane. + final int width; + + /// The pixels planes for this image. + /// + /// The number of planes is determined by the format of the image. + final List planes; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; +} diff --git a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart new file mode 100644 index 000000000000..3baaaf8b1fa1 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -0,0 +1,81 @@ +// 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 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'camera_controller.dart'; + +/// A widget showing a live camera preview. +class CameraPreview extends StatelessWidget { + /// Creates a preview widget for the given camera controller. + const CameraPreview(this.controller, {super.key, this.child}); + + /// The controller for the camera that the preview is shown for. + final CameraController controller; + + /// A widget to overlay on top of the camera preview + final Widget? child; + + @override + Widget build(BuildContext context) { + return controller.value.isInitialized + ? ValueListenableBuilder( + valueListenable: controller, + builder: (BuildContext context, Object? value, Widget? child) { + return AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + _wrapInRotatedBox(child: controller.buildPreview()), + child ?? Container(), + ], + ), + ); + }, + child: child, + ) + : Container(); + } + + Widget _wrapInRotatedBox({required Widget child}) { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + return child; + } + + return RotatedBox( + quarterTurns: _getQuarterTurns(), + child: child, + ); + } + + bool _isLandscape() { + return [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight + ].contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + final Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeRight: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeLeft: 3, + }; + return turns[_getApplicableOrientation()]!; + } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation! + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 244a15281e3f..4fd965271baa 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -2,43 +2,1046 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:io'; + import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:video_player/video_player.dart'; -late List _cameras; +import 'camera_controller.dart'; +import 'camera_preview.dart'; -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - _cameras = await CameraPlatform.instance.availableCameras(); +/// Camera example home widget. +class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({super.key}); - runApp(const MyApp()); + @override + State createState() { + return _CameraExampleHomeState(); + } } -/// Example app -class MyApp extends StatefulWidget { - /// App instantiation - const MyApp({super.key}); - @override - State createState() => _MyAppState(); +/// Returns a suitable camera icon for [direction]. +IconData getCameraLensIcon(CameraLensDirection direction) { + switch (direction) { + case CameraLensDirection.back: + return Icons.camera_rear; + case CameraLensDirection.front: + return Icons.camera_front; + case CameraLensDirection.external: + return Icons.camera; + } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; +} + +void _logError(String code, String? message) { + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } -class _MyAppState extends State { +class _CameraExampleHomeState extends State + with WidgetsBindingObserver, TickerProviderStateMixin { + CameraController? controller; + XFile? imageFile; + XFile? videoFile; + VideoPlayerController? videoController; + VoidCallback? videoPlayerListener; + bool enableAudio = true; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + late AnimationController _flashModeControlRowAnimationController; + late Animation _flashModeControlRowAnimation; + late AnimationController _exposureModeControlRowAnimationController; + late Animation _exposureModeControlRowAnimation; + late AnimationController _focusModeControlRowAnimationController; + late Animation _focusModeControlRowAnimation; + double _minAvailableZoom = 1.0; + double _maxAvailableZoom = 1.0; + double _currentScale = 1.0; + double _baseScale = 1.0; + + // Counting pointers (number of user fingers on screen) + int _pointers = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _focusModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _focusModeControlRowAnimation = CurvedAnimation( + parent: _focusModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + // #docregion AppLifecycle + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + onNewCameraSelected(cameraController.description); + } + } + // #enddocregion AppLifecycle + @override Widget build(BuildContext context) { - String availableCameraNames = 'Available cameras:'; - for (final CameraDescription cameraDescription in _cameras) { - availableCameraNames = '$availableCameraNames ${cameraDescription.name},'; + return Scaffold( + appBar: AppBar( + title: const Text('Camera example'), + ), + body: Column( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.black, + border: Border.all( + color: + controller != null && controller!.value.isRecordingVideo + ? Colors.redAccent + : Colors.grey, + width: 3.0, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), + ), + ), + _captureControlRowWidget(), + _modeControlRowWidget(), + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + _cameraTogglesRowWidget(), + _thumbnailWidget(), + ], + ), + ), + ], + ), + ); + } + + /// Display the preview from the camera (or a message if the preview is not available). + Widget _cameraPreviewWidget() { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + return const Text( + 'Tap a camera', + style: TextStyle( + color: Colors.white, + fontSize: 24.0, + fontWeight: FontWeight.w900, + ), + ); + } else { + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + controller!, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (TapDownDetails details) => + onViewFinderTap(details, constraints), + ); + }), + ), + ); + } + } + + void _handleScaleStart(ScaleStartDetails details) { + _baseScale = _currentScale; + } + + Future _handleScaleUpdate(ScaleUpdateDetails details) async { + // When there are not exactly two fingers on screen don't scale + if (controller == null || _pointers != 2) { + return; } - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Camera Example'), + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await controller!.setZoomLevel(_currentScale); + } + + /// Display the thumbnail of the captured image or video. + Widget _thumbnailWidget() { + final VideoPlayerController? localVideoController = videoController; + + return Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (localVideoController == null && imageFile == null) + Container() + else + SizedBox( + width: 64.0, + height: 64.0, + child: (localVideoController == null) + ? ( + // The captured image on the web contains a network-accessible URL + // pointing to a location within the browser. It may be displayed + // either with Image.network or Image.memory after loading the image + // bytes to memory. + kIsWeb + ? Image.network(imageFile!.path) + : Image.file(File(imageFile!.path))) + : Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), + child: Center( + child: AspectRatio( + aspectRatio: + localVideoController.value.size != null + ? localVideoController.value.aspectRatio + : 1.0, + child: VideoPlayer(localVideoController)), + ), + ), + ), + ], + ), + ), + ); + } + + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_on), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + // The exposure and focus mode are currently not supported on the web. + ...!kIsWeb + ? [ + IconButton( + icon: const Icon(Icons.exposure), + color: Colors.blue, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + ) + ] + : [], + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + ], ), - body: Center( - child: Text(availableCameraNames.substring( - 0, availableCameraNames.length - 1)), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + ], ), ), ); } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _exposureModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + const Center( + child: Text('Exposure Mode'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + style: styleAuto, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + controller!.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + } + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('LOCKED'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('RESET OFFSET'), + ), + ], + ), + const Center( + child: Text('Exposure Offset'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.focusMode == FocusMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.focusMode == FocusMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _focusModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + const Center( + child: Text('Focus Mode'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + style: styleAuto, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + controller!.setFocusPoint(null); + } + showInSnackBar('Resetting focus point'); + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('LOCKED'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Display the control bar with buttons to take pictures and record videos. + Widget _captureControlRowWidget() { + final CameraController? cameraController = controller; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.camera_alt), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.videocam), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: cameraController != null && + cameraController.value.isRecordingPaused + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.stop), + color: Colors.red, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), + ], + ); + } + + /// Display a row of toggle to select the camera (or a message if no camera is available). + Widget _cameraTogglesRowWidget() { + final List toggles = []; + + void onChanged(CameraDescription? description) { + if (description == null) { + return; + } + + onNewCameraSelected(description); + } + + if (_cameras.isEmpty) { + SchedulerBinding.instance.addPostFrameCallback((_) async { + showInSnackBar('No camera found.'); + }); + return const Text('None'); + } else { + for (final CameraDescription cameraDescription in _cameras) { + toggles.add( + SizedBox( + width: 90.0, + child: RadioListTile( + title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), + groupValue: controller?.description, + value: cameraDescription, + onChanged: + controller != null && controller!.value.isRecordingVideo + ? null + : onChanged, + ), + ), + ); + } + } + + return Row(children: toggles); + } + + String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); + + void showInSnackBar(String message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message))); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + if (controller == null) { + return; + } + + final CameraController cameraController = controller!; + + final Offset offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); + } + + Future onNewCameraSelected(CameraDescription cameraDescription) async { + final CameraController? oldController = controller; + if (oldController != null) { + // `controller` needs to be set to null before getting disposed, + // to avoid a race condition when we use the controller that is being + // disposed. This happens when camera permission dialog shows up, + // which triggers `didChangeAppLifecycleState`, which disposes and + // re-creates the controller. + controller = null; + await oldController.dispose(); + } + + final CameraController cameraController = CameraController( + cameraDescription, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, + enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) { + setState(() {}); + } + if (cameraController.value.hasError) { + showInSnackBar( + 'Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + await Future.wait(>[ + // The exposure mode is currently not supported on the web. + ...!kIsWeb + ? >[ + cameraController.getMinExposureOffset().then( + (double value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((double value) => _maxAvailableExposureOffset = value) + ] + : >[], + cameraController + .getMaxZoomLevel() + .then((double value) => _maxAvailableZoom = value), + cameraController + .getMinZoomLevel() + .then((double value) => _minAvailableZoom = value), + ]); + } on CameraException catch (e) { + switch (e.code) { + case 'CameraAccessDenied': + showInSnackBar('You have denied camera access.'); + break; + case 'CameraAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable camera access.'); + break; + case 'CameraAccessRestricted': + // iOS only + showInSnackBar('Camera access is restricted.'); + break; + case 'AudioAccessDenied': + showInSnackBar('You have denied audio access.'); + break; + case 'AudioAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable audio access.'); + break; + case 'AudioAccessRestricted': + // iOS only + showInSnackBar('Audio access is restricted.'); + break; + default: + _showCameraException(e); + break; + } + } + + if (mounted) { + setState(() {}); + } + } + + void onTakePictureButtonPressed() { + takePicture().then((XFile? file) { + if (mounted) { + setState(() { + imageFile = file; + videoController?.dispose(); + videoController = null; + }); + if (file != null) { + showInSnackBar('Picture saved to ${file.path}'); + } + } + }); + } + + void onFlashModeButtonPressed() { + if (_flashModeControlRowAnimationController.value == 1) { + _flashModeControlRowAnimationController.reverse(); + } else { + _flashModeControlRowAnimationController.forward(); + _exposureModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onExposureModeButtonPressed() { + if (_exposureModeControlRowAnimationController.value == 1) { + _exposureModeControlRowAnimationController.reverse(); + } else { + _exposureModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onFocusModeButtonPressed() { + if (_focusModeControlRowAnimationController.value == 1) { + _focusModeControlRowAnimationController.reverse(); + } else { + _focusModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _exposureModeControlRowAnimationController.reverse(); + } + } + + void onAudioModeButtonPressed() { + enableAudio = !enableAudio; + if (controller != null) { + onNewCameraSelected(controller!.description); + } + } + + Future onCaptureOrientationLockButtonPressed() async { + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } on CameraException catch (e) { + _showCameraException(e); + } + } + + void onSetFlashModeButtonPressed(FlashMode mode) { + setFlashMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetExposureModeButtonPressed(ExposureMode mode) { + setExposureMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetFocusModeButtonPressed(FocusMode mode) { + setFocusMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); + }); + } + + void onVideoRecordButtonPressed() { + startVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + }); + } + + void onStopButtonPressed() { + stopVideoRecording().then((XFile? file) { + if (mounted) { + setState(() {}); + } + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } + }); + } + + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) { + setState(() {}); + } + } + + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Video recording resumed'); + }); + } + + Future startVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isRecordingVideo) { + // A recording is already started, do nothing. + return; + } + + try { + await cameraController.startVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + return cameraController.stopVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future pauseVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return; + } + + try { + await cameraController.pauseVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future resumeVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return; + } + + try { + await cameraController.resumeVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFlashMode(FlashMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFlashMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureMode(ExposureMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setExposureMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureOffset(double offset) async { + if (controller == null) { + return; + } + + setState(() { + _currentExposureOffset = offset; + }); + try { + offset = await controller!.setExposureOffset(offset); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFocusMode(FocusMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFocusMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future _startVideoPlayer() async { + if (videoFile == null) { + return; + } + + final VideoPlayerController vController = kIsWeb + ? VideoPlayerController.network(videoFile!.path) + : VideoPlayerController.file(File(videoFile!.path)); + + videoPlayerListener = () { + if (videoController != null && videoController!.value.size != null) { + // Refreshing the state to update video player with the correct ratio. + if (mounted) { + setState(() {}); + } + videoController!.removeListener(videoPlayerListener!); + } + }; + vController.addListener(videoPlayerListener!); + await vController.setLooping(true); + await vController.initialize(); + await videoController?.dispose(); + if (mounted) { + setState(() { + imageFile = null; + videoController = vController; + }); + } + await vController.play(); + } + + Future takePicture() async { + final CameraController? cameraController = controller; + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return null; + } + + if (cameraController.value.isTakingPicture) { + // A capture is already pending, do nothing. + return null; + } + + try { + final XFile file = await cameraController.takePicture(); + return file; + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(CameraException e) { + _logError(e.code, e.description); + showInSnackBar('Error: ${e.code}\n${e.description}'); + } +} + +/// CameraApp is the Main Application. +class CameraApp extends StatelessWidget { + /// Default Constructor + const CameraApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: CameraExampleHome(), + ); + } +} + +List _cameras = []; + +Future main() async { + // Fetch the available cameras before initializing the app. + try { + WidgetsFlutterBinding.ensureInitialized(); + _cameras = await availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp()); } diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index d9756f7ebd9b..49a29b8517d9 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: camera_platform_interface: ^2.2.0 flutter: sdk: flutter + video_player: ^2.4.10 dev_dependencies: flutter_test: diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 300d6717fb46..18debf688547 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -5,11 +5,18 @@ import 'dart:async'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; +import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; +import 'camerax_library.g.dart'; +import 'preview.dart'; import 'process_camera_provider.dart'; +import 'surface.dart'; +import 'system_services.dart'; +import 'use_case.dart'; /// The Android implementation of [CameraPlatform] that uses the CameraX library. class AndroidCameraCameraX extends CameraPlatform { @@ -18,17 +25,45 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } - /// ProcessCameraProvider used to get list of cameras. Visible only for testing. + /// The [ProcessCameraProvider] instance used to access camera functionality. @visibleForTesting ProcessCameraProvider? processCameraProvider; - /// Camera selector used to determine which CameraInfos are back cameras. + /// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is + /// bound to the lifecycle of the camera it manages. @visibleForTesting - CameraSelector? backCameraSelector = CameraSelector.getDefaultBackCamera(); + Camera? camera; - /// Camera selector used to determine which CameraInfos are back cameras. + /// The [Preview] instance that can be configured to present a live camera preview. @visibleForTesting - CameraSelector? frontCameraSelector = CameraSelector.getDefaultFrontCamera(); + Preview? preview; + + /// Whether or not the [preview] is currently bound to the lifecycle that the + /// [processCameraProvider] tracks. + @visibleForTesting + bool previewIsBound = false; + + bool _previewIsPaused = false; + + /// 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 cameraEventStreamController = + StreamController.broadcast(); + + /// The stream of camera events. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((CameraEvent event) => event.cameraId == cameraId); /// Returns list of all available cameras and their descriptions. @override @@ -47,10 +82,12 @@ class AndroidCameraCameraX extends CameraPlatform { 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 backCameraSelector!.filter([cameraInfo])) + if ((await createCameraSelector(CameraSelector.lensFacingBack) + .filter([cameraInfo])) .isNotEmpty) { cameraLensDirection = CameraLensDirection.back; - } else if ((await frontCameraSelector!.filter([cameraInfo])) + } else if ((await createCameraSelector(CameraSelector.lensFacingFront) + .filter([cameraInfo])) .isNotEmpty) { cameraLensDirection = CameraLensDirection.front; } else { @@ -70,4 +107,276 @@ class AndroidCameraCameraX extends CameraPlatform { 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 + /// unitialized 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 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); + + // Retrieve a ProcessCameraProvider instance. + processCameraProvider ??= await ProcessCameraProvider.getInstance(); + + // Configure Preview instance and bind to ProcessCameraProvider. + final int targetRotation = + _getTargetRotation(cameraDescription.sensorOrientation); + final ResolutionInfo? targetResolution = + _getTargetResolutionForPreview(resolutionPreset); + preview = createPreview(targetRotation, targetResolution); + previewIsBound = false; + _previewIsPaused = false; + final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(); + + 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 formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to + /// the image stream. + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + // TODO(camsim99): Use imageFormatGroup to configure ImageAnalysis use case + // for image streaming. + // https://github.com/flutter/flutter/issues/120463 + + // Configure CameraInitializedEvent to send as representation of a + // configured camera: + // Retrieve preview resolution. + assert( + preview != null, + 'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"', + ); + await _bindPreviewToLifecycle(); + final ResolutionInfo previewResolutionInfo = + await preview!.getResolutionInfo(); + _unbindPreviewFromLifecycle(); + + // 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 dispose(int cameraId) async { + preview?.releaseFlutterSurfaceTexture(); + processCameraProvider?.unbindAll(); + } + + /// The camera has been initialized. + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + /// The camera experienced an error. + @override + Stream onCameraError(int cameraId) { + return SystemServices.cameraErrorStreamController.stream + .map((String errorDescription) { + return CameraErrorEvent(cameraId, errorDescription); + }); + } + + /// The ui orientation changed. + @override + Stream onDeviceOrientationChanged() { + return SystemServices.deviceOrientationChangedStreamController.stream; + } + + /// Pause the active preview on the current frame for the selected camera. + /// + /// [cameraId] not used. + @override + Future pausePreview(int cameraId) async { + _unbindPreviewFromLifecycle(); + _previewIsPaused = true; + } + + /// Resume the paused preview for the selected camera. + /// + /// [cameraId] not used. + @override + Future resumePreview(int cameraId) async { + await _bindPreviewToLifecycle(); + _previewIsPaused = false; + } + + /// Returns a widget showing a live camera preview. + @override + Widget buildPreview(int cameraId) { + return FutureBuilder( + future: _bindPreviewToLifecycle(), + builder: (BuildContext context, AsyncSnapshot 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); + } + }); + } + + // 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]. + Future _bindPreviewToLifecycle() async { + assert(processCameraProvider != null); + assert(cameraSelector != null); + + if (previewIsBound || _previewIsPaused) { + // Only bind if preview is not already bound or intentionally paused. + return; + } + + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [preview!]); + previewIsBound = true; + } + + /// Unbinds [preview] instance to camera lifecycle controlled by the + /// [processCameraProvider]. + void _unbindPreviewFromLifecycle() { + if (preview == null || !previewIsBound) { + return; + } + + assert(processCameraProvider != null); + + processCameraProvider!.unbind([preview!]); + previewIsBound = false; + } + + // 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 [ResolutionInfo] that maps to the specified resolution preset for + /// a camera preview. + ResolutionInfo? _getTargetResolutionForPreview(ResolutionPreset? resolution) { + // TODO(camsim99): Implement resolution configuration. + // https://github.com/flutter/flutter/issues/120462 + return null; + } + + // Methods for calls that need to be tested: + + /// Requests camera permissions. + @visibleForTesting + Future 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 + /// resolution. + @visibleForTesting + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return Preview( + targetRotation: targetRotation, targetResolution: targetResolution); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart index 43a1dabd6906..f1d3c5fdb663 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -44,10 +44,24 @@ class CameraSelector extends JavaObject { late final CameraSelectorHostApiImpl _api; /// ID for front facing lens. - static const int LENS_FACING_FRONT = 0; + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_FRONT(). + static const int lensFacingFront = 0; /// ID for back facing lens. - static const int LENS_FACING_BACK = 1; + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_BACK(). + static const int lensFacingBack = 1; + + /// ID for external lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_EXTERNAL(). + static const int lensFacingExternal = 2; + + /// ID for unknown lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_UNKNOWN(). + static const int lensFacingUnknown = -1; /// Selector for default front facing camera. static CameraSelector getDefaultFrontCamera({ @@ -57,7 +71,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_FRONT, + lensFacing: lensFacingFront, ); } @@ -69,7 +83,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_BACK, + lensFacing: lensFacingBack, ); } diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart index 4ca90e257a95..e108b6140bed 100644 --- a/packages/camera/camera_android_camerax/lib/src/system_services.dart +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -142,8 +142,6 @@ class SystemServicesFlutterApiImpl implements SystemServicesFlutterApi { /// Callback method for any errors caused by camera usage on the Java side. @override void onCameraError(String errorDescription) { - // TODO(camsim99): Use this to implement onCameraError method in plugin. - // See https://github.com/flutter/flutter/issues/119571 for context. SystemServices.cameraErrorStreamController.add(errorDescription); } } diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 45d5a2c66abd..f1496c640497 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -26,8 +26,9 @@ dependencies: stream_transform: ^2.1.0 dev_dependencies: + async: ^2.5.0 build_runner: ^2.1.4 flutter_test: sdk: flutter - mockito: ^5.1.0 + mockito: ^5.3.2 pigeon: ^3.2.6 diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 8b16c64c6ede..acfaf16b9ac4 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -2,28 +2,43 @@ // 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_android_camerax/camera_android_camerax.dart'; +import 'package:camera_android_camerax/src/camera.dart'; import 'package:camera_android_camerax/src/camera_info.dart'; import 'package:camera_android_camerax/src/camera_selector.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/preview.dart'; import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:camera_android_camerax/src/system_services.dart'; +import 'package:camera_android_camerax/src/use_case.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart' show DeviceOrientation; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'android_camera_camerax_test.mocks.dart'; -@GenerateMocks([ - ProcessCameraProvider, - CameraSelector, - CameraInfo, +@GenerateNiceMocks(>[ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), ]) +@GenerateMocks([BuildContext]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('Should fetch CameraDescription instances for available cameras', () async { // Arrange + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); final List returnData = [ { 'name': 'Camera 0', @@ -37,31 +52,24 @@ void main() { } ]; - //Create mocks to use - final MockProcessCameraProvider mockProcessCameraProvider = - MockProcessCameraProvider(); - final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); - final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + // Create mocks to use final MockCameraInfo mockFrontCameraInfo = MockCameraInfo(); final MockCameraInfo mockBackCameraInfo = MockCameraInfo(); - AndroidCameraCameraX.registerWith(); - - //Set class level ProcessCameraProvider and camera selectors to created mocks - final AndroidCameraCameraX androidCameraCamerax = AndroidCameraCameraX(); - androidCameraCamerax.backCameraSelector = mockBackCameraSelector; - androidCameraCamerax.frontCameraSelector = mockFrontCameraSelector; - androidCameraCamerax.processCameraProvider = mockProcessCameraProvider; - //Mock calls to native platform - when(mockProcessCameraProvider.getAvailableCameraInfos()).thenAnswer( + // Mock calls to native platform + when(camera.processCameraProvider!.getAvailableCameraInfos()).thenAnswer( (_) async => [mockBackCameraInfo, mockFrontCameraInfo]); - when(mockBackCameraSelector.filter([mockFrontCameraInfo])) + when(camera.mockBackCameraSelector + .filter([mockFrontCameraInfo])) .thenAnswer((_) async => []); - when(mockBackCameraSelector.filter([mockBackCameraInfo])) + when(camera.mockBackCameraSelector + .filter([mockBackCameraInfo])) .thenAnswer((_) async => [mockBackCameraInfo]); - when(mockFrontCameraSelector.filter([mockBackCameraInfo])) + when(camera.mockFrontCameraSelector + .filter([mockBackCameraInfo])) .thenAnswer((_) async => []); - when(mockFrontCameraSelector.filter([mockFrontCameraInfo])) + when(camera.mockFrontCameraSelector + .filter([mockFrontCameraInfo])) .thenAnswer((_) async => [mockFrontCameraInfo]); when(mockBackCameraInfo.getSensorRotationDegrees()) .thenAnswer((_) async => 0); @@ -69,7 +77,7 @@ void main() { .thenAnswer((_) async => 90); final List cameraDescriptions = - await androidCameraCamerax.availableCameras(); + await camera.availableCameras(); expect(cameraDescriptions.length, returnData.length); for (int i = 0; i < returnData.length; i++) { @@ -85,4 +93,313 @@ void main() { expect(cameraDescriptions[i], cameraDescription); } }); + + test( + 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID', + () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + const int testSurfaceTextureId = 6; + + when(camera.testPreview.setSurfaceProvider()) + .thenAnswer((_) async => testSurfaceTextureId); + + expect( + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio), + equals(testSurfaceTextureId)); + + // Verify permissions are requested and the camera starts listening for device orientation changes. + expect(camera.cameraPermissionsRequested, isTrue); + expect(camera.startedListeningForDeviceOrientationChanges, isTrue); + + // Verify CameraSelector is set with appropriate lens direction. + expect(camera.cameraSelector, equals(camera.mockBackCameraSelector)); + + // Verify the camera's Preview instance is instantiated properly. + expect(camera.preview, equals(camera.testPreview)); + + // Verify the camera's Preview instance has its surface provider set. + verify(camera.preview!.setSurfaceProvider()); + }); + + test( + 'initializeCamera throws AssertionError when createCamera has not been called before initializedCamera', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + expect(() => camera.initializeCamera(3), throwsAssertionError); + }); + + test('initializeCamera sends expected CameraInitializedEvent', () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + const int cameraId = 10; + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + const int resolutionWidth = 350; + const int resolutionHeight = 750; + final Camera mockCamera = MockCamera(); + final ResolutionInfo testResolutionInfo = + ResolutionInfo(width: resolutionWidth, height: resolutionHeight); + + // TODO(camsim99): Modify this when camera configuration is supported and + // defualt values no longer being used. + // https://github.com/flutter/flutter/issues/120468 + // https://github.com/flutter/flutter/issues/120467 + final CameraInitializedEvent testCameraInitializedEvent = + CameraInitializedEvent( + cameraId, + resolutionWidth.toDouble(), + resolutionHeight.toDouble(), + ExposureMode.auto, + false, + FocusMode.auto, + false); + + // Call createCamera. + when(camera.testPreview.setSurfaceProvider()) + .thenAnswer((_) async => cameraId); + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio); + + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])) + .thenAnswer((_) async => mockCamera); + when(camera.testPreview.getResolutionInfo()) + .thenAnswer((_) async => testResolutionInfo); + + // Start listening to camera events stream to verify the proper CameraInitializedEvent is sent. + camera.cameraEventStreamController.stream.listen((CameraEvent event) { + expect(event, const TypeMatcher()); + expect(event, equals(testCameraInitializedEvent)); + }); + + await camera.initializeCamera(cameraId); + + // Verify preview was bound and unbound to get preview resolution information. + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])); + verify(camera.processCameraProvider!.unbind([camera.testPreview])); + + // Check camera instance was received, but preview is no longer bound. + expect(camera.camera, equals(mockCamera)); + expect(camera.previewIsBound, isFalse); + }); + + test('dispose releases Flutter surface texture and unbinds all use cases', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.preview = MockPreview(); + camera.processCameraProvider = MockProcessCameraProvider(); + + camera.dispose(3); + + verify(camera.preview!.releaseFlutterSurfaceTexture()); + verify(camera.processCameraProvider!.unbindAll()); + }); + + test('onCameraInitialized stream emits CameraInitializedEvents', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 16; + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + const CameraInitializedEvent testEvent = CameraInitializedEvent( + cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false); + + camera.cameraEventStreamController.add(testEvent); + + expect(await streamQueue.next, testEvent); + await streamQueue.cancel(); + }); + + test('onCameraError stream emits errors caught by system services', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 27; + const String testErrorDescription = 'Test error description!'; + final Stream eventStream = camera.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + SystemServices.cameraErrorStreamController.add(testErrorDescription); + + expect(await streamQueue.next, + equals(const CameraErrorEvent(cameraId, testErrorDescription))); + await streamQueue.cancel(); + }); + + test( + 'onDeviceOrientationChanged stream emits changes in device oreintation detected by system services', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final Stream eventStream = + camera.onDeviceOrientationChanged(); + final StreamQueue streamQueue = + StreamQueue(eventStream); + const DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitDown); + + SystemServices.deviceOrientationChangedStreamController.add(testEvent); + + expect(await streamQueue.next, testEvent); + await streamQueue.cancel(); + }); + + test( + 'pausePreview unbinds preview from lifecycle when preview is nonnull and has been bound to lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.preview = MockPreview(); + camera.previewIsBound = true; + + await camera.pausePreview(579); + + verify(camera.processCameraProvider!.unbind([camera.preview!])); + expect(camera.previewIsBound, isFalse); + }); + + test( + 'pausePreview does not unbind preview from lifecycle when preview has not been bound to lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.preview = MockPreview(); + + await camera.pausePreview(632); + + verifyNever( + camera.processCameraProvider!.unbind([camera.preview!])); + }); + + test('resumePreview does not bind preview to lifecycle if already bound', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + camera.previewIsBound = true; + + await camera.resumePreview(78); + + verifyNever(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + }); + + test('resumePreview binds preview to lifecycle if not already bound', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + await camera.resumePreview(78); + + verify(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + }); + + test( + 'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + final FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.nothing()), + isA()); + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.waiting()), + isA()); + expect( + previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.active, null)), + isA()); + }); + + test( + 'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + final FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + final Texture previewTexture = previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.done, null)) + as Texture; + expect(previewTexture.textureId, equals(textureId)); + }); +} + +/// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for +/// testing. +class MockAndroidCameraCamerax extends AndroidCameraCameraX { + bool cameraPermissionsRequested = false; + bool startedListeningForDeviceOrientationChanges = false; + final MockPreview testPreview = MockPreview(); + final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + + @override + Future requestCameraPermissions(bool enableAudio) async { + cameraPermissionsRequested = true; + } + + @override + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + startedListeningForDeviceOrientationChanges = true; + return; + } + + @override + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingFront: + return mockFrontCameraSelector; + case CameraSelector.lensFacingBack: + default: + return mockBackCameraSelector; + } + } + + @override + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return testPreview; + } } diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index 44171eebda91..af225a10c64a 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -3,11 +3,20 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; +import 'dart:async' as _i8; -import 'package:camera_android_camerax/src/camera_info.dart' as _i4; -import 'package:camera_android_camerax/src/camera_selector.dart' as _i5; -import 'package:camera_android_camerax/src/process_camera_provider.dart' as _i2; +import 'package:camera_android_camerax/src/camera.dart' as _i3; +import 'package:camera_android_camerax/src/camera_info.dart' as _i7; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i9; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; +import 'package:camera_android_camerax/src/preview.dart' as _i10; +import 'package:camera_android_camerax/src/process_camera_provider.dart' + as _i11; +import 'package:camera_android_camerax/src/use_case.dart' as _i12; +import 'package:flutter/foundation.dart' as _i6; +import 'package:flutter/services.dart' as _i5; +import 'package:flutter/src/widgets/framework.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i13; import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: type=lint @@ -21,59 +30,360 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -/// A class which mocks [ProcessCameraProvider]. +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCamera_1 extends _i1.SmartFake implements _i3.Camera { + _FakeCamera_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_2 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_3 extends _i1.SmartFake + implements _i4.InheritedWidget { + _FakeInheritedWidget_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_4 extends _i1.SmartFake + implements _i6.DiagnosticsNode { + _FakeDiagnosticsNode_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i6.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => + super.toString(); +} + +/// A class which mocks [Camera]. /// /// See the documentation for Mockito's code generation for more information. -class MockProcessCameraProvider extends _i1.Mock - implements _i2.ProcessCameraProvider { - MockProcessCameraProvider() { - _i1.throwOnMissingStub(this); - } +class MockCamera extends _i1.Mock implements _i3.Camera {} +/// A class which mocks [CameraInfo]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraInfo extends _i1.Mock implements _i7.CameraInfo { @override - _i3.Future> getAvailableCameraInfos() => - (super.noSuchMethod( + _i8.Future getSensorRotationDegrees() => (super.noSuchMethod( Invocation.method( - #getAvailableCameraInfos, + #getSensorRotationDegrees, [], ), - returnValue: _i3.Future>.value(<_i4.CameraInfo>[]), - ) as _i3.Future>); + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); } /// A class which mocks [CameraSelector]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraSelector extends _i1.Mock implements _i5.CameraSelector { - MockCameraSelector() { - _i1.throwOnMissingStub(this); - } - +class MockCameraSelector extends _i1.Mock implements _i9.CameraSelector { @override - _i3.Future> filter(List<_i4.CameraInfo>? cameraInfos) => + _i8.Future> filter(List<_i7.CameraInfo>? cameraInfos) => (super.noSuchMethod( Invocation.method( #filter, [cameraInfos], ), - returnValue: _i3.Future>.value(<_i4.CameraInfo>[]), - ) as _i3.Future>); + returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i7.CameraInfo>[]), + ) as _i8.Future>); } -/// A class which mocks [CameraInfo]. +/// A class which mocks [Preview]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPreview extends _i1.Mock implements _i10.Preview { + @override + _i8.Future setSurfaceProvider() => (super.noSuchMethod( + Invocation.method( + #setSurfaceProvider, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + void releaseFlutterSurfaceTexture() => super.noSuchMethod( + Invocation.method( + #releaseFlutterSurfaceTexture, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i2.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + Invocation.method( + #getResolutionInfo, + [], + ), + returnValue: _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + ) as _i8.Future<_i2.ResolutionInfo>); +} + +/// A class which mocks [ProcessCameraProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockProcessCameraProvider extends _i1.Mock + implements _i11.ProcessCameraProvider { + @override + _i8.Future> getAvailableCameraInfos() => + (super.noSuchMethod( + Invocation.method( + #getAvailableCameraInfos, + [], + ), + returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i7.CameraInfo>[]), + ) as _i8.Future>); + @override + _i8.Future<_i3.Camera> bindToLifecycle( + _i9.CameraSelector? cameraSelector, + List<_i12.UseCase>? useCases, + ) => + (super.noSuchMethod( + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + returnValue: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + ) as _i8.Future<_i3.Camera>); + @override + void unbind(List<_i12.UseCase>? useCases) => super.noSuchMethod( + Invocation.method( + #unbind, + [useCases], + ), + returnValueForMissingStub: null, + ); + @override + void unbindAll() => super.noSuchMethod( + Invocation.method( + #unbindAll, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraInfo extends _i1.Mock implements _i4.CameraInfo { - MockCameraInfo() { +class MockBuildContext extends _i1.Mock implements _i4.BuildContext { + MockBuildContext() { _i1.throwOnMissingStub(this); } @override - _i3.Future getSensorRotationDegrees() => (super.noSuchMethod( + _i4.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_2( + this, + Invocation.getter(#widget), + ), + ) as _i4.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) as bool); + @override + _i4.InheritedWidget dependOnInheritedElement( + _i4.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( Invocation.method( - #getSensorRotationDegrees, + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_3( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i4.InheritedWidget); + @override + void visitAncestorElements(bool Function(_i4.Element)? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i4.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i13.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i6.DiagnosticsNode describeElement( + String? name, { + _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i6.DiagnosticsNode); + @override + _i6.DiagnosticsNode describeWidget( + String? name, { + _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i6.DiagnosticsNode); + @override + List<_i6.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i6.DiagnosticsNode>[], + ) as List<_i6.DiagnosticsNode>); + @override + _i6.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), ), - returnValue: _i3.Future.value(0), - ) as _i3.Future); + ) as _i6.DiagnosticsNode); } diff --git a/packages/camera/camera_android_camerax/test/camera_selector_test.dart b/packages/camera/camera_android_camerax/test/camera_selector_test.dart index 54f7864fb85f..52f9a18d956e 100644 --- a/packages/camera/camera_android_camerax/test/camera_selector_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.dart @@ -60,10 +60,10 @@ void main() { ); CameraSelector( instanceManager: instanceManager, - lensFacing: CameraSelector.LENS_FACING_BACK); + lensFacing: CameraSelector.lensFacingBack); verify( - mockApi.create(argThat(isA()), CameraSelector.LENS_FACING_BACK)); + mockApi.create(argThat(isA()), CameraSelector.lensFacingBack)); }); test('filterTest', () async { @@ -108,14 +108,14 @@ void main() { instanceManager: instanceManager, ); - flutterApi.create(0, CameraSelector.LENS_FACING_BACK); + flutterApi.create(0, CameraSelector.lensFacingBack); expect(instanceManager.getInstanceWithWeakReference(0), isA()); expect( (instanceManager.getInstanceWithWeakReference(0)! as CameraSelector) .lensFacing, - equals(CameraSelector.LENS_FACING_BACK)); + equals(CameraSelector.lensFacingBack)); }); }); }