From 7894cc25bf65bef99495bc9ff545e9ee7c236771 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 6 Feb 2023 16:29:59 -0800 Subject: [PATCH 01/23] Add base code from proof of concept --- .../example/lib/camera_controller.dart | 435 +++++++ .../example/lib/camera_preview.dart | 86 ++ .../example/lib/main.dart | 1007 ++++++++++++++++- .../lib/src/android_camera_camerax.dart | 257 +++++ .../lib/src/camera_selector.dart | 9 + 5 files changed, 1772 insertions(+), 22 deletions(-) create mode 100644 packages/camera/camera_android_camerax/example/lib/camera_controller.dart create mode 100644 packages/camera/camera_android_camerax/example/lib/camera_preview.dart 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..fd0551f21baa --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -0,0 +1,435 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + required this.isInitialized, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required this.isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }); + + /// 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, + focusMode: FocusMode.auto, + 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; + + /// True when video recording is paused. + 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; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// 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; + + /// 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; + + // TODO(camsim99): Make fix here used for Optional regressions + + /// 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, + Size? previewSize, + bool? isRecordingPaused, + FlashMode? flashMode, + ExposureMode? exposureMode, + FocusMode? focusMode, + bool? exposurePointSupported, + bool? focusPointSupported, + DeviceOrientation? deviceOrientation, + DeviceOrientation? lockedCaptureOrientation, + DeviceOrientation? recordingOrientation, + bool? isPreviewPaused, + DeviceOrientation? previewPauseOrientation, + }) { + return CameraValue( + isInitialized: isInitialized ?? this.isInitialized, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? this.isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: + lockedCaptureOrientation ?? this.lockedCaptureOrientation, + recordingOrientation: recordingOrientation ?? this.recordingOrientation, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: + previewPauseOrientation ?? this.previewPauseOrientation, + ); + } + + @override + String toString() { + return '${objectRuntimeType(this, 'CameraValue')}(' + 'isRecordingVideo: $isRecordingVideo, ' + 'isInitialized: $isInitialized, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// Controls a device camera. +/// +/// This is a stripped-down version of the app-facing controller to serve as a +/// utility for the example and integration tests. It wraps only the calls that +/// have state associated with them, to consolidate tracking of camera state +/// outside of the overall example code. +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; + + late int _cameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? + _deviceOrientationSubscription; + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + Future initialize() async { + 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, + ); + + 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), + ); + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: + value.lockedCaptureOrientation ?? value.deviceOrientation); + } + + /// Resumes the current camera preview + Future resumePreview() async { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith(isPreviewPaused: false); + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + value = value.copyWith(isTakingPicture: true); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } + + /// Start streaming images from platform camera. + Future startImageStream( + Function(CameraImageData image) onAvailable) async { + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(imageData); + }); + value = value.copyWith(isStreamingImages: true); + } + + /// Stop streaming images from platform camera. + Future stopImageStream() async { + value = value.copyWith(isStreamingImages: false); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } + + /// Start a video recording. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording( + {Function(CameraImageData image)? streamCallback}) async { + // await CameraPlatform.instance.startVideoCapturing( + // VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + isStreamingImages: streamCallback != null, + recordingOrientation: + value.lockedCaptureOrientation ?? value.deviceOrientation); + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + if (value.isStreamingImages) { + await stopImageStream(); + } + + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + isRecordingPaused: false, + ); + return file; + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + return CameraPlatform.instance.buildPreview(_cameraId); + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } + + /// Sets the exposure offset for the selected camera. + Future setExposureOffset(double offset) async { + // Check if offset is in range + final List range = await Future.wait(>[ + CameraPlatform.instance.getMinExposureOffset(_cameraId), + CameraPlatform.instance.getMaxExposureOffset(_cameraId) + ]); + + // Round to the closest step if needed + final double stepSize = + await CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + 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; + } + + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation() async { + await CameraPlatform.instance + .lockCaptureOrientation(_cameraId, value.deviceOrientation); + value = value.copyWith(lockedCaptureOrientation: value.deviceOrientation); + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(FocusMode mode) async { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _deviceOrientationSubscription?.cancel(); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + @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); + } + } +} 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..86be03acd020 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -0,0 +1,86 @@ +// 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, {Key? key, this.child}) + : super(key: key); + + /// 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) { + // TODO(camsim99): Fix the issue with these being swapped. + final double cameraAspectRatio = + controller.value.previewSize!.height / + controller.value.previewSize!.width; + return AspectRatio( + aspectRatio: _isLandscape() + ? cameraAspectRatio + : (1 / cameraAspectRatio), + 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..e10c1ea94b57 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -2,43 +2,1006 @@ // 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 'dart:math'; + 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({Key? key}) : super(key: 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; + default: + throw ArgumentError('Unknown lens direction'); + } +} + +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(); + _ambiguate(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() { + _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + @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); + } + } + @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!), + ); } - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Camera Example'), + } + + /// 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)), + ), + ), + ), + ], ), - body: Center( - child: Text(availableCameraNames.substring( - 0, availableCameraNames.length - 1)), + ), + ); + } + + /// 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: controller != null ? onAudioModeButtonPressed : null, + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: () {}, //TODO(camsim99): Add functionality back here. + ), + ], + ), + _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) { + CameraPlatform.instance + .setExposurePoint(controller!.cameraId, 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) { + CameraPlatform.instance + .setFocusPoint(controller!.cameraId, 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.isRecordingVideo || + 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) { + _ambiguate(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 Point point = Point( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + CameraPlatform.instance.setExposurePoint(cameraController.cameraId, point); + CameraPlatform.instance.setFocusPoint(cameraController.cameraId, point); + } + + 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(() {}); + } + }); + + try { + await cameraController.initialize(); + } 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; + case 'cameraPermission': + // Android & web only + showInSnackBar('Unknown permission error.'); + 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({Key? key}) : super(key: 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(); + // TODO(camsim99): Use actual availableCameras method here + _cameras = [ + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ]; + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp()); +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; 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 f03273861793..4b8b31230d9e 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,6 +5,17 @@ import 'dart:async'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'camera.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 { @@ -13,9 +24,255 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } + // Objects used to access camera functionality: + + /// The [ProcessCameraProvider] instance used to access camera functionality. + ProcessCameraProvider? processCameraProvider; + + /// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is + /// bound to the lifecycle of the camera it manages. + Camera? camera; + + // Use cases to configure and bind to ProcessCameraProvider instance: + + /// The [Preview] instance that can be instantiated adn configured to present + /// a live camera preview. + Preview? preview; + + // Objects used for camera configuration: + CameraSelector? _cameraSelector; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to 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); + + // Implementation of Flutter camera platform interface (CameraPlatform): + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { throw UnimplementedError('availableCameras() is not implemented.'); } + + /// Creates an uninitialized camera instance and returns the cameraId. + /// + /// 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 cameraID, which represents 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 obtatin proper permissions before attempts to access a camera. + await SystemServices.requestCameraPermissions(enableAudio); + + // Start listening for device orientation changes preceding camera creation. + final int cameraSelectorLensDirection = _getCameraSelectorLensDirection(cameraDescription.lensDirection); + final bool cameraIsFrontFacing = cameraSelectorLensDirection == CameraSelector.LENS_FACING_FRONT; + _cameraSelector = CameraSelector(lensFacing: cameraSelectorLensDirection); + SystemServices.startListeningForDeviceOrientationChange(cameraIsFrontFacing, cameraDescription.sensorOrientation); + + // Retrieve a ProcessCameraProvider instance. + // TODO(camsim99): Always get a new one? + processCameraProvider = await ProcessCameraProvider.getInstance(); + + // Configure Preview instance and bind to ProcessCameraProvider. + final int targetRotation = _getTargetRotation(cameraDescription.sensorOrientation); + final ResolutionInfo? targetResolution = _getTargetResolutionForPreview(resolutionPreset); + preview = Preview(targetRotation: targetRotation, targetResolution: targetResolution); + final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(); + + return flutterSurfaceTextureId; + } + + /// Initializes the camera on the device. + /// + /// [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. + + // 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. + const ExposureMode exposureMode = ExposureMode.auto; + const bool exposurePointSupported = false; + + // TODO(camsim99): Implement support for retrieving focus mode configuration. + 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. + @override + Future dispose(int cameraId) async { + preview?.releaseFlutterSurfaceTexture(); + processCameraProvider?.unbindAll(); + } + + /// Callback method for the initialization of a camera. + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + /// Callback method for native camera errors. + @override + Stream onCameraError(int cameraId) { + return SystemServices + .cameraErrorStreamController + .stream + .map((String errorDescription) { + return CameraErrorEvent(cameraId, errorDescription); + }); + } + + /// Callback method for changes in device orientation. + @override + Stream onDeviceOrientationChanged() { + return SystemServices.deviceOrientationChangedStreamController.stream; + } + + /// Pause the active preview on the current frame for the selected camera. + @override + Future pausePreview(int cameraId) async { + // TODO(camsim99): Determine whether or not to track if preview is paused or not. Or really how to manage preview and binding. + _unbindPreviewFromLifecycle(); + } + + /// Resume the paused preview for the selected camera. + @override + Future resumePreview(int cameraId) async { + _bindPreviewToLifecycle(); + } + + /// 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); + + camera = await processCameraProvider!.bindToLifecycle(_cameraSelector!, [preview!]); + } + + /// Unbinds [Preview] instance to camera lifecycle controlled by the + /// [processCameraProvider]. + void _unbindPreviewFromLifecycle() { + if (preview == null) { + return; + } + + assert(processCameraProvider != null); + processCameraProvider!.unbind([preview!]); + } + + // 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.LENS_FACING_FRONT; + case CameraLensDirection.back: + return CameraSelector.LENS_FACING_BACK; + case CameraLensDirection.external: + return CameraSelector.EXTERNAL; + } + } + + /// 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. + return null; + } } 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..7ea93ec05d9e 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -44,11 +44,20 @@ class CameraSelector extends JavaObject { late final CameraSelectorHostApiImpl _api; /// ID for front facing lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_FRONT(). static const int LENS_FACING_FRONT = 0; /// ID for back facing lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_BACK(). static const int LENS_FACING_BACK = 1; + /// ID for external lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_EXTERNAL(). + static const int EXTERNAL = 2; + /// Selector for default front facing camera. static CameraSelector getDefaultFrontCamera({ BinaryMessenger? binaryMessenger, From efca3ed8671daf03eb9c1cc9d099f26eb5cf7f87 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 7 Feb 2023 09:35:44 -0800 Subject: [PATCH 02/23] Fix analyzer --- .../example/lib/camera_preview.dart | 3 +- .../example/lib/main.dart | 50 +++++++++---------- .../example/pubspec.yaml | 1 + 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart index 86be03acd020..347cfc16a637 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -11,8 +11,7 @@ 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, {Key? key, this.child}) - : super(key: key); + const CameraPreview(this.controller, {super.key, this.child}); /// The controller for the camera that the preview is shown for. final CameraController controller; diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index e10c1ea94b57..a02b92abe733 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -18,7 +18,7 @@ import 'camera_preview.dart'; /// Camera example home widget. class CameraExampleHome extends StatefulWidget { /// Default Constructor - const CameraExampleHome({Key? key}) : super(key: key); + const CameraExampleHome({super.key}); @override State createState() { @@ -35,8 +35,6 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } } @@ -53,8 +51,10 @@ class _CameraExampleHomeState extends State VideoPlayerController? videoController; VoidCallback? videoPlayerListener; bool enableAudio = true; - double _minAvailableExposureOffset = 0.0; - double _maxAvailableExposureOffset = 0.0; + // TODO(camsim99): Actually use this. + final double _minAvailableExposureOffset = 0.0; + // TODO(camsim99): Actually use this. + final double _maxAvailableExposureOffset = 0.0; double _currentExposureOffset = 0.0; late AnimationController _flashModeControlRowAnimationController; late Animation _flashModeControlRowAnimation; @@ -62,10 +62,6 @@ class _CameraExampleHomeState extends State 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; @@ -245,7 +241,7 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.flash_on), color: Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), // The exposure and focus mode are currently not supported on the web. ...!kIsWeb @@ -253,12 +249,12 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.exposure), color: Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + 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. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ) ] : [], @@ -272,7 +268,7 @@ class _CameraExampleHomeState extends State ? Icons.screen_lock_rotation : Icons.screen_rotation), color: Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), ], ), @@ -295,28 +291,28 @@ class _CameraExampleHomeState extends State color: controller?.value.flashMode == FlashMode.off ? Colors.orange : Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + 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. + 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. + 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. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), ], ), @@ -355,7 +351,7 @@ class _CameraExampleHomeState extends State children: [ TextButton( style: styleAuto, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. onLongPress: () { if (controller != null) { CameraPlatform.instance @@ -367,12 +363,12 @@ class _CameraExampleHomeState extends State ), TextButton( style: styleLocked, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. child: const Text('LOCKED'), ), TextButton( style: styleLocked, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. child: const Text('RESET OFFSET'), ), ], @@ -435,7 +431,7 @@ class _CameraExampleHomeState extends State children: [ TextButton( style: styleAuto, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. onLongPress: () { if (controller != null) { CameraPlatform.instance @@ -447,7 +443,7 @@ class _CameraExampleHomeState extends State ), TextButton( style: styleLocked, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. child: const Text('LOCKED'), ), ], @@ -469,12 +465,12 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.camera_alt), color: Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), IconButton( icon: cameraController != null && @@ -483,12 +479,12 @@ class _CameraExampleHomeState extends State ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), color: Colors.blue, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, - onPressed: () {}, //TODO(camsim99): Add functionality back here. + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), IconButton( icon: const Icon(Icons.pause_presentation), @@ -970,7 +966,7 @@ class _CameraExampleHomeState extends State /// CameraApp is the Main Application. class CameraApp extends StatelessWidget { /// Default Constructor - const CameraApp({Key? key}) : super(key: key); + const CameraApp({super.key}); @override Widget build(BuildContext context) { 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: From b4ae1db021d3323a40b14be1ff23c0ab6424082e Mon Sep 17 00:00:00 2001 From: camsim99 Date: Thu, 9 Feb 2023 13:26:35 -0800 Subject: [PATCH 03/23] Add example app and tests --- .../example/lib/camera_preview.dart | 5 +- .../example/lib/main.dart | 23 ++- .../lib/src/android_camera_camerax.dart | 148 +++++++++++------- .../lib/src/system_services.dart | 2 - 4 files changed, 112 insertions(+), 66 deletions(-) diff --git a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart index 347cfc16a637..4fbdb3a2b069 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -25,10 +25,9 @@ class CameraPreview extends StatelessWidget { ? ValueListenableBuilder( valueListenable: controller, builder: (BuildContext context, Object? value, Widget? child) { - // TODO(camsim99): Fix the issue with these being swapped. final double cameraAspectRatio = - controller.value.previewSize!.height / - controller.value.previewSize!.width; + controller.value.previewSize!.width / + controller.value.previewSize!.height; return AspectRatio( aspectRatio: _isLandscape() ? cameraAspectRatio diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index a02b92abe733..806e9cf1362b 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -249,19 +249,21 @@ class _CameraExampleHomeState extends State IconButton( icon: const Icon(Icons.exposure), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + 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. + onPressed: + () {}, // TODO(camsim99): Add functionality back here. ) ] : [], IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), color: Colors.blue, - onPressed: controller != null ? onAudioModeButtonPressed : null, + onPressed: () {}, // TODO(camsim99): Add functionality back here. ), IconButton( icon: Icon(controller?.value.isCaptureOrientationLocked ?? false @@ -351,7 +353,8 @@ class _CameraExampleHomeState extends State children: [ TextButton( style: styleAuto, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + () {}, // TODO(camsim99): Add functionality back here. onLongPress: () { if (controller != null) { CameraPlatform.instance @@ -363,12 +366,14 @@ class _CameraExampleHomeState extends State ), TextButton( style: styleLocked, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + () {}, // TODO(camsim99): Add functionality back here. child: const Text('LOCKED'), ), TextButton( style: styleLocked, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + () {}, // TODO(camsim99): Add functionality back here. child: const Text('RESET OFFSET'), ), ], @@ -431,7 +436,8 @@ class _CameraExampleHomeState extends State children: [ TextButton( style: styleAuto, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + () {}, // TODO(camsim99): Add functionality back here. onLongPress: () { if (controller != null) { CameraPlatform.instance @@ -443,7 +449,8 @@ class _CameraExampleHomeState extends State ), TextButton( style: styleLocked, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: + () {}, // TODO(camsim99): Add functionality back here. child: const Text('LOCKED'), ), ], 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 4b8b31230d9e..d182dc9e56d7 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 @@ -32,15 +32,17 @@ class AndroidCameraCameraX extends CameraPlatform { /// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is /// bound to the lifecycle of the camera it manages. Camera? camera; - + // Use cases to configure and bind to ProcessCameraProvider instance: /// The [Preview] instance that can be instantiated adn configured to present /// a live camera preview. Preview? preview; + bool previewIsBound = false; + bool _previewIsPaused = false; // Objects used for camera configuration: - CameraSelector? _cameraSelector; + CameraSelector? cameraSelector; /// The controller we need to broadcast the different events coming /// from handleMethodCall, specific to camera events. @@ -83,29 +85,37 @@ class AndroidCameraCameraX extends CameraPlatform { bool enableAudio = false, }) async { // Must obtatin proper permissions before attempts to access a camera. - await SystemServices.requestCameraPermissions(enableAudio); + await requestCameraPermissions(enableAudio); + final int cameraSelectorLensDirection = + _getCameraSelectorLensDirection(cameraDescription.lensDirection); + final bool cameraIsFrontFacing = + cameraSelectorLensDirection == CameraSelector.LENS_FACING_FRONT; + cameraSelector = createCameraSelector(cameraSelectorLensDirection); // Start listening for device orientation changes preceding camera creation. - final int cameraSelectorLensDirection = _getCameraSelectorLensDirection(cameraDescription.lensDirection); - final bool cameraIsFrontFacing = cameraSelectorLensDirection == CameraSelector.LENS_FACING_FRONT; - _cameraSelector = CameraSelector(lensFacing: cameraSelectorLensDirection); - SystemServices.startListeningForDeviceOrientationChange(cameraIsFrontFacing, cameraDescription.sensorOrientation); + startListeningForDeviceOrientationChange( + cameraIsFrontFacing, cameraDescription.sensorOrientation); // Retrieve a ProcessCameraProvider instance. - // TODO(camsim99): Always get a new one? - processCameraProvider = await ProcessCameraProvider.getInstance(); + processCameraProvider ??= await getProcessCameraProviderInstance(); // Configure Preview instance and bind to ProcessCameraProvider. - final int targetRotation = _getTargetRotation(cameraDescription.sensorOrientation); - final ResolutionInfo? targetResolution = _getTargetResolutionForPreview(resolutionPreset); - preview = Preview(targetRotation: targetRotation, targetResolution: targetResolution); + final int targetRotation = + _getTargetRotation(cameraDescription.sensorOrientation); + final ResolutionInfo? targetResolution = + _getTargetResolutionForPreview(resolutionPreset); + preview = createPreview(targetRotation, targetResolution); 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 only 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. @@ -119,18 +129,17 @@ class AndroidCameraCameraX extends CameraPlatform { // Configure CameraInitializedEvent to send as representation of a // configured camera: - // Retrieve preview resolution. - assert ( + assert( preview != null, 'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"', ); - await _bindPreviewToLifecycle(); - final ResolutionInfo previewResolutionInfo = await preview!.getResolutionInfo(); - _unbindPreviewFromLifecycle(); + await bindPreviewToLifecycle(); + final ResolutionInfo previewResolutionInfo = + await preview!.getResolutionInfo(); + unbindPreviewFromLifecycle(); // Retrieve exposure and focus mode configurations: - // TODO(camsim99): Implement support for retrieving exposure mode configuration. const ExposureMode exposureMode = ExposureMode.auto; const bool exposurePointSupported = false; @@ -139,19 +148,19 @@ class AndroidCameraCameraX extends CameraPlatform { const FocusMode focusMode = FocusMode.auto; const bool focusPointSupported = false; - cameraEventStreamController.add( - CameraInitializedEvent( + cameraEventStreamController.add(CameraInitializedEvent( cameraId, previewResolutionInfo.width.toDouble(), previewResolutionInfo.height.toDouble(), exposureMode, exposurePointSupported, focusMode, - focusPointSupported) - ); + focusPointSupported)); } /// Releases the resources of the accessed camera. + /// + /// [cameraId] not used. @override Future dispose(int cameraId) async { preview?.releaseFlutterSurfaceTexture(); @@ -167,12 +176,10 @@ class AndroidCameraCameraX extends CameraPlatform { /// Callback method for native camera errors. @override Stream onCameraError(int cameraId) { - return SystemServices - .cameraErrorStreamController - .stream - .map((String errorDescription) { - return CameraErrorEvent(cameraId, errorDescription); - }); + return SystemServices.cameraErrorStreamController.stream + .map((String errorDescription) { + return CameraErrorEvent(cameraId, errorDescription); + }); } /// Callback method for changes in device orientation. @@ -184,33 +191,33 @@ class AndroidCameraCameraX extends CameraPlatform { /// Pause the active preview on the current frame for the selected camera. @override Future pausePreview(int cameraId) async { - // TODO(camsim99): Determine whether or not to track if preview is paused or not. Or really how to manage preview and binding. - _unbindPreviewFromLifecycle(); + unbindPreviewFromLifecycle(); + _previewIsPaused = true; } /// Resume the paused preview for the selected camera. @override Future resumePreview(int cameraId) async { - _bindPreviewToLifecycle(); + 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); - } - } - ); + 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 @@ -218,22 +225,31 @@ class AndroidCameraCameraX extends CameraPlatform { /// Binds [Preview] instance to the camera lifecycle controlled by the /// [processCameraProvider]. - Future _bindPreviewToLifecycle() async { + Future bindPreviewToLifecycle() async { assert(processCameraProvider != null); - assert(_cameraSelector != 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!]); + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [preview!]); + previewIsBound = true; } /// Unbinds [Preview] instance to camera lifecycle controlled by the /// [processCameraProvider]. - void _unbindPreviewFromLifecycle() { - if (preview == null) { + void unbindPreviewFromLifecycle() { + if (preview == null || !previewIsBound) { return; } assert(processCameraProvider != null); + processCameraProvider!.unbind([preview!]); + previewIsBound = false; } // Methods for mapping Flutter camera constants to CameraX constants: @@ -265,14 +281,40 @@ class AndroidCameraCameraX extends CameraPlatform { return Surface.ROTATION_0; default: throw ArgumentError( - '"$sensorOrientation" is not a valid sensor orientation value'); - } + '"$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) { + ResolutionInfo? _getTargetResolutionForPreview(ResolutionPreset? resolution) { // TODO(camsim99): Implement resolution configuration. return null; } + + // Methods for calls that need to be tested: + + Future requestCameraPermissions(bool enableAudio) async { + await SystemServices.requestCameraPermissions(enableAudio); + } + + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + SystemServices.startListeningForDeviceOrientationChange( + cameraIsFrontFacing, sensorOrientation); + } + + Future getProcessCameraProviderInstance() async { + ProcessCameraProvider instance = await ProcessCameraProvider.getInstance(); + return instance; + } + + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + return CameraSelector(lensFacing: cameraSelectorLensDirection); + } + + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return Preview( + targetRotation: targetRotation, targetResolution: targetResolution); + } } 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); } } From 7c8168b39b5c8bdf4339b151c6ed977fa327efe0 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Thu, 9 Feb 2023 13:28:57 -0800 Subject: [PATCH 04/23] Actually add tets and visible for testing annotation --- .../lib/src/android_camera_camerax.dart | 5 + .../test/android_camera_camerax_test.dart | 337 +++++++++++++++ .../android_camera_camerax_test.mocks.dart | 407 ++++++++++++++++++ 3 files changed, 749 insertions(+) create mode 100644 packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart create mode 100644 packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart 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 d182dc9e56d7..952b103c2206 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 @@ -294,25 +294,30 @@ class AndroidCameraCameraX extends CameraPlatform { // Methods for calls that need to be tested: + @visibleForTesting Future requestCameraPermissions(bool enableAudio) async { await SystemServices.requestCameraPermissions(enableAudio); } + @visibleForTesting void startListeningForDeviceOrientationChange( bool cameraIsFrontFacing, int sensorOrientation) { SystemServices.startListeningForDeviceOrientationChange( cameraIsFrontFacing, sensorOrientation); } + @visibleForTesting Future getProcessCameraProviderInstance() async { ProcessCameraProvider instance = await ProcessCameraProvider.getInstance(); return instance; } + @visibleForTesting CameraSelector createCameraSelector(int cameraSelectorLensDirection) { return CameraSelector(lensFacing: cameraSelectorLensDirection); } + @visibleForTesting Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { return Preview( targetRotation: targetRotation, targetResolution: targetResolution); 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 new file mode 100644 index 000000000000..d779ba1a2c1e --- /dev/null +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -0,0 +1,337 @@ +// 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:async/async.dart'; +import 'package:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/camera_selector.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/surface.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/widgets.dart'; +import 'package:flutter/services.dart' show DeviceOrientation; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'android_camera_camerax_test.mocks.dart'; + +@GenerateNiceMocks(>[ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test( + 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID ', + () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + 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 permnissions 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.testCameraSelector)); + + // Verify ProcessCameraProvider instance is received. + expect( + camera.processCameraProvider, equals(camera.testProcessCameraProvider)); + + // 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(); + final 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; + final int resolutionWidth = 350; + final 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. + 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.testProcessCameraProvider.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, TypeMatcher()); + expect(event, equals(testCameraInitializedEvent)); + }); + + await camera.initializeCamera(cameraId); + + // Verify preview was bound and unbound to get preview resolution information. + verify(camera.testProcessCameraProvider + .bindToLifecycle(camera.cameraSelector, [camera.testPreview])); + verify( + camera.testProcessCameraProvider.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(); + final int cameraId = 16; + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + final 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(); + final int cameraId = 27; + final 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(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); + final 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(); + final int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + expect(previewWidget.builder(MockBuildContext(), AsyncSnapshot.nothing()), + isA()); + expect(previewWidget.builder(MockBuildContext(), AsyncSnapshot.waiting()), + isA()); + expect( + previewWidget.builder(MockBuildContext(), + 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(); + final int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + Texture previewTexture = previewWidget.builder(MockBuildContext(), + 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 MockProcessCameraProvider testProcessCameraProvider = + MockProcessCameraProvider(); + final MockPreview testPreview = MockPreview(); + final MockCameraSelector testCameraSelector = MockCameraSelector(); + + @override + Future requestCameraPermissions(bool enableAudio) async { + cameraPermissionsRequested = true; + } + + @override + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + startedListeningForDeviceOrientationChanges = true; + return; + } + + @override + Future getProcessCameraProviderInstance() async { + return testProcessCameraProvider; + } + + @override + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + return testCameraSelector; + } + + @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 new file mode 100644 index 000000000000..f934de2fe4d7 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -0,0 +1,407 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/android_camera_camerax_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i8; + +import 'package:camera_android_camerax/src/camera.dart' as _i5; +import 'package:camera_android_camerax/src/camera_info.dart' as _i9; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i7; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i4; +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 _i3; +import 'package:flutter/src/widgets/framework.dart' as _i2; +import 'package:flutter/src/widgets/notification_listener.dart' as _i6; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { + _FakeWidget_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_1 extends _i1.SmartFake + implements _i2.InheritedWidget { + _FakeInheritedWidget_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_2 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i3.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => + super.toString(); +} + +class _FakeResolutionInfo_3 extends _i1.SmartFake + implements _i4.ResolutionInfo { + _FakeResolutionInfo_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCamera_4 extends _i1.SmartFake implements _i5.Camera { + _FakeCamera_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i2.BuildContext { + @override + _i2.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_0( + this, + Invocation.getter(#widget), + ), + returnValueForMissingStub: _FakeWidget_0( + this, + Invocation.getter(#widget), + ), + ) as _i2.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i2.InheritedWidget dependOnInheritedElement( + _i2.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_1( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + returnValueForMissingStub: _FakeInheritedWidget_1( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i2.InheritedWidget); + @override + void visitAncestorElements(_i2.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i2.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i6.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i3.DiagnosticsNode); + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i3.DiagnosticsNode); + @override + List<_i3.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i3.DiagnosticsNode>[], + returnValueForMissingStub: <_i3.DiagnosticsNode>[], + ) as List<_i3.DiagnosticsNode>); + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + returnValueForMissingStub: _FakeDiagnosticsNode_2( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i3.DiagnosticsNode); +} + +/// A class which mocks [Camera]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCamera extends _i1.Mock implements _i5.Camera {} + +/// A class which mocks [CameraSelector]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraSelector extends _i1.Mock implements _i7.CameraSelector { + @override + _i8.Future> filter(List<_i9.CameraInfo>? cameraInfos) => + (super.noSuchMethod( + Invocation.method( + #filter, + [cameraInfos], + ), + returnValue: _i8.Future>.value(<_i9.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i9.CameraInfo>[]), + ) as _i8.Future>); +} + +/// 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<_i4.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + Invocation.method( + #getResolutionInfo, + [], + ), + returnValue: _i8.Future<_i4.ResolutionInfo>.value(_FakeResolutionInfo_3( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i4.ResolutionInfo>.value(_FakeResolutionInfo_3( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + ) as _i8.Future<_i4.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(<_i9.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i9.CameraInfo>[]), + ) as _i8.Future>); + @override + _i8.Future<_i5.Camera> bindToLifecycle( + _i7.CameraSelector? cameraSelector, + List<_i12.UseCase>? useCases, + ) => + (super.noSuchMethod( + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + returnValue: _i8.Future<_i5.Camera>.value(_FakeCamera_4( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i5.Camera>.value(_FakeCamera_4( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + ) as _i8.Future<_i5.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, + ); +} From 93d442ba5b21284ee580f134eefb017b4aae522d Mon Sep 17 00:00:00 2001 From: camsim99 Date: Thu, 9 Feb 2023 13:30:53 -0800 Subject: [PATCH 05/23] Make methods private --- .../lib/src/android_camera_camerax.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 952b103c2206..0a1ab7fd5cfe 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 @@ -134,10 +134,10 @@ class AndroidCameraCameraX extends CameraPlatform { preview != null, 'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"', ); - await bindPreviewToLifecycle(); + await _bindPreviewToLifecycle(); final ResolutionInfo previewResolutionInfo = await preview!.getResolutionInfo(); - unbindPreviewFromLifecycle(); + _unbindPreviewFromLifecycle(); // Retrieve exposure and focus mode configurations: // TODO(camsim99): Implement support for retrieving exposure mode configuration. @@ -191,14 +191,14 @@ class AndroidCameraCameraX extends CameraPlatform { /// Pause the active preview on the current frame for the selected camera. @override Future pausePreview(int cameraId) async { - unbindPreviewFromLifecycle(); + _unbindPreviewFromLifecycle(); _previewIsPaused = true; } /// Resume the paused preview for the selected camera. @override Future resumePreview(int cameraId) async { - await bindPreviewToLifecycle(); + await _bindPreviewToLifecycle(); _previewIsPaused = false; } @@ -206,7 +206,7 @@ class AndroidCameraCameraX extends CameraPlatform { @override Widget buildPreview(int cameraId) { return FutureBuilder( - future: bindPreviewToLifecycle(), + future: _bindPreviewToLifecycle(), builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: @@ -225,7 +225,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// Binds [Preview] instance to the camera lifecycle controlled by the /// [processCameraProvider]. - Future bindPreviewToLifecycle() async { + Future _bindPreviewToLifecycle() async { assert(processCameraProvider != null); assert(cameraSelector != null); @@ -241,7 +241,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// Unbinds [Preview] instance to camera lifecycle controlled by the /// [processCameraProvider]. - void unbindPreviewFromLifecycle() { + void _unbindPreviewFromLifecycle() { if (preview == null || !previewIsBound) { return; } From bf1ca9a8506e946c62d3a9a56519bf1069723511 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Thu, 9 Feb 2023 14:46:25 -0800 Subject: [PATCH 06/23] Fix tests --- .../lib/src/android_camera_camerax.dart | 8 +++++--- .../test/android_camera_camerax_test.dart | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) 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 b05ab87b0db0..5fafe609a733 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 @@ -82,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 createCameraSelector(CameraSelector.LENS_FACING_BACK).filter([cameraInfo])) + if ((await createCameraSelector(CameraSelector.LENS_FACING_BACK) + .filter([cameraInfo])) .isNotEmpty) { cameraLensDirection = CameraLensDirection.back; - } else if ((await createCameraSelector(CameraSelector.LENS_FACING_FRONT).filter([cameraInfo])) + } else if ((await createCameraSelector(CameraSelector.LENS_FACING_FRONT) + .filter([cameraInfo])) .isNotEmpty) { cameraLensDirection = CameraLensDirection.front; } else { @@ -352,7 +354,7 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting CameraSelector createCameraSelector(int cameraSelectorLensDirection) { - switch(cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { case CameraSelector.LENS_FACING_FRONT: return CameraSelector.getDefaultFrontCamera(); case CameraSelector.LENS_FACING_BACK: 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 7970c6005fc6..86335d6c81c2 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 @@ -57,13 +57,17 @@ void main() { // Mock calls to native platform when(camera.testProcessCameraProvider.getAvailableCameraInfos()).thenAnswer( (_) async => [mockBackCameraInfo, mockFrontCameraInfo]); - when(camera.mockBackCameraSelector.filter([mockFrontCameraInfo])) + when(camera.mockBackCameraSelector + .filter([mockFrontCameraInfo])) .thenAnswer((_) async => []); - when(camera.mockBackCameraSelector.filter([mockBackCameraInfo])) + when(camera.mockBackCameraSelector + .filter([mockBackCameraInfo])) .thenAnswer((_) async => [mockBackCameraInfo]); - when(camera.mockFrontCameraSelector.filter([mockBackCameraInfo])) + when(camera.mockFrontCameraSelector + .filter([mockBackCameraInfo])) .thenAnswer((_) async => []); - when(camera.mockFrontCameraSelector.filter([mockFrontCameraInfo])) + when(camera.mockFrontCameraSelector + .filter([mockFrontCameraInfo])) .thenAnswer((_) async => [mockFrontCameraInfo]); when(mockBackCameraInfo.getSensorRotationDegrees()) .thenAnswer((_) async => 0); @@ -365,7 +369,6 @@ class MockAndroidCameraCamerax extends AndroidCameraCameraX { final MockPreview testPreview = MockPreview(); final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); - final MockCameraSelector testCameraSelector = MockCameraSelector(); @override Future requestCameraPermissions(bool enableAudio) async { @@ -386,13 +389,12 @@ class MockAndroidCameraCamerax extends AndroidCameraCameraX { @override CameraSelector createCameraSelector(int cameraSelectorLensDirection) { - switch(cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { case CameraSelector.LENS_FACING_FRONT: return mockFrontCameraSelector; case CameraSelector.LENS_FACING_BACK: - return mockBackCameraSelector; default: - return testCameraSelector; + return mockBackCameraSelector; } } From b237d6ed3906ce9dc90c3c2910da7d63eb59d0cd Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 10:18:17 -0800 Subject: [PATCH 07/23] Fix analyze: --- .../lib/src/android_camera_camerax.dart | 25 +++++++++-- .../camera_android_camerax/pubspec.yaml | 1 + .../test/android_camera_camerax_test.dart | 45 ++++++++++--------- 3 files changed, 45 insertions(+), 26 deletions(-) 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 5fafe609a733..92423cca36c1 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 @@ -25,7 +25,7 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } - // Objects used to access camera functionality: + // Instances used to access camera functionality: /// The [ProcessCameraProvider] instance used to access camera functionality. @visibleForTesting @@ -36,15 +36,25 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting Camera? camera; - // Use cases to configure and bind to ProcessCameraProvider instance: + // Instances used to configure and bind use cases to ProcessCameraProvider instance: /// The [Preview] instance that can be instantiated adn configured to present /// a live camera preview. + @visibleForTesting Preview? preview; + + /// Whether or not the [Preview] is currently bound to the lifecycle that the + /// [processCameraProvider] tracks. + @visibleForTesting bool previewIsBound = false; + bool _previewIsPaused = false; - // Objects used for camera configuration: + // Instances used for camera configuration: + + /// The [CameraSelector] used to configure the [processCameraProvider] to use + /// the desired camera. + @visibleForTesting CameraSelector? cameraSelector; /// The controller we need to broadcast the different events coming @@ -334,11 +344,13 @@ class AndroidCameraCameraX extends CameraPlatform { // 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) { @@ -346,12 +358,15 @@ class AndroidCameraCameraX extends CameraPlatform { cameraIsFrontFacing, sensorOrientation); } + /// Retrives an instance of the [ProcessCameraProvider] to access camera + /// functionality. @visibleForTesting Future getProcessCameraProviderInstance() async { - ProcessCameraProvider instance = await ProcessCameraProvider.getInstance(); + final ProcessCameraProvider instance = await ProcessCameraProvider.getInstance(); return instance; } + /// Returns a [CameraSelector] based on the specified camera lens direction. @visibleForTesting CameraSelector createCameraSelector(int cameraSelectorLensDirection) { switch (cameraSelectorLensDirection) { @@ -364,6 +379,8 @@ class AndroidCameraCameraX extends CameraPlatform { } } + /// Returns a [Preview] configured with the specified target rotation and + /// resolution. @visibleForTesting Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { return Preview( diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 7f81ecbd4f71..50e1d4b379a2 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: stream_transform: ^2.1.0 dev_dependencies: + async: ^2.5.0 build_runner: ^2.1.4 flutter_test: sdk: flutter 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 86335d6c81c2..26ed88fda7fe 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,20 +2,21 @@ // 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/camerax_library.g.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/surface.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/widgets.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'; @@ -141,7 +142,7 @@ void main() { test('initializeCamera sends expected CameraInitializedEvent', () async { final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); - final int cameraId = 10; + const int cameraId = 10; const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 90; const CameraDescription testCameraDescription = CameraDescription( @@ -150,8 +151,8 @@ void main() { sensorOrientation: testSensorOrientation); const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; const bool enableAudio = true; - final int resolutionWidth = 350; - final int resolutionHeight = 750; + const int resolutionWidth = 350; + const int resolutionHeight = 750; final Camera mockCamera = MockCamera(); final ResolutionInfo testResolutionInfo = ResolutionInfo(width: resolutionWidth, height: resolutionHeight); @@ -181,7 +182,7 @@ void main() { // Start listening to camera events stream to verify the proper CameraInitializedEvent is sent. camera.cameraEventStreamController.stream.listen((CameraEvent event) { - expect(event, TypeMatcher()); + expect(event, const TypeMatcher()); expect(event, equals(testCameraInitializedEvent)); }); @@ -213,12 +214,12 @@ void main() { test('onCameraInitialized stream emits CameraInitializedEvents', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - final int cameraId = 16; + const int cameraId = 16; final Stream eventStream = camera.onCameraInitialized(cameraId); final StreamQueue streamQueue = StreamQueue(eventStream); - final CameraInitializedEvent testEvent = CameraInitializedEvent( + const CameraInitializedEvent testEvent = CameraInitializedEvent( cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false); camera.cameraEventStreamController.add(testEvent); @@ -229,8 +230,8 @@ void main() { test('onCameraError stream emits errors caught by system services', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - final int cameraId = 27; - final String testErrorDescription = 'Test error description!'; + const int cameraId = 27; + const String testErrorDescription = 'Test error description!'; final Stream eventStream = camera.onCameraError(cameraId); final StreamQueue streamQueue = StreamQueue(eventStream); @@ -238,7 +239,7 @@ void main() { SystemServices.cameraErrorStreamController.add(testErrorDescription); expect(await streamQueue.next, - equals(CameraErrorEvent(cameraId, testErrorDescription))); + equals(const CameraErrorEvent(cameraId, testErrorDescription))); await streamQueue.cancel(); }); @@ -250,7 +251,7 @@ void main() { camera.onDeviceOrientationChanged(); final StreamQueue streamQueue = StreamQueue(eventStream); - final DeviceOrientationChangedEvent testEvent = + const DeviceOrientationChangedEvent testEvent = DeviceOrientationChangedEvent(DeviceOrientation.portraitDown); SystemServices.deviceOrientationChangedStreamController.add(testEvent); @@ -321,22 +322,22 @@ void main() { 'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - final int textureId = 75; + const int textureId = 75; camera.processCameraProvider = MockProcessCameraProvider(); camera.cameraSelector = MockCameraSelector(); camera.preview = MockPreview(); - FutureBuilder previewWidget = + final FutureBuilder previewWidget = camera.buildPreview(textureId) as FutureBuilder; - expect(previewWidget.builder(MockBuildContext(), AsyncSnapshot.nothing()), + expect(previewWidget.builder(MockBuildContext(), const AsyncSnapshot.nothing()), isA()); - expect(previewWidget.builder(MockBuildContext(), AsyncSnapshot.waiting()), + expect(previewWidget.builder(MockBuildContext(), const AsyncSnapshot.waiting()), isA()); expect( previewWidget.builder(MockBuildContext(), - AsyncSnapshot.withData(ConnectionState.active, null)), + const AsyncSnapshot.withData(ConnectionState.active, null)), isA()); }); @@ -344,17 +345,17 @@ void main() { 'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - final int textureId = 75; + const int textureId = 75; camera.processCameraProvider = MockProcessCameraProvider(); camera.cameraSelector = MockCameraSelector(); camera.preview = MockPreview(); - FutureBuilder previewWidget = + final FutureBuilder previewWidget = camera.buildPreview(textureId) as FutureBuilder; - Texture previewTexture = previewWidget.builder(MockBuildContext(), - AsyncSnapshot.withData(ConnectionState.done, null)) as Texture; + final Texture previewTexture = previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.done, null)) as Texture; expect(previewTexture.textureId, equals(textureId)); }); } From a21429a39e3368bdc59bdb7efaaced88c3fe35bb Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 10:22:24 -0800 Subject: [PATCH 08/23] Update changelog --- packages/camera/camera_android_camerax/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 50fdf586c5e7..6959ffb57398 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -9,4 +9,5 @@ * 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. From dcff8bc7f13e400201024aa4a8cc74c1c32abb76 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 10:29:37 -0800 Subject: [PATCH 09/23] Formatting --- .../lib/src/android_camera_camerax.dart | 3 ++- .../test/android_camera_camerax_test.dart | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) 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 92423cca36c1..ec271d431e93 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 @@ -362,7 +362,8 @@ class AndroidCameraCameraX extends CameraPlatform { /// functionality. @visibleForTesting Future getProcessCameraProviderInstance() async { - final ProcessCameraProvider instance = await ProcessCameraProvider.getInstance(); + final ProcessCameraProvider instance = + await ProcessCameraProvider.getInstance(); return instance; } 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 26ed88fda7fe..1d579082730e 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 @@ -331,9 +331,13 @@ void main() { final FutureBuilder previewWidget = camera.buildPreview(textureId) as FutureBuilder; - expect(previewWidget.builder(MockBuildContext(), const AsyncSnapshot.nothing()), + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.nothing()), isA()); - expect(previewWidget.builder(MockBuildContext(), const AsyncSnapshot.waiting()), + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.waiting()), isA()); expect( previewWidget.builder(MockBuildContext(), @@ -355,7 +359,8 @@ void main() { camera.buildPreview(textureId) as FutureBuilder; final Texture previewTexture = previewWidget.builder(MockBuildContext(), - const AsyncSnapshot.withData(ConnectionState.done, null)) as Texture; + const AsyncSnapshot.withData(ConnectionState.done, null)) + as Texture; expect(previewTexture.textureId, equals(textureId)); }); } From f1639077e326ed66c0ff6b3aee310afe0cd91ea2 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 11:27:51 -0800 Subject: [PATCH 10/23] Update todos with links and modify camera controller --- .../example/lib/camera_controller.dart | 155 ++++++++++++++++-- .../example/lib/main.dart | 4 +- .../lib/src/android_camera_camerax.dart | 4 + .../test/android_camera_camerax_test.dart | 5 +- 4 files changed, 147 insertions(+), 21 deletions(-) diff --git a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart index fd0551f21baa..8139dcdb0220 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; @@ -91,8 +92,6 @@ class CameraValue { /// The orientation of the currently running video recording. final DeviceOrientation? recordingOrientation; - // TODO(camsim99): Make fix here used for Optional regressions - /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -110,10 +109,10 @@ class CameraValue { bool? exposurePointSupported, bool? focusPointSupported, DeviceOrientation? deviceOrientation, - DeviceOrientation? lockedCaptureOrientation, - DeviceOrientation? recordingOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, bool? isPreviewPaused, - DeviceOrientation? previewPauseOrientation, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -126,12 +125,16 @@ class CameraValue { exposureMode: exposureMode ?? this.exposureMode, focusMode: focusMode ?? this.focusMode, deviceOrientation: deviceOrientation ?? this.deviceOrientation, - lockedCaptureOrientation: - lockedCaptureOrientation ?? this.lockedCaptureOrientation, - recordingOrientation: recordingOrientation ?? this.recordingOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, - previewPauseOrientation: - previewPauseOrientation ?? this.previewPauseOrientation, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -259,14 +262,16 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.pausePreview(_cameraId); value = value.copyWith( isPreviewPaused: true, - previewPauseOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } /// Resumes the current camera preview Future resumePreview() async { await CameraPlatform.instance.resumePreview(_cameraId); - value = value.copyWith(isPreviewPaused: false); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); } /// Captures an image and returns the file where it was saved. @@ -303,14 +308,14 @@ class CameraController extends ValueNotifier { /// Throws a [CameraException] if the capture fails. Future startVideoRecording( {Function(CameraImageData image)? streamCallback}) async { - // await CameraPlatform.instance.startVideoCapturing( - // VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, isStreamingImages: streamCallback != null, - recordingOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } /// Stops the video recording and returns the file where it was saved. @@ -326,6 +331,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isRecordingVideo: false, isRecordingPaused: false, + recordingOrientation: const Optional.absent(), ); return file; } @@ -394,12 +400,16 @@ class CameraController extends ValueNotifier { Future lockCaptureOrientation() async { await CameraPlatform.instance .lockCaptureOrientation(_cameraId, value.deviceOrientation); - value = value.copyWith(lockedCaptureOrientation: value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.of(value.deviceOrientation)); } /// Unlocks the capture orientation. Future unlockCaptureOrientation() async { await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); } /// Sets the focus mode for taking pictures. @@ -433,3 +443,112 @@ class CameraController extends ValueNotifier { } } } + +/// 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/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 806e9cf1362b..468b30def374 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -51,9 +51,9 @@ class _CameraExampleHomeState extends State VideoPlayerController? videoController; VoidCallback? videoPlayerListener; bool enableAudio = true; - // TODO(camsim99): Actually use this. + // TODO(camsim99): Use exposure offset values when exposure configuration supported. + // https://github.com/flutter/flutter/issues/120468 final double _minAvailableExposureOffset = 0.0; - // TODO(camsim99): Actually use this. final double _maxAvailableExposureOffset = 0.0; double _currentExposureOffset = 0.0; late AnimationController _flashModeControlRowAnimationController; 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 ec271d431e93..e3f04fcbf72d 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 @@ -176,6 +176,7 @@ class AndroidCameraCameraX extends CameraPlatform { }) 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: @@ -191,10 +192,12 @@ class AndroidCameraCameraX extends CameraPlatform { // 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; @@ -339,6 +342,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// a camera preview. ResolutionInfo? _getTargetResolutionForPreview(ResolutionPreset? resolution) { // TODO(camsim99): Implement resolution configuration. + // https://github.com/flutter/flutter/issues/120462 return null; } 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 1d579082730e..6a78d6be27a6 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 @@ -157,7 +157,10 @@ void main() { final ResolutionInfo testResolutionInfo = ResolutionInfo(width: resolutionWidth, height: resolutionHeight); - // TODO(camsim99): Modify this when camera configuration is supported and defualt values no longer being used. + // 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, From 285659d488820830e3e7104cd7306278f8a544f5 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 11:29:17 -0800 Subject: [PATCH 11/23] Format and add availableCameras --- .../camera/camera_android_camerax/example/lib/main.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 468b30def374..124e049eadfe 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -989,13 +989,7 @@ Future main() async { // Fetch the available cameras before initializing the app. try { WidgetsFlutterBinding.ensureInitialized(); - // TODO(camsim99): Use actual availableCameras method here - _cameras = [ - const CameraDescription( - name: 'cam', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90), - ]; + _cameras = await CameraPlatform.instance.availableCameras(); } on CameraException catch (e) { _logError(e.code, e.description); } From cffc0ac24ee59672c5d8d0d90fba386ad39cdd2e Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 11:47:10 -0800 Subject: [PATCH 12/23] Fix mocks --- .../test/android_camera_camerax_test.dart | 2 +- .../android_camera_camerax_test.mocks.dart | 387 ++++++++---------- 2 files changed, 178 insertions(+), 211 deletions(-) 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 6a78d6be27a6..4c3b465836bc 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 @@ -24,13 +24,13 @@ import 'package:mockito/mockito.dart'; import 'android_camera_camerax_test.mocks.dart'; @GenerateNiceMocks(>[ - MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), ]) +@GenerateMocks([BuildContext]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); 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 5740c15be63b..66c488eceb1d 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 @@ -5,17 +5,18 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i8; -import 'package:camera_android_camerax/src/camera.dart' as _i5; +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 _i4; +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 _i3; -import 'package:flutter/src/widgets/framework.dart' as _i2; -import 'package:flutter/src/widgets/notification_listener.dart' as _i6; +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 @@ -29,38 +30,29 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { - _FakeWidget_0( +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( Object parent, Invocation parentInvocation, ) : super( parent, parentInvocation, ); - - @override - String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => - super.toString(); } -class _FakeInheritedWidget_1 extends _i1.SmartFake - implements _i2.InheritedWidget { - _FakeInheritedWidget_1( +class _FakeCamera_1 extends _i1.SmartFake implements _i3.Camera { + _FakeCamera_1( Object parent, Invocation parentInvocation, ) : super( parent, parentInvocation, ); - - @override - String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => - super.toString(); } -class _FakeDiagnosticsNode_2 extends _i1.SmartFake - implements _i3.DiagnosticsNode { - _FakeDiagnosticsNode_2( +class _FakeWidget_2 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_2( Object parent, Invocation parentInvocation, ) : super( @@ -69,212 +61,47 @@ class _FakeDiagnosticsNode_2 extends _i1.SmartFake ); @override - String toString({ - _i3.TextTreeConfiguration? parentConfiguration, - _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, - }) => + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => super.toString(); } -class _FakeResolutionInfo_3 extends _i1.SmartFake - implements _i4.ResolutionInfo { - _FakeResolutionInfo_3( +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 _FakeCamera_4 extends _i1.SmartFake implements _i5.Camera { - _FakeCamera_4( +class _FakeDiagnosticsNode_4 extends _i1.SmartFake + implements _i6.DiagnosticsNode { + _FakeDiagnosticsNode_4( Object parent, Invocation parentInvocation, ) : super( parent, parentInvocation, ); -} -/// A class which mocks [BuildContext]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockBuildContext extends _i1.Mock implements _i2.BuildContext { - @override - _i2.Widget get widget => (super.noSuchMethod( - Invocation.getter(#widget), - returnValue: _FakeWidget_0( - this, - Invocation.getter(#widget), - ), - returnValueForMissingStub: _FakeWidget_0( - this, - Invocation.getter(#widget), - ), - ) as _i2.Widget); - @override - bool get mounted => (super.noSuchMethod( - Invocation.getter(#mounted), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); - @override - bool get debugDoingBuild => (super.noSuchMethod( - Invocation.getter(#debugDoingBuild), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); - @override - _i2.InheritedWidget dependOnInheritedElement( - _i2.InheritedElement? ancestor, { - Object? aspect, - }) => - (super.noSuchMethod( - Invocation.method( - #dependOnInheritedElement, - [ancestor], - {#aspect: aspect}, - ), - returnValue: _FakeInheritedWidget_1( - this, - Invocation.method( - #dependOnInheritedElement, - [ancestor], - {#aspect: aspect}, - ), - ), - returnValueForMissingStub: _FakeInheritedWidget_1( - this, - Invocation.method( - #dependOnInheritedElement, - [ancestor], - {#aspect: aspect}, - ), - ), - ) as _i2.InheritedWidget); - @override - void visitAncestorElements(_i2.ConditionalElementVisitor? visitor) => - super.noSuchMethod( - Invocation.method( - #visitAncestorElements, - [visitor], - ), - returnValueForMissingStub: null, - ); - @override - void visitChildElements(_i2.ElementVisitor? visitor) => super.noSuchMethod( - Invocation.method( - #visitChildElements, - [visitor], - ), - returnValueForMissingStub: null, - ); - @override - void dispatchNotification(_i6.Notification? notification) => - super.noSuchMethod( - Invocation.method( - #dispatchNotification, - [notification], - ), - returnValueForMissingStub: null, - ); - @override - _i3.DiagnosticsNode describeElement( - String? name, { - _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, - }) => - (super.noSuchMethod( - Invocation.method( - #describeElement, - [name], - {#style: style}, - ), - returnValue: _FakeDiagnosticsNode_2( - this, - Invocation.method( - #describeElement, - [name], - {#style: style}, - ), - ), - returnValueForMissingStub: _FakeDiagnosticsNode_2( - this, - Invocation.method( - #describeElement, - [name], - {#style: style}, - ), - ), - ) as _i3.DiagnosticsNode); @override - _i3.DiagnosticsNode describeWidget( - String? name, { - _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + String toString({ + _i6.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, }) => - (super.noSuchMethod( - Invocation.method( - #describeWidget, - [name], - {#style: style}, - ), - returnValue: _FakeDiagnosticsNode_2( - this, - Invocation.method( - #describeWidget, - [name], - {#style: style}, - ), - ), - returnValueForMissingStub: _FakeDiagnosticsNode_2( - this, - Invocation.method( - #describeWidget, - [name], - {#style: style}, - ), - ), - ) as _i3.DiagnosticsNode); - @override - List<_i3.DiagnosticsNode> describeMissingAncestor( - {required Type? expectedAncestorType}) => - (super.noSuchMethod( - Invocation.method( - #describeMissingAncestor, - [], - {#expectedAncestorType: expectedAncestorType}, - ), - returnValue: <_i3.DiagnosticsNode>[], - returnValueForMissingStub: <_i3.DiagnosticsNode>[], - ) as List<_i3.DiagnosticsNode>); - @override - _i3.DiagnosticsNode describeOwnershipChain(String? name) => - (super.noSuchMethod( - Invocation.method( - #describeOwnershipChain, - [name], - ), - returnValue: _FakeDiagnosticsNode_2( - this, - Invocation.method( - #describeOwnershipChain, - [name], - ), - ), - returnValueForMissingStub: _FakeDiagnosticsNode_2( - this, - Invocation.method( - #describeOwnershipChain, - [name], - ), - ), - ) as _i3.DiagnosticsNode); + super.toString(); } /// A class which mocks [Camera]. /// /// See the documentation for Mockito's code generation for more information. -class MockCamera extends _i1.Mock implements _i5.Camera {} +class MockCamera extends _i1.Mock implements _i3.Camera {} /// A class which mocks [CameraInfo]. /// @@ -330,12 +157,12 @@ class MockPreview extends _i1.Mock implements _i10.Preview { returnValueForMissingStub: null, ); @override - _i8.Future<_i4.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + _i8.Future<_i2.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( Invocation.method( #getResolutionInfo, [], ), - returnValue: _i8.Future<_i4.ResolutionInfo>.value(_FakeResolutionInfo_3( + returnValue: _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( this, Invocation.method( #getResolutionInfo, @@ -343,14 +170,14 @@ class MockPreview extends _i1.Mock implements _i10.Preview { ), )), returnValueForMissingStub: - _i8.Future<_i4.ResolutionInfo>.value(_FakeResolutionInfo_3( + _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( this, Invocation.method( #getResolutionInfo, [], ), )), - ) as _i8.Future<_i4.ResolutionInfo>); + ) as _i8.Future<_i2.ResolutionInfo>); } /// A class which mocks [ProcessCameraProvider]. @@ -370,7 +197,7 @@ class MockProcessCameraProvider extends _i1.Mock _i8.Future>.value(<_i7.CameraInfo>[]), ) as _i8.Future>); @override - _i8.Future<_i5.Camera> bindToLifecycle( + _i8.Future<_i3.Camera> bindToLifecycle( _i9.CameraSelector? cameraSelector, List<_i12.UseCase>? useCases, ) => @@ -382,7 +209,7 @@ class MockProcessCameraProvider extends _i1.Mock useCases, ], ), - returnValue: _i8.Future<_i5.Camera>.value(_FakeCamera_4( + returnValue: _i8.Future<_i3.Camera>.value(_FakeCamera_1( this, Invocation.method( #bindToLifecycle, @@ -392,7 +219,7 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - returnValueForMissingStub: _i8.Future<_i5.Camera>.value(_FakeCamera_4( + returnValueForMissingStub: _i8.Future<_i3.Camera>.value(_FakeCamera_1( this, Invocation.method( #bindToLifecycle, @@ -402,7 +229,7 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - ) as _i8.Future<_i5.Camera>); + ) as _i8.Future<_i3.Camera>); @override void unbind(List<_i12.UseCase>? useCases) => super.noSuchMethod( Invocation.method( @@ -420,3 +247,143 @@ class MockProcessCameraProvider extends _i1.Mock returnValueForMissingStub: null, ); } + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i4.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _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( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_3( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i4.InheritedWidget); + @override + void visitAncestorElements(_i4.ConditionalElementVisitor? 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], + ), + ), + ) as _i6.DiagnosticsNode); +} From 889802d996550b630ea2aa3e0eed6d500bd210c5 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 15:31:26 -0800 Subject: [PATCH 13/23] Try bumping mockito version --- packages/camera/camera_android_camerax/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 50e1d4b379a2..f7f00edb08a4 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -28,5 +28,5 @@ dev_dependencies: build_runner: ^2.1.4 flutter_test: sdk: flutter - mockito: ^5.1.0 + mockito: ^5.3.2 pigeon: ^3.2.6 From 24ee512bb82e1efb2bcb82dec431c20037758f22 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Fri, 10 Feb 2023 15:55:56 -0800 Subject: [PATCH 14/23] Review documentation --- .../lib/src/android_camera_camerax.dart | 28 +++++++++++-------- .../test/android_camera_camerax_test.dart | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) 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 e3f04fcbf72d..04863e9f5b1e 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 @@ -43,7 +43,7 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting Preview? preview; - /// Whether or not the [Preview] is currently bound to the lifecycle that the + /// Whether or not the [preview] is currently bound to the lifecycle that the /// [processCameraProvider] tracks. @visibleForTesting bool previewIsBound = false; @@ -57,8 +57,7 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting CameraSelector? cameraSelector; - /// The controller we need to broadcast the different events coming - /// from handleMethodCall, specific to camera events. + /// 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. @@ -118,14 +117,14 @@ class AndroidCameraCameraX extends CameraPlatform { return cameraDescriptions; } - /// Creates an uninitialized camera instance and returns the cameraId. + /// 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 cameraID, which represents the ID of the surface texture + /// 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 @@ -134,9 +133,10 @@ class AndroidCameraCameraX extends CameraPlatform { ResolutionPreset? resolutionPreset, { bool enableAudio = false, }) async { - // Must obtatin proper permissions before attempts to access a camera. + // Must obtatin 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 = @@ -163,7 +163,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// Initializes the camera on the device. /// /// Since initialization of a camera does not directly map as an operation to - /// the CameraX library, this method only retrieves information about the + /// the CameraX library, this method just retrieves information about the /// camera and sends a [CameraInitializedEvent]. /// /// [imageFormatGroup] is used to specify the image formatting used. @@ -220,13 +220,13 @@ class AndroidCameraCameraX extends CameraPlatform { processCameraProvider?.unbindAll(); } - /// Callback method for the initialization of a camera. + /// The camera has been initialized. @override Stream onCameraInitialized(int cameraId) { return _cameraEvents(cameraId).whereType(); } - /// Callback method for native camera errors. + /// The camera experienced an error. @override Stream onCameraError(int cameraId) { return SystemServices.cameraErrorStreamController.stream @@ -235,13 +235,15 @@ class AndroidCameraCameraX extends CameraPlatform { }); } - /// Callback method for changes in device orientation. + /// 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(); @@ -249,6 +251,8 @@ class AndroidCameraCameraX extends CameraPlatform { } /// Resume the paused preview for the selected camera. + /// + /// [cameraId] not used. @override Future resumePreview(int cameraId) async { await _bindPreviewToLifecycle(); @@ -276,7 +280,7 @@ class AndroidCameraCameraX extends CameraPlatform { // 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 + /// Binds [preview] instance to the camera lifecycle controlled by the /// [processCameraProvider]. Future _bindPreviewToLifecycle() async { assert(processCameraProvider != null); @@ -292,7 +296,7 @@ class AndroidCameraCameraX extends CameraPlatform { previewIsBound = true; } - /// Unbinds [Preview] instance to camera lifecycle controlled by the + /// Unbinds [preview] instance to camera lifecycle controlled by the /// [processCameraProvider]. void _unbindPreviewFromLifecycle() { if (preview == null || !previewIsBound) { 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 4c3b465836bc..831b3033aba6 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 @@ -115,7 +115,7 @@ void main() { enableAudio: enableAudio), equals(testSurfaceTextureId)); - // Verify permnissions are requested and the camera starts listening for device orientation changes. + // Verify permissions are requested and the camera starts listening for device orientation changes. expect(camera.cameraPermissionsRequested, isTrue); expect(camera.startedListeningForDeviceOrientationChanges, isTrue); From 573cf33372ab2941702ddd61c9749725728607b3 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Mon, 13 Feb 2023 14:06:49 -0800 Subject: [PATCH 15/23] Re-generate mocks --- .../test/android_camera_camerax_test.mocks.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 66c488eceb1d..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 @@ -295,7 +295,7 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { ), ) as _i4.InheritedWidget); @override - void visitAncestorElements(_i4.ConditionalElementVisitor? visitor) => + void visitAncestorElements(bool Function(_i4.Element)? visitor) => super.noSuchMethod( Invocation.method( #visitAncestorElements, From d08c087dbc61fb5a2587720a2de7ca5378c0ab6c Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 10:35:30 -0800 Subject: [PATCH 16/23] Fix typo --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 04863e9f5b1e..cdbca7ab644a 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 @@ -133,7 +133,7 @@ class AndroidCameraCameraX extends CameraPlatform { ResolutionPreset? resolutionPreset, { bool enableAudio = false, }) async { - // Must obtatin proper permissions before attempting to access a camera. + // Must obtain proper permissions before attempting to access a camera. await requestCameraPermissions(enableAudio); // Save CameraSelector that matches cameraDescription. From 1e1dd8b8b9a234c1b6f72c5e819fe00828e92b28 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 11:58:58 -0800 Subject: [PATCH 17/23] Fix another typo --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 cdbca7ab644a..8dcd410f34d4 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 @@ -38,8 +38,7 @@ class AndroidCameraCameraX extends CameraPlatform { // Instances used to configure and bind use cases to ProcessCameraProvider instance: - /// The [Preview] instance that can be instantiated adn configured to present - /// a live camera preview. + /// The [Preview] instance that can be configured to present a live camera preview. @visibleForTesting Preview? preview; From e1cea856643bd308ef9ace34c0c3cb8415e9e46c Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 14:48:23 -0800 Subject: [PATCH 18/23] Address review and fix bug --- .../example/lib/camera_controller.dart | 647 ++++++++++++++---- .../example/lib/camera_image.dart | 177 +++++ .../example/lib/camera_preview.dart | 10 +- .../example/lib/main.dart | 107 ++- .../lib/src/android_camera_camerax.dart | 38 +- .../lib/src/camera_selector.dart | 10 +- .../test/android_camera_camerax_test.dart | 9 +- 7 files changed, 799 insertions(+), 199 deletions(-) create mode 100644 packages/camera/camera_android_camerax/example/lib/camera_image.dart diff --git a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart index 8139dcdb0220..b1b5e9d4ceb9 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -4,31 +4,56 @@ 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 this.isRecordingPaused, + 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() @@ -40,7 +65,9 @@ class CameraValue { isRecordingPaused: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, + exposurePointSupported: false, focusMode: FocusMode.auto, + focusPointSupported: false, deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, ); @@ -57,8 +84,7 @@ class CameraValue { /// True when images from the camera are being streamed. final bool isStreamingImages; - /// True when video recording is paused. - final bool isRecordingPaused; + final bool _isRecordingPaused; /// True when the preview widget has been paused manually. final bool isPreviewPaused; @@ -66,11 +92,30 @@ class CameraValue { /// 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; @@ -80,6 +125,12 @@ class CameraValue { /// 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; @@ -101,6 +152,7 @@ class CameraValue { bool? isRecordingVideo, bool? isTakingPicture, bool? isStreamingImages, + String? errorDescription, Size? previewSize, bool? isRecordingPaused, FlashMode? flashMode, @@ -116,14 +168,18 @@ class CameraValue { }) { 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 ?? this.isRecordingPaused, + 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 @@ -143,11 +199,14 @@ class CameraValue { 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, ' @@ -158,10 +217,11 @@ class CameraValue { /// Controls a device camera. /// -/// This is a stripped-down version of the app-facing controller to serve as a -/// utility for the example and integration tests. It wraps only the calls that -/// have state associated with them, to consolidate tracking of camera state -/// outside of the overall example code. +/// 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( @@ -190,7 +250,10 @@ class CameraController extends ValueNotifier { /// When null the imageFormat will fallback to the platforms default. final ImageFormatGroup? imageFormatGroup; - late int _cameraId; + /// The id of a camera that hasn't been initialized. + @visibleForTesting + static const int kUninitializedCameraId = -1; + int _cameraId = kUninitializedCameraId; bool _isDisposed = false; StreamSubscription? _imageStreamSubscription; @@ -198,188 +261,472 @@ class CameraController extends ValueNotifier { 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 { - final Completer initializeCompleter = - Completer(); - - _deviceOrientationSubscription = CameraPlatform.instance - .onDeviceOrientationChanged() - .listen((DeviceOrientationChangedEvent event) { - value = value.copyWith( - deviceOrientation: event.orientation, + 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, - ); + _cameraId = await CameraPlatform.instance.createCamera( + description, + resolutionPreset, + enableAudio: enableAudio, + ); - CameraPlatform.instance - .onCameraInitialized(_cameraId) - .first - .then((CameraInitializedEvent event) { - initializeCompleter.complete(event); - }); + _unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((CameraInitializedEvent event) { + initializeCompleter.complete(event); + })); - await CameraPlatform.instance.initializeCamera( - _cameraId, - imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown, - ); + 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), - ); + 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 { - await CameraPlatform.instance.pausePreview(_cameraId); - value = value.copyWith( - isPreviewPaused: true, - previewPauseOrientation: Optional.of( - value.lockedCaptureOrientation ?? value.deviceOrientation)); + 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 { - await CameraPlatform.instance.resumePreview(_cameraId); - value = value.copyWith( - isPreviewPaused: false, - previewPauseOrientation: const Optional.absent()); + 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 { - value = value.copyWith(isTakingPicture: true); - final XFile file = await CameraPlatform.instance.takePicture(_cameraId); - value = value.copyWith(isTakingPicture: false); - return file; + _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. - Future startImageStream( - Function(CameraImageData image) onAvailable) async { - _imageStreamSubscription = CameraPlatform.instance - .onStreamedFrameAvailable(_cameraId) - .listen((CameraImageData imageData) { - onAvailable(imageData); - }); - value = value.copyWith(isStreamingImages: true); + /// + /// 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 { - value = value.copyWith(isStreamingImages: false); - await _imageStreamSubscription?.cancel(); - _imageStreamSubscription = null; + 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( - {Function(CameraImageData image)? streamCallback}) async { - await CameraPlatform.instance.startVideoCapturing( - VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); - value = value.copyWith( - isRecordingVideo: true, - isRecordingPaused: false, - isStreamingImages: streamCallback != null, - recordingOrientation: Optional.of( - value.lockedCaptureOrientation ?? value.deviceOrientation)); + {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) { - await stopImageStream(); + stopImageStream(); } - final XFile file = - await CameraPlatform.instance.stopVideoRecording(_cameraId); - value = value.copyWith( - isRecordingVideo: false, - isRecordingPaused: false, - recordingOrientation: const Optional.absent(), - ); - return file; + 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 { - await CameraPlatform.instance.pauseVideoRecording(_cameraId); - value = value.copyWith(isRecordingPaused: true); + _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 { - await CameraPlatform.instance.resumeVideoRecording(_cameraId); - value = value.copyWith(isRecordingPaused: false); + _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() { - return CameraPlatform.instance.buildPreview(_cameraId); + _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 { - await CameraPlatform.instance.setFlashMode(_cameraId, mode); - value = value.copyWith(flashMode: mode); + 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 { - await CameraPlatform.instance.setExposureMode(_cameraId, mode); - value = value.copyWith(exposureMode: mode); + 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(>[ - CameraPlatform.instance.getMinExposureOffset(_cameraId), - CameraPlatform.instance.getMaxExposureOffset(_cameraId) - ]); + 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 CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + final double stepSize = await getExposureOffsetStepSize(); if (stepSize > 0) { final double inv = 1.0 / stepSize; double roundedOffset = (offset * inv).roundToDouble() / inv; @@ -391,31 +738,72 @@ class CameraController extends ValueNotifier { offset = roundedOffset; } - return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + 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() async { - await CameraPlatform.instance - .lockCaptureOrientation(_cameraId, value.deviceOrientation); - value = value.copyWith( - lockedCaptureOrientation: - Optional.of(value.deviceOrientation)); + 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 { - await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); - value = value.copyWith( - lockedCaptureOrientation: const Optional.absent()); + 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 mode for taking pictures. - Future setFocusMode(FocusMode mode) async { - await CameraPlatform.instance.setFocusMode(_cameraId, mode); - value = value.copyWith(focusMode: mode); + /// 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. @@ -424,7 +812,7 @@ class CameraController extends ValueNotifier { if (_isDisposed) { return; } - _deviceOrientationSubscription?.cancel(); + _unawaited(_deviceOrientationSubscription?.cancel()); _isDisposed = true; super.dispose(); if (_initCalled != null) { @@ -433,6 +821,21 @@ class CameraController extends ValueNotifier { } } + 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 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 index 4fbdb3a2b069..c012e22b3dea 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -11,7 +11,8 @@ 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}); + const CameraPreview(this.controller, {Key? key, this.child}) + : super(key: key); /// The controller for the camera that the preview is shown for. final CameraController controller; @@ -25,13 +26,10 @@ class CameraPreview extends StatelessWidget { ? ValueListenableBuilder( valueListenable: controller, builder: (BuildContext context, Object? value, Widget? child) { - final double cameraAspectRatio = - controller.value.previewSize!.width / - controller.value.previewSize!.height; return AspectRatio( aspectRatio: _isLandscape() - ? cameraAspectRatio - : (1 / cameraAspectRatio), + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), child: Stack( fit: StackFit.expand, children: [ diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 124e049eadfe..e6a21d12e3a2 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:io'; -import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +17,7 @@ import 'camera_preview.dart'; /// Camera example home widget. class CameraExampleHome extends StatefulWidget { /// Default Constructor - const CameraExampleHome({super.key}); + const CameraExampleHome({Key? key}) : super(key: key); @override State createState() { @@ -36,6 +35,10 @@ IconData getCameraLensIcon(CameraLensDirection direction) { 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) { @@ -51,10 +54,8 @@ class _CameraExampleHomeState extends State VideoPlayerController? videoController; VoidCallback? videoPlayerListener; bool enableAudio = true; - // TODO(camsim99): Use exposure offset values when exposure configuration supported. - // https://github.com/flutter/flutter/issues/120468 - final double _minAvailableExposureOffset = 0.0; - final double _maxAvailableExposureOffset = 0.0; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; double _currentExposureOffset = 0.0; late AnimationController _flashModeControlRowAnimationController; late Animation _flashModeControlRowAnimation; @@ -62,6 +63,10 @@ class _CameraExampleHomeState extends State 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; @@ -69,7 +74,7 @@ class _CameraExampleHomeState extends State @override void initState() { super.initState(); - _ambiguate(WidgetsBinding.instance)?.addObserver(this); + WidgetsBinding.instance.addObserver(this); _flashModeControlRowAnimationController = AnimationController( duration: const Duration(milliseconds: 300), @@ -99,12 +104,13 @@ class _CameraExampleHomeState extends State @override void dispose() { - _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + WidgetsBinding.instance.removeObserver(this); _flashModeControlRowAnimationController.dispose(); _exposureModeControlRowAnimationController.dispose(); super.dispose(); } + // #docregion AppLifecycle @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = controller; @@ -120,6 +126,7 @@ class _CameraExampleHomeState extends State onNewCameraSelected(cameraController.description); } } + // #enddocregion AppLifecycle @override Widget build(BuildContext context) { @@ -182,11 +189,39 @@ class _CameraExampleHomeState extends State return Listener( onPointerDown: (_) => _pointers++, onPointerUp: (_) => _pointers--, - child: CameraPreview(controller!), + 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; + } + + _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; @@ -357,8 +392,7 @@ class _CameraExampleHomeState extends State () {}, // TODO(camsim99): Add functionality back here. onLongPress: () { if (controller != null) { - CameraPlatform.instance - .setExposurePoint(controller!.cameraId, null); + controller!.setExposurePoint(null); showInSnackBar('Resetting exposure point'); } }, @@ -440,8 +474,7 @@ class _CameraExampleHomeState extends State () {}, // TODO(camsim99): Add functionality back here. onLongPress: () { if (controller != null) { - CameraPlatform.instance - .setFocusPoint(controller!.cameraId, null); + controller!.setFocusPoint(null); } showInSnackBar('Resetting focus point'); }, @@ -481,8 +514,7 @@ class _CameraExampleHomeState extends State ), IconButton( icon: cameraController != null && - (!cameraController.value.isRecordingVideo || - cameraController.value.isRecordingPaused) + cameraController.value.isRecordingPaused ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), color: Colors.blue, @@ -519,7 +551,7 @@ class _CameraExampleHomeState extends State } if (_cameras.isEmpty) { - _ambiguate(SchedulerBinding.instance)?.addPostFrameCallback((_) async { + SchedulerBinding.instance.addPostFrameCallback((_) async { showInSnackBar('No camera found.'); }); return const Text('None'); @@ -559,12 +591,12 @@ class _CameraExampleHomeState extends State final CameraController cameraController = controller!; - final Point point = Point( + final Offset offset = Offset( details.localPosition.dx / constraints.maxWidth, details.localPosition.dy / constraints.maxHeight, ); - CameraPlatform.instance.setExposurePoint(cameraController.cameraId, point); - CameraPlatform.instance.setFocusPoint(cameraController.cameraId, point); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); } Future onNewCameraSelected(CameraDescription cameraDescription) async { @@ -593,10 +625,32 @@ class _CameraExampleHomeState extends State 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': @@ -621,10 +675,6 @@ class _CameraExampleHomeState extends State // iOS only showInSnackBar('Audio access is restricted.'); break; - case 'cameraPermission': - // Android & web only - showInSnackBar('Unknown permission error.'); - break; default: _showCameraException(e); break; @@ -973,7 +1023,7 @@ class _CameraExampleHomeState extends State /// CameraApp is the Main Application. class CameraApp extends StatelessWidget { /// Default Constructor - const CameraApp({super.key}); + const CameraApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -989,16 +1039,9 @@ Future main() async { // Fetch the available cameras before initializing the app. try { WidgetsFlutterBinding.ensureInitialized(); - _cameras = await CameraPlatform.instance.availableCameras(); + _cameras = await availableCameras(); } on CameraException catch (e) { _logError(e.code, e.description); } runApp(const CameraApp()); } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; 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 8dcd410f34d4..3656f71cb24f 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 @@ -25,8 +25,6 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } - // Instances used to access camera functionality: - /// The [ProcessCameraProvider] instance used to access camera functionality. @visibleForTesting ProcessCameraProvider? processCameraProvider; @@ -36,8 +34,6 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting Camera? camera; - // Instances used to configure and bind use cases to ProcessCameraProvider instance: - /// The [Preview] instance that can be configured to present a live camera preview. @visibleForTesting Preview? preview; @@ -49,8 +45,6 @@ class AndroidCameraCameraX extends CameraPlatform { bool _previewIsPaused = false; - // Instances used for camera configuration: - /// The [CameraSelector] used to configure the [processCameraProvider] to use /// the desired camera. @visibleForTesting @@ -71,14 +65,12 @@ class AndroidCameraCameraX extends CameraPlatform { cameraEventStreamController.stream .where((CameraEvent event) => event.cameraId == cameraId); - // Implementation of Flutter camera platform interface (CameraPlatform): - /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { final List cameraDescriptions = []; - processCameraProvider ??= await getProcessCameraProviderInstance(); + processCameraProvider ??= await ProcessCameraProvider.getInstance(); final List cameraInfos = await processCameraProvider!.getAvailableCameraInfos(); @@ -90,11 +82,11 @@ 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 createCameraSelector(CameraSelector.LENS_FACING_BACK) + if ((await createCameraSelector(CameraSelector.lensFacingBack) .filter([cameraInfo])) .isNotEmpty) { cameraLensDirection = CameraLensDirection.back; - } else if ((await createCameraSelector(CameraSelector.LENS_FACING_FRONT) + } else if ((await createCameraSelector(CameraSelector.lensFacingFront) .filter([cameraInfo])) .isNotEmpty) { cameraLensDirection = CameraLensDirection.front; @@ -139,14 +131,14 @@ class AndroidCameraCameraX extends CameraPlatform { final int cameraSelectorLensDirection = _getCameraSelectorLensDirection(cameraDescription.lensDirection); final bool cameraIsFrontFacing = - cameraSelectorLensDirection == CameraSelector.LENS_FACING_FRONT; + 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 getProcessCameraProviderInstance(); + processCameraProvider ??= await ProcessCameraProvider.getInstance(); // Configure Preview instance and bind to ProcessCameraProvider. final int targetRotation = @@ -154,6 +146,7 @@ class AndroidCameraCameraX extends CameraPlatform { final ResolutionInfo? targetResolution = _getTargetResolutionForPreview(resolutionPreset); preview = createPreview(targetRotation, targetResolution); + previewIsBound = false; final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(); return flutterSurfaceTextureId; @@ -315,11 +308,11 @@ class AndroidCameraCameraX extends CameraPlatform { int _getCameraSelectorLensDirection(CameraLensDirection lensDirection) { switch (lensDirection) { case CameraLensDirection.front: - return CameraSelector.LENS_FACING_FRONT; + return CameraSelector.lensFacingFront; case CameraLensDirection.back: - return CameraSelector.LENS_FACING_BACK; + return CameraSelector.lensFacingBack; case CameraLensDirection.external: - return CameraSelector.EXTERNAL; + return CameraSelector.lensFacingExternal; } } @@ -365,22 +358,13 @@ class AndroidCameraCameraX extends CameraPlatform { cameraIsFrontFacing, sensorOrientation); } - /// Retrives an instance of the [ProcessCameraProvider] to access camera - /// functionality. - @visibleForTesting - Future getProcessCameraProviderInstance() async { - final ProcessCameraProvider instance = - await ProcessCameraProvider.getInstance(); - return instance; - } - /// Returns a [CameraSelector] based on the specified camera lens direction. @visibleForTesting CameraSelector createCameraSelector(int cameraSelectorLensDirection) { switch (cameraSelectorLensDirection) { - case CameraSelector.LENS_FACING_FRONT: + case CameraSelector.lensFacingFront: return CameraSelector.getDefaultFrontCamera(); - case CameraSelector.LENS_FACING_BACK: + case CameraSelector.lensFacingBack: return CameraSelector.getDefaultBackCamera(); default: return CameraSelector(lensFacing: cameraSelectorLensDirection); 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 7ea93ec05d9e..b9b7bb4deaa3 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -46,17 +46,17 @@ class CameraSelector extends JavaObject { /// ID for front facing lens. /// /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_FRONT(). - static const int LENS_FACING_FRONT = 0; + static const int lensFacingFront = 0; /// ID for back facing lens. /// /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_BACK(). - static const int LENS_FACING_BACK = 1; + 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 EXTERNAL = 2; + static const int lensFacingExternal = 2; /// Selector for default front facing camera. static CameraSelector getDefaultFrontCamera({ @@ -66,7 +66,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_FRONT, + lensFacing: lensFacingFront, ); } @@ -78,7 +78,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/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 831b3033aba6..d4d0887a8319 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 @@ -391,17 +391,12 @@ class MockAndroidCameraCamerax extends AndroidCameraCameraX { return; } - @override - Future getProcessCameraProviderInstance() async { - return testProcessCameraProvider; - } - @override CameraSelector createCameraSelector(int cameraSelectorLensDirection) { switch (cameraSelectorLensDirection) { - case CameraSelector.LENS_FACING_FRONT: + case CameraSelector.lensFacingFront: return mockFrontCameraSelector; - case CameraSelector.LENS_FACING_BACK: + case CameraSelector.lensFacingBack: default: return mockBackCameraSelector; } From 7129845472069d953b4ad71fafec4c760de03844 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 14:57:20 -0800 Subject: [PATCH 19/23] Fix analyze --- .../example/lib/camera_preview.dart | 3 +-- .../camera/camera_android_camerax/example/lib/main.dart | 4 ++-- .../camera_android_camerax/test/camera_selector_test.dart | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart index c012e22b3dea..3baaaf8b1fa1 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -11,8 +11,7 @@ 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, {Key? key, this.child}) - : super(key: key); + const CameraPreview(this.controller, {super.key, this.child}); /// The controller for the camera that the preview is shown for. final CameraController controller; diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index e6a21d12e3a2..4fd965271baa 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -17,7 +17,7 @@ import 'camera_preview.dart'; /// Camera example home widget. class CameraExampleHome extends StatefulWidget { /// Default Constructor - const CameraExampleHome({Key? key}) : super(key: key); + const CameraExampleHome({super.key}); @override State createState() { @@ -1023,7 +1023,7 @@ class _CameraExampleHomeState extends State /// CameraApp is the Main Application. class CameraApp extends StatelessWidget { /// Default Constructor - const CameraApp({Key? key}) : super(key: key); + const CameraApp({super.key}); @override Widget build(BuildContext context) { 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)); }); }); } From 5f30bc70a1802406c7b75cf1c8622b1c5188586a Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 15:20:04 -0800 Subject: [PATCH 20/23] Fix tests --- .../test/android_camera_camerax_test.dart | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) 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 d4d0887a8319..8816892870d6 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 @@ -38,6 +38,7 @@ void main() { () async { // Arrange final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); final List returnData = [ { 'name': 'Camera 0', @@ -56,7 +57,7 @@ void main() { final MockCameraInfo mockBackCameraInfo = MockCameraInfo(); // Mock calls to native platform - when(camera.testProcessCameraProvider.getAvailableCameraInfos()).thenAnswer( + when(camera.processCameraProvider!.getAvailableCameraInfos()).thenAnswer( (_) async => [mockBackCameraInfo, mockFrontCameraInfo]); when(camera.mockBackCameraSelector .filter([mockFrontCameraInfo])) @@ -97,6 +98,7 @@ void main() { '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( @@ -122,10 +124,6 @@ void main() { // Verify CameraSelector is set with appropriate lens direction. expect(camera.cameraSelector, equals(camera.mockBackCameraSelector)); - // Verify ProcessCameraProvider instance is received. - expect( - camera.processCameraProvider, equals(camera.testProcessCameraProvider)); - // Verify the camera's Preview instance is instantiated properly. expect(camera.preview, equals(camera.testPreview)); @@ -142,6 +140,7 @@ void main() { 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; @@ -177,8 +176,8 @@ void main() { await camera.createCamera(testCameraDescription, testResolutionPreset, enableAudio: enableAudio); - when(camera.testProcessCameraProvider.bindToLifecycle( - camera.cameraSelector, [camera.testPreview])) + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])) .thenAnswer((_) async => mockCamera); when(camera.testPreview.getResolutionInfo()) .thenAnswer((_) async => testResolutionInfo); @@ -192,10 +191,10 @@ void main() { await camera.initializeCamera(cameraId); // Verify preview was bound and unbound to get preview resolution information. - verify(camera.testProcessCameraProvider - .bindToLifecycle(camera.cameraSelector, [camera.testPreview])); + verify(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.testPreview])); verify( - camera.testProcessCameraProvider.unbind([camera.testPreview])); + camera.processCameraProvider!.unbind([camera.testPreview])); // Check camera instance was received, but preview is no longer bound. expect(camera.camera, equals(mockCamera)); @@ -373,8 +372,6 @@ void main() { class MockAndroidCameraCamerax extends AndroidCameraCameraX { bool cameraPermissionsRequested = false; bool startedListeningForDeviceOrientationChanges = false; - final MockProcessCameraProvider testProcessCameraProvider = - MockProcessCameraProvider(); final MockPreview testPreview = MockPreview(); final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); From 8fc71f9149205eb3ae624ad0f930c63ff8590ea0 Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 15:27:07 -0800 Subject: [PATCH 21/23] Foramtting --- .../test/android_camera_camerax_test.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 8816892870d6..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 @@ -191,10 +191,9 @@ void main() { 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])); + 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)); From d50bd879ea951b720c08032c7e91e37302bcc43f Mon Sep 17 00:00:00 2001 From: camsim99 Date: Tue, 14 Feb 2023 16:16:44 -0800 Subject: [PATCH 22/23] Clear preview is paused --- .../camera_android_camerax/lib/src/android_camera_camerax.dart | 1 + 1 file changed, 1 insertion(+) 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 3656f71cb24f..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 @@ -147,6 +147,7 @@ class AndroidCameraCameraX extends CameraPlatform { _getTargetResolutionForPreview(resolutionPreset); preview = createPreview(targetRotation, targetResolution); previewIsBound = false; + _previewIsPaused = false; final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(); return flutterSurfaceTextureId; From de2fc295500cd354f506816984be900e82b2ee4a Mon Sep 17 00:00:00 2001 From: camsim99 Date: Wed, 15 Feb 2023 09:46:37 -0800 Subject: [PATCH 23/23] Add unknown lens --- .../camera_android_camerax/lib/src/camera_selector.dart | 5 +++++ 1 file changed, 5 insertions(+) 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 b9b7bb4deaa3..f1d3c5fdb663 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -58,6 +58,11 @@ class CameraSelector extends JavaObject { /// 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({ BinaryMessenger? binaryMessenger,