diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 42268d7417d..40159b68c97 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.21 + +* Implements NV21 support for image streaming. + ## 0.6.20+3 * Bumps com.google.guava:guava from 33.4.0-android to 33.4.8-android. diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index d6eb0cc36c0..b6a61aa1cb7 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -73,7 +73,9 @@ in the merged Android manifest of your app, then take the following steps to rem tools:node="remove" /> ``` -### Allowing image streaming in the background +### Notes on image streaming + +#### Allowing image streaming in the background As of Android 14, to allow for background image streaming, you will need to specify the foreground [`TYPE_CAMERA`][12] foreground service permission in your app's manifest. Specifically, in @@ -86,6 +88,12 @@ As of Android 14, to allow for background image streaming, you will need to spec ``` +#### Configuring NV21 image format + +If you initialize a `CameraController` with `ImageFormatGroup.nv21`, then streamed images will +still have the `ImageFormatGroup.yuv420` format, but their image data will be formatted in NV21. +See https://developer.android.com/reference/kotlin/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21(). + ## Contributing For more information on contributing to this plugin, see [`CONTRIBUTING.md`](CONTRIBUTING.md). diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index 9b21bc51fde..e9198381169 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -40,8 +40,8 @@ android { } defaultConfig { - // Many of the CameraX APIs require API 21. - minSdkVersion 21 + // CameraX APIs require API 23 or later. + minSdk = 23 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -72,7 +72,7 @@ android { dependencies { // CameraX core library using the camera2 implementation must use same version number. - def camerax_version = "1.5.0-beta01" + def camerax_version = "1.5.0-rc01" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt index 71041b05add..f4a84496acc 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// Autogenerated from Pigeon (v25.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -4761,7 +4761,8 @@ abstract class PigeonApiImageAnalysis( ) { abstract fun pigeon_defaultConstructor( resolutionSelector: androidx.camera.core.resolutionselector.ResolutionSelector?, - targetRotation: Long? + targetRotation: Long?, + outputImageFormat: Long? ): androidx.camera.core.ImageAnalysis abstract fun resolutionSelector( @@ -4800,10 +4801,12 @@ abstract class PigeonApiImageAnalysis( val resolutionSelectorArg = args[1] as androidx.camera.core.resolutionselector.ResolutionSelector? val targetRotationArg = args[2] as Long? + val outputImageFormatArg = args[3] as Long? val wrapped: List = try { api.pigeonRegistrar.instanceManager.addDartCreatedInstance( - api.pigeon_defaultConstructor(resolutionSelectorArg, targetRotationArg), + api.pigeon_defaultConstructor( + resolutionSelectorArg, targetRotationArg, outputImageFormatArg), pigeon_identifierArg) listOf(null) } catch (exception: Throwable) { diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisProxyApi.java index b0eadba8d0f..4a6e7ba9627 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisProxyApi.java @@ -21,7 +21,9 @@ class ImageAnalysisProxyApi extends PigeonApiImageAnalysis { @NonNull @Override public ImageAnalysis pigeon_defaultConstructor( - @Nullable ResolutionSelector resolutionSelector, @Nullable Long targetRotation) { + @Nullable ResolutionSelector resolutionSelector, + @Nullable Long targetRotation, + @Nullable Long outputImageFormat) { final ImageAnalysis.Builder builder = new ImageAnalysis.Builder(); if (resolutionSelector != null) { builder.setResolutionSelector(resolutionSelector); @@ -29,6 +31,11 @@ public ImageAnalysis pigeon_defaultConstructor( if (targetRotation != null) { builder.setTargetRotation(targetRotation.intValue()); } + + if (outputImageFormat != null) { + builder.setOutputImageFormat(outputImageFormat.intValue()); + } + return builder.build(); } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java index f4a60b19be5..f49f9a00c88 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java @@ -69,7 +69,7 @@ public void getCameraCharacteristic_returnsCorrespondingValueOfKeyWhenKeyNotReco assertEquals(value, api.getCameraCharacteristic(instance, key)); } - @Config(minSdk = 21) + @Config(minSdk = 23) @SuppressWarnings("unchecked") @Test public void getCameraCharacteristic_returnsExpectedCameraHardwareLevelWhenRequested() { diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java index 5b8aa9a9c0c..56b49ea362b 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java @@ -32,11 +32,13 @@ public void pigeon_defaultConstructor_createsExpectedImageAnalysisInstance() { final ResolutionSelector mockResolutionSelector = new ResolutionSelector.Builder().build(); final long targetResolution = Surface.ROTATION_0; + final long outputImageFormat = ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21; final ImageAnalysis imageAnalysis = - api.pigeon_defaultConstructor(mockResolutionSelector, targetResolution); + api.pigeon_defaultConstructor(mockResolutionSelector, targetResolution, outputImageFormat); assertEquals(imageAnalysis.getResolutionSelector(), mockResolutionSelector); assertEquals(imageAnalysis.getTargetRotation(), Surface.ROTATION_0); + assertEquals(imageAnalysis.getOutputImageFormat(), ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21); } @Test diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java index ad606cb5082..0883191b4dc 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java @@ -98,7 +98,7 @@ public void start_callsStartOnInstance() { .when(() -> ContextCompat.getMainExecutor(any())) .thenAnswer((Answer) invocation -> mock(Executor.class)); - when(instance.start(any(), any())).thenReturn(value); + when(instance.start(any(Executor.class), any())).thenReturn(value); assertEquals(value, api.start(instance, listener)); } diff --git a/packages/camera/camera_android_camerax/example/lib/camera_image.dart b/packages/camera/camera_android_camerax/example/lib/camera_image.dart index 43dc9d21e39..097e15f7c00 100644 --- a/packages/camera/camera_android_camerax/example/lib/camera_image.dart +++ b/packages/camera/camera_android_camerax/example/lib/camera_image.dart @@ -83,6 +83,9 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { // android.graphics.ImageFormat.YUV_420_888 case 35: return ImageFormatGroup.yuv420; + // android.graphics.ImageFormat.NV21 + case 17: + return ImageFormatGroup.nv21; // android.graphics.ImageFormat.JPEG case 256: return ImageFormatGroup.jpeg; 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 2055e87ae49..5527ce65d64 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 @@ -174,15 +174,30 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting StreamController? cameraImageDataStreamController; - /// Constant representing the multi-plane Android YUV 420 image format. + /// Constant representing the multi-plane Android YUV 420 image format used by ImageProxy. /// /// See https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888. - static const int imageFormatYuv420_888 = 35; + static const int imageProxyFormatYuv420_888 = 35; - /// Constant representing the compressed JPEG image format. + /// Constant representing the NV21 image format used by ImageProxy. + /// + /// See https://developer.android.com/reference/android/graphics/ImageFormat#NV21. + static const int imageProxyFormatNv21 = 17; + + /// Constant representing the compressed JPEG image format used by ImageProxy. /// /// See https://developer.android.com/reference/android/graphics/ImageFormat#JPEG. - static const int imageFormatJpeg = 256; + static const int imageProxyFormatJpeg = 256; + + /// Constant representing the YUV 420 image format used for configuring ImageAnalysis. + /// + /// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888() + static const int imageAnalysisOutputImageFormatYuv420_888 = 1; + + /// Constant representing the NV21 image format used for configuring ImageAnalysis. + /// + /// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21(). + static const int imageAnalysisOutputImageFormatNv21 = 3; /// Error code indicating a [ZoomState] was requested, but one has not been /// set for the camera in use. @@ -269,6 +284,12 @@ class AndroidCameraCameraX extends CameraPlatform { /// A map to associate a [CameraInfo] with its camera name. final Map _savedCameras = {}; + /// The preset resolution selector for the camera. + ResolutionSelector? _presetResolutionSelector; + + /// The ID of the surface texture that the camera preview is drawn to. + late int _flutterSurfaceTextureId; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -380,8 +401,9 @@ class AndroidCameraCameraX extends CameraPlatform { ); // Determine ResolutionSelector and QualitySelector based on // resolutionPreset for camera UseCases. - final ResolutionSelector? presetResolutionSelector = - _getResolutionSelectorFromPreset(mediaSettings?.resolutionPreset); + _presetResolutionSelector = _getResolutionSelectorFromPreset( + mediaSettings?.resolutionPreset, + ); final QualitySelector? presetQualitySelector = _getQualitySelectorFromPreset(mediaSettings?.resolutionPreset); @@ -391,55 +413,26 @@ class AndroidCameraCameraX extends CameraPlatform { // Configure Preview instance. preview = proxy.newPreview( - resolutionSelector: presetResolutionSelector, + resolutionSelector: _presetResolutionSelector, /* use CameraX default target rotation */ targetRotation: null, ); - final int flutterSurfaceTextureId = await preview!.setSurfaceProvider( + _flutterSurfaceTextureId = await preview!.setSurfaceProvider( systemServicesManager, ); // Configure ImageCapture instance. imageCapture = proxy.newImageCapture( - resolutionSelector: presetResolutionSelector, + resolutionSelector: _presetResolutionSelector, /* use CameraX default target rotation */ targetRotation: await deviceOrientationManager.getDefaultDisplayRotation(), ); - // Configure ImageAnalysis instance. - // Defaults to YUV_420_888 image format. - imageAnalysis = proxy.newImageAnalysis( - resolutionSelector: presetResolutionSelector, - /* use CameraX default target rotation */ targetRotation: null, - ); - // Configure VideoCapture and Recorder instances. recorder = proxy.newRecorder(qualitySelector: presetQualitySelector); videoCapture = proxy.withOutputVideoCapture(videoOutput: recorder!); - // Bind configured UseCases to ProcessCameraProvider instance & mark Preview - // instance as bound but not paused. Video capture is bound at first use - // instead of here. - camera = await processCameraProvider!.bindToLifecycle( - cameraSelector!, - [preview!, imageCapture!, imageAnalysis!], - ); - await _updateCameraInfoAndLiveCameraState(flutterSurfaceTextureId); - previewInitiallyBound = true; - _previewIsPaused = false; - // Retrieve info required for correcting the rotation of the camera preview // if necessary. - - final Camera2CameraInfo camera2CameraInfo = proxy.fromCamera2CameraInfo( - cameraInfo: cameraInfo!, - ); - sensorOrientationDegrees = - ((await camera2CameraInfo.getCameraCharacteristic( - proxy.sensorOrientationCameraCharacteristics(), - ))! - as int) - .toDouble(); - sensorOrientationDegrees = cameraDescription.sensorOrientation.toDouble(); _handlesCropAndRotation = await preview! .surfaceProducerHandlesCropAndRotation(); @@ -449,35 +442,59 @@ class AndroidCameraCameraX extends CameraPlatform { _initialDefaultDisplayRotation = await deviceOrientationManager .getDefaultDisplayRotation(); - return flutterSurfaceTextureId; + return _flutterSurfaceTextureId; } /// Initializes the camera on the device. /// - /// Since initialization of a camera does not directly map as an operation to - /// the CameraX library, this method just retrieves information about the - /// camera and sends a [CameraInitializedEvent]. + /// Specifically, this method: + /// * Configures the [ImageAnalysis] instance according to the specified + /// [imageFormatGroup] + /// * Binds the configured [Preview], [ImageCapture], and [ImageAnalysis] + /// instances to the [ProcessCameraProvider] instance. + /// * Retrieves information about the camera and sends a [CameraInitializedEvent]. /// /// [imageFormatGroup] is used to specify the image format used for image - /// streaming, but CameraX currently only supports YUV_420_888 (supported by - /// Flutter) and RGBA (not supported by Flutter). CameraX uses YUV_420_888 - /// by default, so [imageFormatGroup] is not used. + /// streaming, but CameraX currently only supports YUV_420_888 (the CameraX default), + /// NV21, and RGBA (not supported by Flutter). @override Future initializeCamera( int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) async { - // Configure CameraInitializedEvent to send as representation of a - // configured camera: - // Retrieve preview resolution. + // If preview has not been created, then no camera has been created, which signals that + // createCamera was not called before initializeCamera. if (preview == null) { - // No camera has been created; createCamera must be called before initializeCamera. throw CameraException( 'cameraNotFound', "Camera not found. Please call the 'create' method before calling 'initialize'", ); } + // Configure ImageAnalysis instance. + // Defaults to YUV_420_888 image format. + imageAnalysis = proxy.newImageAnalysis( + resolutionSelector: _presetResolutionSelector, + outputImageFormat: _imageAnalysisOutputFormatFromImageFormatGroup( + imageFormatGroup, + ), + /* use CameraX default target rotation */ targetRotation: null, + ); + + // Bind configured UseCases to ProcessCameraProvider instance & mark Preview + // instance as bound but not paused. Video capture is bound at first use + // instead of here. + camera = await processCameraProvider!.bindToLifecycle( + cameraSelector!, + [preview!, imageCapture!, imageAnalysis!], + ); + await _updateCameraInfoAndLiveCameraState(_flutterSurfaceTextureId); + previewInitiallyBound = true; + _previewIsPaused = false; + + // Configure CameraInitializedEvent to send as representation of a + // configured camera: + // Retrieve preview resolution. final ResolutionInfo previewResolutionInfo = (await preview! .getResolutionInfo())!; @@ -1214,6 +1231,10 @@ class AndroidCameraCameraX extends CameraPlatform { /// implementation using a broadcast [StreamController], which does not /// support those operations. /// + /// If the camera was initialized with [ImageFormatGroup.nv21], then the + /// streamed images will still have format [ImageFormatGroup.yuv420], but + /// their image data will be formatted in NV21. + /// /// [cameraId] and [options] are not used. @override Stream onStreamedFrameAvailable( @@ -1326,14 +1347,30 @@ class AndroidCameraCameraX extends CameraPlatform { await imageAnalysis!.clearAnalyzer(); } - /// Converts between Android ImageFormat constants and [ImageFormatGroup]s. + /// Converts [ImageFormatGroup]s to Android ImageAnalysis output format constants. + /// + /// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis. + int? _imageAnalysisOutputFormatFromImageFormatGroup(dynamic format) { + switch (format) { + case ImageFormatGroup.yuv420: + return imageAnalysisOutputImageFormatYuv420_888; + case ImageFormatGroup.nv21: + return imageAnalysisOutputImageFormatNv21; + } + + return null; + } + + /// Converts from Android ImageFormat constants to [ImageFormatGroup]s. /// /// See https://developer.android.com/reference/android/graphics/ImageFormat. ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { switch (data) { - case imageFormatYuv420_888: // android.graphics.ImageFormat.YUV_420_888 + case imageProxyFormatYuv420_888: // android.graphics.ImageFormat.YUV_420_888 return ImageFormatGroup.yuv420; - case imageFormatJpeg: // android.graphics.ImageFormat.JPEG + case imageProxyFormatNv21: // android.graphics.ImageFormat.NV21 + return ImageFormatGroup.nv21; + case imageProxyFormatJpeg: // android.graphics.ImageFormat.JPEG return ImageFormatGroup.jpeg; } diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index b2f730b89c1..b1e1f737ac3 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -1,11 +1,12 @@ // 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. -// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// Autogenerated from Pigeon (v25.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; +import 'dart:io' show Platform; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' @@ -128,6 +129,9 @@ class PigeonInstanceManager { late final void Function(int) onWeakReferenceRemoved; static PigeonInstanceManager _initInstance() { + if (Platform.environment['FLUTTER_TEST'] == 'true') { + return PigeonInstanceManager(onWeakReferenceRemoved: (_) {}); + } WidgetsFlutterBinding.ensureInitialized(); final _PigeonInternalInstanceManagerApi api = _PigeonInternalInstanceManagerApi(); @@ -5916,6 +5920,7 @@ class ImageAnalysis extends UseCase { super.pigeon_instanceManager, this.resolutionSelector, int? targetRotation, + int? outputImageFormat, }) : super.pigeon_detached() { final int pigeonVar_instanceIdentifier = pigeon_instanceManager .addDartCreatedInstance(this); @@ -5930,13 +5935,13 @@ class ImageAnalysis extends UseCase { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [ - pigeonVar_instanceIdentifier, - resolutionSelector, - targetRotation, - ], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([ + pigeonVar_instanceIdentifier, + resolutionSelector, + targetRotation, + outputImageFormat, + ]); () async { final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart index 6ad4e335ecc..dfda9a7bc6f 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -207,6 +207,7 @@ class CameraXProxy { /// Constructs [ImageAnalysis]. final ImageAnalysis Function({ int? targetRotation, + int? outputImageFormat, ResolutionSelector? resolutionSelector, BinaryMessenger? pigeon_binaryMessenger, PigeonInstanceManager? pigeon_instanceManager, diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 102fcd2d288..f9114df3f7c 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -792,7 +792,7 @@ abstract class ZoomState { ), ) abstract class ImageAnalysis extends UseCase { - ImageAnalysis(int? targetRotation); + ImageAnalysis(int? targetRotation, int? outputImageFormat); late final ResolutionSelector? resolutionSelector; diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index ae8cb16d85b..eeed6e6a4c1 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.20+3 +version: 0.6.21 environment: sdk: ^3.8.1 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 14141986808..766c4ddedfe 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 @@ -114,9 +114,9 @@ void main() { return cameraClosingEventSent && cameraErrorSent; } - /// CameraXProxy for testing functionality related to the camera resolution - /// preset (setting expected ResolutionSelectors, QualitySelectors, etc.). - CameraXProxy getProxyForTestingResolutionPreset( + /// CameraXProxy for testing functionality related to the configuration + /// of CameraX UseCases. + CameraXProxy getProxyForTestingUseCaseConfiguration( MockProcessCameraProvider mockProcessCameraProvider, { ResolutionFilter Function({ required CameraSize preferredSize, @@ -152,6 +152,16 @@ void main() { PigeonInstanceManager? pigeon_instanceManager, })? newPreview, + ImageAnalysis Function({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? outputImageFormat, + int? targetRotation, + })? + newImageAnalysis, }) { late final CameraXProxy proxy; final AspectRatioStrategy ratio_4_3FallbackAutoStrategyAspectRatioStrategy = @@ -199,10 +209,15 @@ void main() { PigeonInstanceManager? pigeon_instanceManager, }) { final MockPreview mockPreview = MockPreview(); + final ResolutionInfo testResolutionInfo = + ResolutionInfo.pigeon_detached(resolution: MockCameraSize()); when( mockPreview.surfaceProducerHandlesCropAndRotation(), ).thenAnswer((_) async => false); when(mockPreview.resolutionSelector).thenReturn(resolutionSelector); + when( + mockPreview.getResolutionInfo(), + ).thenAnswer((_) async => testResolutionInfo); return mockPreview; }, newImageCapture: @@ -248,8 +263,10 @@ void main() { return MockVideoCapture(); }, newImageAnalysis: + newImageAnalysis ?? ({ int? targetRotation, + int? outputImageFormat, ResolutionSelector? resolutionSelector, // ignore: non_constant_identifier_names BinaryMessenger? pigeon_binaryMessenger, @@ -874,6 +891,7 @@ void main() { newImageAnalysis: ({ int? targetRotation, + int? outputImageFormat, ResolutionSelector? resolutionSelector, // ignore: non_constant_identifier_names BinaryMessenger? pigeon_binaryMessenger, @@ -1082,22 +1100,11 @@ void main() { // Verify the camera's Preview instance has its surface provider set. verify(camera.preview!.setSurfaceProvider(mockSystemServicesManager)); - - // Verify the camera state observer is updated. - expect( - await testCameraClosingObserver( - camera, - testSurfaceTextureId, - verify(mockLiveCameraState.observe(captureAny)).captured.single - as Observer, - ), - isTrue, - ); }, ); test( - 'createCamera binds Preview and ImageCapture use cases to ProcessCameraProvider instance', + 'createCamera and initializeCamera properly set preset resolution selection strategy for non-video capture use cases', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); const CameraLensDirection testLensDirection = CameraLensDirection.back; @@ -1107,317 +1114,188 @@ void main() { lensDirection: testLensDirection, sensorOrientation: testSensorOrientation, ); - const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; const bool enableAudio = true; + final MockCamera mockCamera = MockCamera(); // Mock/Detached objects for (typically attached) objects created by // createCamera. final MockProcessCameraProvider mockProcessCameraProvider = MockProcessCameraProvider(); - final MockPreview mockPreview = MockPreview(); - final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); - final MockImageCapture mockImageCapture = MockImageCapture(); - final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); - final MockRecorder mockRecorder = MockRecorder(); - final MockVideoCapture mockVideoCapture = MockVideoCapture(); - final MockCamera mockCamera = MockCamera(); final MockCameraInfo mockCameraInfo = MockCameraInfo(); - final MockCameraControl mockCameraControl = MockCameraControl(); - final MockCamera2CameraInfo mockCamera2CameraInfo = - MockCamera2CameraInfo(); - final MockCameraCharacteristicsKey mockCameraCharacteristicsKey = - MockCameraCharacteristicsKey(); - // Tell plugin to create mock/detached objects and stub method calls for the - // testing of createCamera. - camera.proxy = CameraXProxy( - getInstanceProcessCameraProvider: - ({ - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) async { - return mockProcessCameraProvider; - }, - newCameraSelector: - ({ - LensFacing? requireLensFacing, - CameraInfo? cameraInfoForFilter, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - switch (requireLensFacing) { - case LensFacing.front: - return MockCameraSelector(); - case LensFacing.back: - case LensFacing.external: - case LensFacing.unknown: - case null: - } + when( + mockProcessCameraProvider.bindToLifecycle(any, any), + ).thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when( + mockCameraInfo.getCameraState(), + ).thenAnswer((_) async => MockLiveCameraState()); + camera.processCameraProvider = mockProcessCameraProvider; - return mockBackCameraSelector; - }, - newPreview: - ({ - int? targetRotation, - ResolutionSelector? resolutionSelector, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return mockPreview; - }, - newImageCapture: - ({ - int? targetRotation, - CameraXFlashMode? flashMode, - ResolutionSelector? resolutionSelector, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return mockImageCapture; - }, - newRecorder: - ({ - int? aspectRatio, - int? targetVideoEncodingBitRate, - QualitySelector? qualitySelector, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return mockRecorder; - }, - withOutputVideoCapture: + // Tell plugin to create mock/detached objects for testing createCamera + // as needed. + camera.proxy = getProxyForTestingUseCaseConfiguration( + mockProcessCameraProvider, + ); + + // Test non-null resolution presets. + for (final ResolutionPreset resolutionPreset in ResolutionPreset.values) { + final int flutterSurfaceTextureId = await camera.createCamera( + testCameraDescription, + resolutionPreset, + enableAudio: enableAudio, + ); + await camera.initializeCamera(flutterSurfaceTextureId); + + late final CameraSize? expectedBoundSize; + final PigeonInstanceManager testInstanceManager = PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + switch (resolutionPreset) { + case ResolutionPreset.low: + expectedBoundSize = CameraSize.pigeon_detached( + width: 320, + height: 240, + pigeon_instanceManager: testInstanceManager, + ); + case ResolutionPreset.medium: + expectedBoundSize = CameraSize.pigeon_detached( + width: 720, + height: 480, + pigeon_instanceManager: testInstanceManager, + ); + case ResolutionPreset.high: + expectedBoundSize = CameraSize.pigeon_detached( + width: 1280, + height: 720, + pigeon_instanceManager: testInstanceManager, + ); + case ResolutionPreset.veryHigh: + expectedBoundSize = CameraSize.pigeon_detached( + width: 1920, + height: 1080, + pigeon_instanceManager: testInstanceManager, + ); + case ResolutionPreset.ultraHigh: + expectedBoundSize = CameraSize.pigeon_detached( + width: 3840, + height: 2160, + pigeon_instanceManager: testInstanceManager, + ); + case ResolutionPreset.max: + continue; + } + + final CameraSize? previewSize = await camera + .preview! + .resolutionSelector! + .resolutionStrategy! + .getBoundSize(); + expect(previewSize?.width, equals(expectedBoundSize.width)); + expect(previewSize?.height, equals(expectedBoundSize.height)); + expect( + await camera.preview!.resolutionSelector!.resolutionStrategy! + .getFallbackRule(), + ResolutionStrategyFallbackRule.closestLowerThenHigher, + ); + + final CameraSize? imageCaptureSize = await camera + .imageCapture! + .resolutionSelector! + .resolutionStrategy! + .getBoundSize(); + expect(imageCaptureSize?.width, equals(expectedBoundSize.width)); + expect(imageCaptureSize?.height, equals(expectedBoundSize.height)); + expect( + await camera.imageCapture!.resolutionSelector!.resolutionStrategy! + .getFallbackRule(), + ResolutionStrategyFallbackRule.closestLowerThenHigher, + ); + + final CameraSize? imageAnalysisSize = await camera + .imageAnalysis! + .resolutionSelector! + .resolutionStrategy! + .getBoundSize(); + expect(imageAnalysisSize?.width, equals(expectedBoundSize.width)); + expect(imageAnalysisSize?.height, equals(expectedBoundSize.height)); + expect( + await camera.imageAnalysis!.resolutionSelector!.resolutionStrategy! + .getFallbackRule(), + ResolutionStrategyFallbackRule.closestLowerThenHigher, + ); + } + + // Test max case. + await camera.createCamera( + testCameraDescription, + ResolutionPreset.max, + enableAudio: true, + ); + + expect( + camera.preview!.resolutionSelector!.resolutionStrategy, + equals(camera.proxy.highestAvailableStrategyResolutionStrategy()), + ); + expect( + camera.imageCapture!.resolutionSelector!.resolutionStrategy, + equals(camera.proxy.highestAvailableStrategyResolutionStrategy()), + ); + expect( + camera.imageAnalysis!.resolutionSelector!.resolutionStrategy, + equals(camera.proxy.highestAvailableStrategyResolutionStrategy()), + ); + + // Test null case. + final int flutterSurfaceTextureId = await camera.createCamera( + testCameraDescription, + null, + ); + await camera.initializeCamera(flutterSurfaceTextureId); + + expect(camera.preview!.resolutionSelector, isNull); + expect(camera.imageCapture!.resolutionSelector, isNull); + expect(camera.imageAnalysis!.resolutionSelector, isNull); + }, + ); + + test( + 'createCamera and initializeCamera properly set filter for resolution preset for non-video capture use cases', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const CameraLensDirection testLensDirection = CameraLensDirection.front; + const int testSensorOrientation = 180; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation, + ); + const bool enableAudio = true; + final MockCamera mockCamera = MockCamera(); + + // Mock/Detached objects for (typically attached) objects created by + // createCamera. + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + + // Tell plugin to create mock/detached objects for testing createCamera + // as needed. + CameraSize? lastSetPreferredSize; + camera.proxy = getProxyForTestingUseCaseConfiguration( + mockProcessCameraProvider, + createWithOnePreferredSizeResolutionFilter: ({ - required VideoOutput videoOutput, + required CameraSize preferredSize, // ignore: non_constant_identifier_names BinaryMessenger? pigeon_binaryMessenger, // ignore: non_constant_identifier_names PigeonInstanceManager? pigeon_instanceManager, }) { - return mockVideoCapture; + lastSetPreferredSize = preferredSize; + return MockResolutionFilter(); }, - newImageAnalysis: - ({ - int? targetRotation, - ResolutionSelector? resolutionSelector, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return mockImageAnalysis; - }, - newResolutionStrategy: - ({ - required CameraSize boundSize, - required ResolutionStrategyFallbackRule fallbackRule, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockResolutionStrategy(); - }, - newResolutionSelector: - ({ - AspectRatioStrategy? aspectRatioStrategy, - ResolutionStrategy? resolutionStrategy, - ResolutionFilter? resolutionFilter, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockResolutionSelector(); - }, - fromQualitySelector: - ({ - required VideoQuality quality, - FallbackStrategy? fallbackStrategy, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockQualitySelector(); - }, - newObserver: - ({ - required void Function(Observer, T) onChanged, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return Observer.detached( - onChanged: onChanged, - pigeon_instanceManager: PigeonInstanceManager( - onWeakReferenceRemoved: (_) {}, - ), - ); - }, - newSystemServicesManager: - ({ - required void Function(SystemServicesManager, String) - onCameraError, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockSystemServicesManager(); - }, - newDeviceOrientationManager: - ({ - required void Function(DeviceOrientationManager, String) - onDeviceOrientationChanged, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - final MockDeviceOrientationManager manager = - MockDeviceOrientationManager(); - when(manager.getUiOrientation()).thenAnswer((_) async { - return 'PORTRAIT_UP'; - }); - return manager; - }, - newAspectRatioStrategy: - ({ - required AspectRatio preferredAspectRatio, - required AspectRatioStrategyFallbackRule fallbackRule, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockAspectRatioStrategy(); - }, - createWithOnePreferredSizeResolutionFilter: - ({ - required CameraSize preferredSize, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockResolutionFilter(); - }, - fromCamera2CameraInfo: - ({ - required CameraInfo cameraInfo, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - when( - mockCamera2CameraInfo.getCameraCharacteristic( - mockCameraCharacteristicsKey, - ), - ).thenAnswer((_) async => testSensorOrientation); - return mockCamera2CameraInfo; - }, - newCameraSize: - ({ - required int width, - required int height, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockCameraSize(); - }, - sensorOrientationCameraCharacteristics: () { - return mockCameraCharacteristicsKey; - }, - lowerQualityOrHigherThanFallbackStrategy: - ({ - required VideoQuality quality, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - return MockFallbackStrategy(); - }, - ); - - when( - mockProcessCameraProvider.bindToLifecycle( - mockBackCameraSelector, - [mockPreview, mockImageCapture, mockImageAnalysis], - ), - ).thenAnswer((_) async => mockCamera); - when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); - when( - mockCameraInfo.getCameraState(), - ).thenAnswer((_) async => MockLiveCameraState()); - when(mockCamera.cameraControl).thenAnswer((_) => mockCameraControl); - - camera.processCameraProvider = mockProcessCameraProvider; - - await camera.createCameraWithSettings( - testCameraDescription, - const MediaSettings( - resolutionPreset: testResolutionPreset, - fps: 15, - videoBitrate: 2000000, - audioBitrate: 64000, - enableAudio: enableAudio, - ), - ); - - // Verify expected UseCases were bound. - verify( - camera.processCameraProvider!.bindToLifecycle( - camera.cameraSelector!, - [mockPreview, mockImageCapture, mockImageAnalysis], - ), - ); - - // Verify the camera's CameraInfo instance got updated. - expect(camera.cameraInfo, equals(mockCameraInfo)); - - // Verify camera's CameraControl instance got updated. - expect(camera.cameraControl, equals(mockCameraControl)); - - // Verify preview has been marked as bound to the camera lifecycle by - // createCamera. - expect(camera.previewInitiallyBound, isTrue); - }, - ); - - test( - 'createCamera properly sets preset resolution selection strategy for non-video capture use cases', - () async { - final AndroidCameraCameraX camera = AndroidCameraCameraX(); - const CameraLensDirection testLensDirection = CameraLensDirection.back; - const int testSensorOrientation = 90; - const CameraDescription testCameraDescription = CameraDescription( - name: 'cameraName', - lensDirection: testLensDirection, - sensorOrientation: testSensorOrientation, ); - const bool enableAudio = true; - final MockCamera mockCamera = MockCamera(); - - // Mock/Detached objects for (typically attached) objects created by - // createCamera. - final MockProcessCameraProvider mockProcessCameraProvider = - MockProcessCameraProvider(); - final MockCameraInfo mockCameraInfo = MockCameraInfo(); when( mockProcessCameraProvider.bindToLifecycle(any, any), @@ -1428,70 +1306,74 @@ void main() { ).thenAnswer((_) async => MockLiveCameraState()); camera.processCameraProvider = mockProcessCameraProvider; - // Tell plugin to create mock/detached objects for testing createCamera - // as needed. - camera.proxy = getProxyForTestingResolutionPreset( - mockProcessCameraProvider, - ); - // Test non-null resolution presets. for (final ResolutionPreset resolutionPreset in ResolutionPreset.values) { - await camera.createCamera( + final int flutterSurfaceTextureId = await camera.createCamera( testCameraDescription, resolutionPreset, enableAudio: enableAudio, ); + await camera.initializeCamera(flutterSurfaceTextureId); - late final CameraSize? expectedBoundSize; + CameraSize? expectedPreferredResolution; final PigeonInstanceManager testInstanceManager = PigeonInstanceManager( onWeakReferenceRemoved: (_) {}, ); switch (resolutionPreset) { case ResolutionPreset.low: - expectedBoundSize = CameraSize.pigeon_detached( + expectedPreferredResolution = CameraSize.pigeon_detached( width: 320, height: 240, pigeon_instanceManager: testInstanceManager, ); case ResolutionPreset.medium: - expectedBoundSize = CameraSize.pigeon_detached( + expectedPreferredResolution = CameraSize.pigeon_detached( width: 720, height: 480, pigeon_instanceManager: testInstanceManager, ); case ResolutionPreset.high: - expectedBoundSize = CameraSize.pigeon_detached( + expectedPreferredResolution = CameraSize.pigeon_detached( width: 1280, height: 720, pigeon_instanceManager: testInstanceManager, ); case ResolutionPreset.veryHigh: - expectedBoundSize = CameraSize.pigeon_detached( + expectedPreferredResolution = CameraSize.pigeon_detached( width: 1920, height: 1080, pigeon_instanceManager: testInstanceManager, ); case ResolutionPreset.ultraHigh: - expectedBoundSize = CameraSize.pigeon_detached( + expectedPreferredResolution = CameraSize.pigeon_detached( width: 3840, height: 2160, pigeon_instanceManager: testInstanceManager, ); case ResolutionPreset.max: - continue; + expectedPreferredResolution = null; + } + + if (expectedPreferredResolution == null) { + expect(camera.preview!.resolutionSelector!.resolutionFilter, isNull); + expect( + camera.imageCapture!.resolutionSelector!.resolutionFilter, + isNull, + ); + expect( + camera.imageAnalysis!.resolutionSelector!.resolutionFilter, + isNull, + ); + continue; } - final CameraSize? previewSize = await camera - .preview! - .resolutionSelector! - .resolutionStrategy! - .getBoundSize(); - expect(previewSize?.width, equals(expectedBoundSize.width)); - expect(previewSize?.height, equals(expectedBoundSize.height)); expect( - await camera.preview!.resolutionSelector!.resolutionStrategy! - .getFallbackRule(), - ResolutionStrategyFallbackRule.closestLowerThenHigher, + lastSetPreferredSize?.width, + equals(expectedPreferredResolution.width), + ); + expect( + lastSetPreferredSize?.height, + equals(expectedPreferredResolution.height), ); final CameraSize? imageCaptureSize = await camera @@ -1499,12 +1381,13 @@ void main() { .resolutionSelector! .resolutionStrategy! .getBoundSize(); - expect(imageCaptureSize?.width, equals(expectedBoundSize.width)); - expect(imageCaptureSize?.height, equals(expectedBoundSize.height)); expect( - await camera.imageCapture!.resolutionSelector!.resolutionStrategy! - .getFallbackRule(), - ResolutionStrategyFallbackRule.closestLowerThenHigher, + imageCaptureSize?.width, + equals(expectedPreferredResolution.width), + ); + expect( + imageCaptureSize?.height, + equals(expectedPreferredResolution.height), ); final CameraSize? imageAnalysisSize = await camera @@ -1512,37 +1395,23 @@ void main() { .resolutionSelector! .resolutionStrategy! .getBoundSize(); - expect(imageAnalysisSize?.width, equals(expectedBoundSize.width)); - expect(imageAnalysisSize?.height, equals(expectedBoundSize.height)); expect( - await camera.imageAnalysis!.resolutionSelector!.resolutionStrategy! - .getFallbackRule(), - ResolutionStrategyFallbackRule.closestLowerThenHigher, + imageAnalysisSize?.width, + equals(expectedPreferredResolution.width), + ); + expect( + imageAnalysisSize?.height, + equals(expectedPreferredResolution.height), ); } - // Test max case. - await camera.createCamera( + // Test null case. + final int flutterSurfaceTextureId = await camera.createCamera( testCameraDescription, - ResolutionPreset.max, - enableAudio: true, - ); - - expect( - camera.preview!.resolutionSelector!.resolutionStrategy, - equals(camera.proxy.highestAvailableStrategyResolutionStrategy()), - ); - expect( - camera.imageCapture!.resolutionSelector!.resolutionStrategy, - equals(camera.proxy.highestAvailableStrategyResolutionStrategy()), - ); - expect( - camera.imageAnalysis!.resolutionSelector!.resolutionStrategy, - equals(camera.proxy.highestAvailableStrategyResolutionStrategy()), + null, ); + await camera.initializeCamera(flutterSurfaceTextureId); - // Test null case. - await camera.createCamera(testCameraDescription, null); expect(camera.preview!.resolutionSelector, isNull); expect(camera.imageCapture!.resolutionSelector, isNull); expect(camera.imageAnalysis!.resolutionSelector, isNull); @@ -1550,17 +1419,18 @@ void main() { ); test( - 'createCamera properly sets filter for resolution preset for non-video capture use cases', + 'createCamera and initializeCamera properly set aspect ratio based on preset resolution for non-video capture use cases', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - const CameraLensDirection testLensDirection = CameraLensDirection.front; - const int testSensorOrientation = 180; + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; const CameraDescription testCameraDescription = CameraDescription( name: 'cameraName', lensDirection: testLensDirection, sensorOrientation: testSensorOrientation, ); const bool enableAudio = true; + const int testCameraId = 12; final MockCamera mockCamera = MockCamera(); // Mock/Detached objects for (typically attached) objects created by @@ -1571,22 +1441,9 @@ void main() { // Tell plugin to create mock/detached objects for testing createCamera // as needed. - CameraSize? lastSetPreferredSize; - camera.proxy = getProxyForTestingResolutionPreset( + camera.proxy = getProxyForTestingUseCaseConfiguration( mockProcessCameraProvider, - createWithOnePreferredSizeResolutionFilter: - ({ - required CameraSize preferredSize, - // ignore: non_constant_identifier_names - BinaryMessenger? pigeon_binaryMessenger, - // ignore: non_constant_identifier_names - PigeonInstanceManager? pigeon_instanceManager, - }) { - lastSetPreferredSize = preferredSize; - return MockResolutionFilter(); - }, ); - when( mockProcessCameraProvider.bindToLifecycle(any, any), ).thenAnswer((_) async => mockCamera); @@ -1598,104 +1455,100 @@ void main() { // Test non-null resolution presets. for (final ResolutionPreset resolutionPreset in ResolutionPreset.values) { - await camera.createCamera( + final int flutterSurfaceTextureId = await camera.createCamera( testCameraDescription, resolutionPreset, enableAudio: enableAudio, ); + await camera.initializeCamera(flutterSurfaceTextureId); - CameraSize? expectedPreferredResolution; - final PigeonInstanceManager testInstanceManager = PigeonInstanceManager( - onWeakReferenceRemoved: (_) {}, - ); + AspectRatio? expectedAspectRatio; + AspectRatioStrategyFallbackRule? expectedFallbackRule; switch (resolutionPreset) { case ResolutionPreset.low: - expectedPreferredResolution = CameraSize.pigeon_detached( - width: 320, - height: 240, - pigeon_instanceManager: testInstanceManager, - ); - case ResolutionPreset.medium: - expectedPreferredResolution = CameraSize.pigeon_detached( - width: 720, - height: 480, - pigeon_instanceManager: testInstanceManager, - ); + expectedAspectRatio = AspectRatio.ratio4To3; + expectedFallbackRule = AspectRatioStrategyFallbackRule.auto; case ResolutionPreset.high: - expectedPreferredResolution = CameraSize.pigeon_detached( - width: 1280, - height: 720, - pigeon_instanceManager: testInstanceManager, - ); case ResolutionPreset.veryHigh: - expectedPreferredResolution = CameraSize.pigeon_detached( - width: 1920, - height: 1080, - pigeon_instanceManager: testInstanceManager, - ); case ResolutionPreset.ultraHigh: - expectedPreferredResolution = CameraSize.pigeon_detached( - width: 3840, - height: 2160, - pigeon_instanceManager: testInstanceManager, - ); + expectedAspectRatio = AspectRatio.ratio16To9; + expectedFallbackRule = AspectRatioStrategyFallbackRule.auto; + case ResolutionPreset.medium: + // Medium resolution preset uses aspect ratio 3:2 which is unsupported + // by CameraX. case ResolutionPreset.max: - expectedPreferredResolution = null; } - if (expectedPreferredResolution == null) { - expect(camera.preview!.resolutionSelector!.resolutionFilter, isNull); + if (expectedAspectRatio == null) { expect( - camera.imageCapture!.resolutionSelector!.resolutionFilter, - isNull, + await camera.preview!.resolutionSelector!.getAspectRatioStrategy(), + equals( + camera.proxy.ratio_4_3FallbackAutoStrategyAspectRatioStrategy(), + ), ); expect( - camera.imageAnalysis!.resolutionSelector!.resolutionFilter, - isNull, + await camera.imageCapture!.resolutionSelector! + .getAspectRatioStrategy(), + equals( + camera.proxy.ratio_4_3FallbackAutoStrategyAspectRatioStrategy(), + ), + ); + expect( + await camera.imageAnalysis!.resolutionSelector! + .getAspectRatioStrategy(), + equals( + camera.proxy.ratio_4_3FallbackAutoStrategyAspectRatioStrategy(), + ), ); continue; } + final AspectRatioStrategy previewStrategy = await camera + .preview! + .resolutionSelector! + .getAspectRatioStrategy(); + final AspectRatioStrategy imageCaptureStrategy = await camera + .imageCapture! + .resolutionSelector! + .getAspectRatioStrategy(); + final AspectRatioStrategy imageAnalysisStrategy = await camera + .imageCapture! + .resolutionSelector! + .getAspectRatioStrategy(); + + // Check aspect ratio. expect( - lastSetPreferredSize?.width, - equals(expectedPreferredResolution.width), + await previewStrategy.getPreferredAspectRatio(), + equals(expectedAspectRatio), ); expect( - lastSetPreferredSize?.height, - equals(expectedPreferredResolution.height), + await imageCaptureStrategy.getPreferredAspectRatio(), + equals(expectedAspectRatio), ); - - final CameraSize? imageCaptureSize = await camera - .imageCapture! - .resolutionSelector! - .resolutionStrategy! - .getBoundSize(); expect( - imageCaptureSize?.width, - equals(expectedPreferredResolution.width), + await imageAnalysisStrategy.getPreferredAspectRatio(), + equals(expectedAspectRatio), ); + + // Check fallback rule. expect( - imageCaptureSize?.height, - equals(expectedPreferredResolution.height), + await previewStrategy.getFallbackRule(), + equals(expectedFallbackRule), ); - - final CameraSize? imageAnalysisSize = await camera - .imageAnalysis! - .resolutionSelector! - .resolutionStrategy! - .getBoundSize(); expect( - imageAnalysisSize?.width, - equals(expectedPreferredResolution.width), + await imageCaptureStrategy.getFallbackRule(), + equals(expectedFallbackRule), ); expect( - imageAnalysisSize?.height, - equals(expectedPreferredResolution.height), + await imageAnalysisStrategy.getFallbackRule(), + equals(expectedFallbackRule), ); } // Test null case. await camera.createCamera(testCameraDescription, null); + await camera.initializeCamera(testCameraId); + expect(camera.preview!.resolutionSelector, isNull); expect(camera.imageCapture!.resolutionSelector, isNull); expect(camera.imageAnalysis!.resolutionSelector, isNull); @@ -1703,7 +1556,7 @@ void main() { ); test( - 'createCamera properly sets aspect ratio based on preset resolution for non-video capture use cases', + 'createCamera and initializeCamera binds Preview, ImageCapture, and ImageAnalysis use cases to ProcessCameraProvider instance', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); const CameraLensDirection testLensDirection = CameraLensDirection.back; @@ -1713,125 +1566,302 @@ void main() { lensDirection: testLensDirection, sensorOrientation: testSensorOrientation, ); - const bool enableAudio = true; - final MockCamera mockCamera = MockCamera(); - - // Mock/Detached objects for (typically attached) objects created by - // createCamera. - final MockProcessCameraProvider mockProcessCameraProvider = - MockProcessCameraProvider(); - final MockCameraInfo mockCameraInfo = MockCameraInfo(); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + + // Mock/Detached objects for (typically attached) objects created by + // createCamera. + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockPreview mockPreview = MockPreview(); + final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); + final MockImageCapture mockImageCapture = MockImageCapture(); + final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); + final MockRecorder mockRecorder = MockRecorder(); + final MockVideoCapture mockVideoCapture = MockVideoCapture(); + final MockCamera mockCamera = MockCamera(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraInfo mockCamera2CameraInfo = + MockCamera2CameraInfo(); + final MockCameraCharacteristicsKey mockCameraCharacteristicsKey = + MockCameraCharacteristicsKey(); + + // Tell plugin to create mock/detached objects and stub method calls for the + // testing of createCamera. + camera.proxy = CameraXProxy( + getInstanceProcessCameraProvider: + ({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) async { + return mockProcessCameraProvider; + }, + newCameraSelector: + ({ + LensFacing? requireLensFacing, + CameraInfo? cameraInfoForFilter, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + switch (requireLensFacing) { + case LensFacing.front: + return MockCameraSelector(); + case LensFacing.back: + case LensFacing.external: + case LensFacing.unknown: + case null: + } + + return mockBackCameraSelector; + }, + newPreview: + ({ + int? targetRotation, + ResolutionSelector? resolutionSelector, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final ResolutionInfo testResolutionInfo = + ResolutionInfo.pigeon_detached(resolution: MockCameraSize()); + when( + mockPreview.getResolutionInfo(), + ).thenAnswer((_) async => testResolutionInfo); + return mockPreview; + }, + newImageCapture: + ({ + int? targetRotation, + CameraXFlashMode? flashMode, + ResolutionSelector? resolutionSelector, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return mockImageCapture; + }, + newRecorder: + ({ + int? aspectRatio, + int? targetVideoEncodingBitRate, + QualitySelector? qualitySelector, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return mockRecorder; + }, + withOutputVideoCapture: + ({ + required VideoOutput videoOutput, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return mockVideoCapture; + }, + newImageAnalysis: + ({ + int? targetRotation, + int? outputImageFormat, + ResolutionSelector? resolutionSelector, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return mockImageAnalysis; + }, + newResolutionStrategy: + ({ + required CameraSize boundSize, + required ResolutionStrategyFallbackRule fallbackRule, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockResolutionStrategy(); + }, + newResolutionSelector: + ({ + AspectRatioStrategy? aspectRatioStrategy, + ResolutionStrategy? resolutionStrategy, + ResolutionFilter? resolutionFilter, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockResolutionSelector(); + }, + fromQualitySelector: + ({ + required VideoQuality quality, + FallbackStrategy? fallbackStrategy, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockQualitySelector(); + }, + newObserver: + ({ + required void Function(Observer, T) onChanged, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return Observer.detached( + onChanged: onChanged, + pigeon_instanceManager: PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ), + ); + }, + newSystemServicesManager: + ({ + required void Function(SystemServicesManager, String) + onCameraError, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockSystemServicesManager(); + }, + newDeviceOrientationManager: + ({ + required void Function(DeviceOrientationManager, String) + onDeviceOrientationChanged, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockDeviceOrientationManager manager = + MockDeviceOrientationManager(); + when(manager.getUiOrientation()).thenAnswer((_) async { + return 'PORTRAIT_UP'; + }); + return manager; + }, + newAspectRatioStrategy: + ({ + required AspectRatio preferredAspectRatio, + required AspectRatioStrategyFallbackRule fallbackRule, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockAspectRatioStrategy(); + }, + createWithOnePreferredSizeResolutionFilter: + ({ + required CameraSize preferredSize, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockResolutionFilter(); + }, + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + when( + mockCamera2CameraInfo.getCameraCharacteristic( + mockCameraCharacteristicsKey, + ), + ).thenAnswer((_) async => testSensorOrientation); + return mockCamera2CameraInfo; + }, + newCameraSize: + ({ + required int width, + required int height, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockCameraSize(); + }, + sensorOrientationCameraCharacteristics: () { + return mockCameraCharacteristicsKey; + }, + lowerQualityOrHigherThanFallbackStrategy: + ({ + required VideoQuality quality, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + return MockFallbackStrategy(); + }, + ); - // Tell plugin to create mock/detached objects for testing createCamera - // as needed. - camera.proxy = getProxyForTestingResolutionPreset( - mockProcessCameraProvider, - ); when( - mockProcessCameraProvider.bindToLifecycle(any, any), + mockProcessCameraProvider.bindToLifecycle( + mockBackCameraSelector, + [mockPreview, mockImageCapture, mockImageAnalysis], + ), ).thenAnswer((_) async => mockCamera); when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); when( mockCameraInfo.getCameraState(), ).thenAnswer((_) async => MockLiveCameraState()); + when(mockCamera.cameraControl).thenAnswer((_) => mockCameraControl); + camera.processCameraProvider = mockProcessCameraProvider; - // Test non-null resolution presets. - for (final ResolutionPreset resolutionPreset in ResolutionPreset.values) { - await camera.createCamera( - testCameraDescription, - resolutionPreset, + final int flutterSurfaceTextureId = await camera.createCameraWithSettings( + testCameraDescription, + const MediaSettings( + resolutionPreset: testResolutionPreset, + fps: 15, + videoBitrate: 2000000, + audioBitrate: 64000, enableAudio: enableAudio, - ); - - AspectRatio? expectedAspectRatio; - AspectRatioStrategyFallbackRule? expectedFallbackRule; - switch (resolutionPreset) { - case ResolutionPreset.low: - expectedAspectRatio = AspectRatio.ratio4To3; - expectedFallbackRule = AspectRatioStrategyFallbackRule.auto; - case ResolutionPreset.high: - case ResolutionPreset.veryHigh: - case ResolutionPreset.ultraHigh: - expectedAspectRatio = AspectRatio.ratio16To9; - expectedFallbackRule = AspectRatioStrategyFallbackRule.auto; - case ResolutionPreset.medium: - // Medium resolution preset uses aspect ratio 3:2 which is unsupported - // by CameraX. - case ResolutionPreset.max: - } - - if (expectedAspectRatio == null) { - expect( - await camera.preview!.resolutionSelector!.getAspectRatioStrategy(), - equals( - camera.proxy.ratio_4_3FallbackAutoStrategyAspectRatioStrategy(), - ), - ); - expect( - await camera.imageCapture!.resolutionSelector! - .getAspectRatioStrategy(), - equals( - camera.proxy.ratio_4_3FallbackAutoStrategyAspectRatioStrategy(), - ), - ); - expect( - await camera.imageAnalysis!.resolutionSelector! - .getAspectRatioStrategy(), - equals( - camera.proxy.ratio_4_3FallbackAutoStrategyAspectRatioStrategy(), - ), - ); - continue; - } + ), + ); + await camera.initializeCamera(flutterSurfaceTextureId); - final AspectRatioStrategy previewStrategy = await camera - .preview! - .resolutionSelector! - .getAspectRatioStrategy(); - final AspectRatioStrategy imageCaptureStrategy = await camera - .imageCapture! - .resolutionSelector! - .getAspectRatioStrategy(); - final AspectRatioStrategy imageAnalysisStrategy = await camera - .imageCapture! - .resolutionSelector! - .getAspectRatioStrategy(); + // Verify expected UseCases were bound. + verify( + camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, + [mockPreview, mockImageCapture, mockImageAnalysis], + ), + ); - // Check aspect ratio. - expect( - await previewStrategy.getPreferredAspectRatio(), - equals(expectedAspectRatio), - ); - expect( - await imageCaptureStrategy.getPreferredAspectRatio(), - equals(expectedAspectRatio), - ); - expect( - await imageAnalysisStrategy.getPreferredAspectRatio(), - equals(expectedAspectRatio), - ); + // Verify the camera's CameraInfo instance got updated. + expect(camera.cameraInfo, equals(mockCameraInfo)); - // Check fallback rule. - expect( - await previewStrategy.getFallbackRule(), - equals(expectedFallbackRule), - ); - expect( - await imageCaptureStrategy.getFallbackRule(), - equals(expectedFallbackRule), - ); - expect( - await imageAnalysisStrategy.getFallbackRule(), - equals(expectedFallbackRule), - ); - } + // Verify camera's CameraControl instance got updated. + expect(camera.cameraControl, equals(mockCameraControl)); - // Test null case. - await camera.createCamera(testCameraDescription, null); - expect(camera.preview!.resolutionSelector, isNull); - expect(camera.imageCapture!.resolutionSelector, isNull); - expect(camera.imageAnalysis!.resolutionSelector, isNull); + // Verify preview has been marked as bound to the camera lifecycle by + // createCamera. + expect(camera.previewInitiallyBound, isTrue); }, ); @@ -1862,7 +1892,7 @@ void main() { FallbackStrategy? setFallbackStrategy; final MockFallbackStrategy mockFallbackStrategy = MockFallbackStrategy(); final MockQualitySelector mockQualitySelector = MockQualitySelector(); - camera.proxy = getProxyForTestingResolutionPreset( + camera.proxy = getProxyForTestingUseCaseConfiguration( mockProcessCameraProvider, lowerQualityOrHigherThanFallbackStrategy: ({ @@ -1968,7 +1998,7 @@ void main() { // The proxy needed for this test is the same as testing resolution // presets except for mocking the retrieval of the sensor and current // UI orientation. - camera.proxy = getProxyForTestingResolutionPreset( + camera.proxy = getProxyForTestingUseCaseConfiguration( mockProcessCameraProvider, newPreview: ({ @@ -2166,6 +2196,7 @@ void main() { newImageAnalysis: ({ int? targetRotation, + int? outputImageFormat, ResolutionSelector? resolutionSelector, // ignore: non_constant_identifier_names BinaryMessenger? pigeon_binaryMessenger, @@ -2404,6 +2435,176 @@ void main() { }, ); + test('initializeCamera sets camera state observer as expected', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation, + ); + const bool enableAudio = true; + final MockCamera mockCamera = MockCamera(); + const int testSurfaceTextureId = 244; + + // Mock/Detached objects for (typically attached) objects created by + // createCamera. + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); + final MockPreview mockPreview = MockPreview(); + final ResolutionInfo testResolutionInfo = ResolutionInfo.pigeon_detached( + resolution: MockCameraSize(), + ); + + when( + mockProcessCameraProvider.bindToLifecycle(any, any), + ).thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when( + mockCameraInfo.getCameraState(), + ).thenAnswer((_) async => mockLiveCameraState); + when( + mockPreview.getResolutionInfo(), + ).thenAnswer((_) async => testResolutionInfo); + when( + mockPreview.setSurfaceProvider(any), + ).thenAnswer((_) async => testSurfaceTextureId); + camera.processCameraProvider = mockProcessCameraProvider; + + // Tell plugin to create mock/detached objects for testing createCamera + // as needed. + camera.proxy = getProxyForTestingUseCaseConfiguration( + mockProcessCameraProvider, + newPreview: + ({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? targetRotation, + }) => mockPreview, + ); + + // Create and initialize camera. + await camera.createCameraWithSettings( + testCameraDescription, + const MediaSettings(enableAudio: enableAudio), + ); + await camera.initializeCamera(testSurfaceTextureId); + + // Verify the camera state observer is updated. + expect( + await testCameraClosingObserver( + camera, + testSurfaceTextureId, + verify(mockLiveCameraState.observe(captureAny)).captured.single + as Observer, + ), + isTrue, + ); + }); + + test( + 'initializeCamera sets image format of ImageAnalysis use case as expected', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation, + ); + const bool enableAudio = true; + final MockCamera mockCamera = MockCamera(); + const int testSurfaceTextureId = 244; + + // Mock/Detached objects for (typically attached) objects created by + // createCamera. + final MockProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockLiveCameraState mockLiveCameraState = MockLiveCameraState(); + final MockPreview mockPreview = MockPreview(); + final ResolutionInfo testResolutionInfo = ResolutionInfo.pigeon_detached( + resolution: MockCameraSize(), + ); + final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); + + // Configure mocks for camera initialization. + when( + mockProcessCameraProvider.bindToLifecycle(any, any), + ).thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when( + mockCameraInfo.getCameraState(), + ).thenAnswer((_) async => mockLiveCameraState); + when( + mockPreview.getResolutionInfo(), + ).thenAnswer((_) async => testResolutionInfo); + when( + mockPreview.setSurfaceProvider(any), + ).thenAnswer((_) async => testSurfaceTextureId); + camera.processCameraProvider = mockProcessCameraProvider; + + for (final ImageFormatGroup imageFormatGroup in ImageFormatGroup.values) { + // Get CameraX image format constant for imageFormatGroup. + final int? cameraXImageFormat = switch (imageFormatGroup) { + ImageFormatGroup.yuv420 => + AndroidCameraCameraX.imageAnalysisOutputImageFormatYuv420_888, + ImageFormatGroup.nv21 => + AndroidCameraCameraX.imageAnalysisOutputImageFormatNv21, + _ => null, + }; + // Tell plugin to create mock/detached objects for testing createCamera + // as needed. + int? imageAnalysisOutputImageFormat; + camera.proxy = getProxyForTestingUseCaseConfiguration( + mockProcessCameraProvider, + newImageAnalysis: + ({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? targetRotation, + int? outputImageFormat, + }) { + imageAnalysisOutputImageFormat = outputImageFormat; + return mockImageAnalysis; + }, + newPreview: + ({ + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + ResolutionSelector? resolutionSelector, + int? targetRotation, + }) => mockPreview, + ); + + // Create and initialize camera. + await camera.createCameraWithSettings( + testCameraDescription, + const MediaSettings(enableAudio: enableAudio), + ); + await camera.initializeCamera( + testSurfaceTextureId, + imageFormatGroup: imageFormatGroup, + ); + + // Test image format group is set as expected. + expect(imageAnalysisOutputImageFormat, cameraXImageFormat); + } + }, + ); + test('initializeCamera sends expected CameraInitializedEvent', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); @@ -2506,6 +2707,7 @@ void main() { newImageAnalysis: ({ int? targetRotation, + int? outputImageFormat, ResolutionSelector? resolutionSelector, // ignore: non_constant_identifier_names BinaryMessenger? pigeon_binaryMessenger, 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 4a2df856f15..de4575d8041 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 @@ -1474,6 +1474,7 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { @override _i2.ImageAnalysis Function({ + int? outputImageFormat, _i8.BinaryMessenger? pigeon_binaryMessenger, _i2.PigeonInstanceManager? pigeon_instanceManager, _i2.ResolutionSelector? resolutionSelector, @@ -1484,6 +1485,7 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { Invocation.getter(#newImageAnalysis), returnValue: ({ + int? outputImageFormat, _i8.BinaryMessenger? pigeon_binaryMessenger, _i2.PigeonInstanceManager? pigeon_instanceManager, _i2.ResolutionSelector? resolutionSelector, @@ -1494,6 +1496,7 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { ), returnValueForMissingStub: ({ + int? outputImageFormat, _i8.BinaryMessenger? pigeon_binaryMessenger, _i2.PigeonInstanceManager? pigeon_instanceManager, _i2.ResolutionSelector? resolutionSelector, @@ -1504,6 +1507,7 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { ), ) as _i2.ImageAnalysis Function({ + int? outputImageFormat, _i8.BinaryMessenger? pigeon_binaryMessenger, _i2.PigeonInstanceManager? pigeon_instanceManager, _i2.ResolutionSelector? resolutionSelector, diff --git a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart index 6a9eff3d11d..ff3bda223fb 100644 --- a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart @@ -120,6 +120,10 @@ void main() { when( preview.surfaceProducerHandlesCropAndRotation(), ).thenAnswer((_) async => handlesCropAndRotation); + when(preview.getResolutionInfo()).thenAnswer( + (_) async => + ResolutionInfo.pigeon_detached(resolution: MockCameraSize()), + ); return preview; }, newImageCapture: @@ -156,6 +160,7 @@ void main() { ({ int? targetRotation, ResolutionSelector? resolutionSelector, + int? outputImageFormat, // ignore: non_constant_identifier_names BinaryMessenger? pigeon_binaryMessenger, // ignore: non_constant_identifier_names @@ -524,10 +529,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -577,10 +584,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -628,10 +637,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -679,10 +690,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -771,10 +784,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -822,10 +837,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -873,10 +890,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -924,10 +943,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1013,10 +1034,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Calculated according to: counterClockwiseCurrentDefaultDisplayRotation - cameraPreviewPreAppliedRotation, // where the cameraPreviewPreAppliedRotation is the clockwise rotation applied by the CameraPreview widget @@ -1114,10 +1137,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Calculated according to: counterClockwiseCurrentDefaultDisplayRotation - cameraPreviewPreAppliedRotation, // where the cameraPreviewPreAppliedRotation is the clockwise rotation applied by the CameraPreview widget @@ -1228,10 +1253,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1287,10 +1314,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1346,10 +1375,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1405,10 +1436,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1508,10 +1541,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1567,10 +1602,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1630,10 +1667,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1691,10 +1730,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -1791,10 +1832,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Calculated according to: ((270 - counterClockwiseDefaultDisplayRotation * 1 + 360) % 360) - 90. // 90 is used in this calculation for the CameraPreview pre-applied rotation because it is the @@ -1906,10 +1949,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Calculated according to: ((90 - 270 * 1 + 360) % 360) - cameraPreviewPreAppliedRotation. // 270 is used in this calculation for the device orientation because it is the @@ -2040,10 +2085,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -2098,10 +2145,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -2193,10 +2242,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes. @@ -2271,10 +2322,12 @@ void main() { final List availableCameras = await camera .availableCameras(); expect(availableCameras.length, 1); - await camera.createCameraWithSettings( - availableCameras.first, - testMediaSettings, - ); + final int flutterSurfaceTextureId = await camera + .createCameraWithSettings( + availableCameras.first, + testMediaSettings, + ); + await camera.initializeCamera(flutterSurfaceTextureId); // Put camera preview in widget tree and pump one frame so that Future to retrieve // the initial default display rotation completes.