diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 81e8fd45b17d..77e675566d9f 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.6 + +* Adds `CaptureMode` as a parameter to allow configuring the camera for taking photos or videos. `CaptureMode.photo` selects an appropriate 4:3 aspect ratio depending on the `ResolutionPreset`. + ## 0.10.5+5 * Fixes bug where old camera resources were not disposed when switching between camera descriptions. diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 3b23ee596ded..ddeaebf989d9 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -30,3 +30,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera: {path: ../../../camera/camera}, camera_android: {path: ../../../camera/camera_android}, camera_avfoundation: {path: ../../../camera/camera_avfoundation}, camera_platform_interface: {path: ../../../camera/camera_platform_interface}, camera_web: {path: ../../../camera/camera_web}} diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 900c2633a5d7..6189aeb55253 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -7,6 +7,7 @@ export 'package:camera_platform_interface/camera_platform_interface.dart' CameraDescription, CameraException, CameraLensDirection, + CaptureMode, FlashMode, ExposureMode, FocusMode, diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 2e8d0df8d748..7e017132d6c9 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -50,6 +50,7 @@ class CameraValue { required this.focusPointSupported, required this.deviceOrientation, required this.description, + required this.captureMode, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -72,6 +73,7 @@ class CameraValue { deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, description: description, + captureMode: CaptureMode.video, ); /// True after [CameraController.initialize] has completed successfully. @@ -148,6 +150,9 @@ class CameraValue { /// The properties of the camera device controlled by this controller. final CameraDescription description; + /// The current capture mode. + final CaptureMode captureMode; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -171,6 +176,7 @@ class CameraValue { bool? isPreviewPaused, CameraDescription? description, Optional? previewPauseOrientation, + CaptureMode? captureMode, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -198,6 +204,7 @@ class CameraValue { previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, + captureMode: captureMode ?? this.captureMode, ); } @@ -219,7 +226,8 @@ class CameraValue { 'recordingOrientation: $recordingOrientation, ' 'isPreviewPaused: $isPreviewPaused, ' 'previewPausedOrientation: $previewPauseOrientation, ' - 'description: $description)'; + 'description: $description, ' + 'captureMode: $captureMode)'; } } @@ -343,6 +351,8 @@ class CameraController extends ValueNotifier { .then((CameraInitializedEvent event) => event.exposureMode), focusMode: await initializeCompleter.future .then((CameraInitializedEvent event) => event.focusMode), + captureMode: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.captureMode), exposurePointSupported: await initializeCompleter.future.then( (CameraInitializedEvent event) => event.exposurePointSupported), focusPointSupported: await initializeCompleter.future @@ -529,6 +539,12 @@ class CameraController extends ValueNotifier { 'startVideoRecording was called when a recording is already started.', ); } + if (value.captureMode == CaptureMode.photo) { + throw CameraException( + 'The camera is configured for still images.', + 'startVideoRecording was called when the camera was configured for still images.', + ); + } Function(CameraImageData image)? streamCallback; if (onAvailable != null) { @@ -610,6 +626,13 @@ class CameraController extends ValueNotifier { 'resumeVideoRecording was called when no video is recording.', ); } + if (value.captureMode == CaptureMode.photo) { + throw CameraException( + 'The camera is configured for still images.', + 'resumeVideoRecording was called when the camera was configured for still images.', + ); + } + try { await CameraPlatform.instance.resumeVideoRecording(_cameraId); value = value.copyWith(isRecordingPaused: false); @@ -682,6 +705,18 @@ class CameraController extends ValueNotifier { } } + /// Sets the capture mode for the camera. + Future setCaptureMode(CaptureMode mode) async { + _throwIfNotInitialized('setCaptureMode'); + try { + final Size? previewSize = + await CameraPlatform.instance.setCaptureMode(_cameraId, mode); + value = value.copyWith(captureMode: mode, previewSize: previewSize); + } 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 diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index c246e4c232ab..31fef79756b2 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.5+5 +version: 0.11.0 environment: sdk: ">=2.19.0 <4.0.0" @@ -21,10 +21,10 @@ flutter: default_package: camera_web dependencies: - camera_android: ^0.10.7 - camera_avfoundation: ^0.9.13 - camera_platform_interface: ^2.5.0 - camera_web: ^0.3.1 + camera_android: ^0.10.9 + camera_avfoundation: ^0.9.14 + camera_platform_interface: ^2.5.3 + camera_web: ^0.3.2 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 @@ -39,3 +39,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_android: {path: ../../camera/camera_android}, camera_avfoundation: {path: ../../camera/camera_avfoundation}, camera_platform_interface: {path: ../../camera/camera_platform_interface}} diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index c73e1816445c..edd30aebc69c 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -120,6 +120,9 @@ class FakeController extends ValueNotifier @override Future setDescription(CameraDescription description) async {} + @override + Future setCaptureMode(CaptureMode captureMode) async {} + @override CameraDescription get description => value.description; } diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index ec111ed85949..80c6b088ee5c 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -36,6 +36,7 @@ CameraInitializedEvent get mockOnCameraInitializedEvent => true, FocusMode.auto, true, + CaptureMode.video, ); DeviceOrientationChangedEvent get mockOnDeviceOrientationChangedEvent => @@ -1401,6 +1402,52 @@ void main() { 'This is a test error message', ))); }); + + test('setCaptureMode() calls $CameraPlatform', () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .setCaptureMode(mockInitializeCamera, CaptureMode.photo)) + .thenAnswer((_) => Future.value(const Size(640, 480))); + await cameraController.setCaptureMode(CaptureMode.photo); + + verify(CameraPlatform.instance + .setCaptureMode(cameraController.cameraId, CaptureMode.photo)) + .called(1); + }); + + test('setCaptureMode throws $CameraException on $PlatformException', + () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setCaptureMode(cameraController.cameraId, CaptureMode.photo)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + ), + ); + + expect( + cameraController.setCaptureMode(CaptureMode.photo), + throwsA(isA().having( + (CameraException error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); }); } @@ -1554,6 +1601,13 @@ class MockCameraPlatform extends Mock Invocation.method(#setExposureOffset, [cameraId, offset]), returnValue: Future.value(1.0), ) as Future; + + @override + Future setCaptureMode(int? cameraId, CaptureMode mode) async => + super.noSuchMethod( + Invocation.method(#setCaptureMode, [cameraId, mode]), + returnValue: Future.value(const Size(680, 480)), + ) as Future; } class MockCameraDescription extends CameraDescription { diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index dbb1ddcbf789..167390ad2be1 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -28,6 +28,7 @@ void main() { focusPointSupported: true, previewPauseOrientation: DeviceOrientation.portraitUp, description: FakeController.fakeDescription, + captureMode: CaptureMode.video, ); expect(cameraValue, isA()); @@ -144,10 +145,11 @@ void main() { isPreviewPaused: true, previewPauseOrientation: DeviceOrientation.portraitUp, description: FakeController.fakeDescription, + captureMode: CaptureMode.video, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp, description: CameraDescription(, CameraLensDirection.back, 0))'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp, description: CameraDescription(, CameraLensDirection.back, 0), captureMode: CaptureMode.video)'); }); }); } diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index acaa35064435..9d8740bcd8b7 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.9 + +* Adds `CaptureMode` as a parameter to allow configuring the camera for taking photos or videos. `CaptureMode.photo` selects an appropriate 4:3 aspect ratio depending on the `ResolutionPreset`. + ## 0.10.8+13 * Updates annotations lib to 1.7.0. diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 5e526975172e..418fc2c6dc4f 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -59,6 +59,7 @@ import io.flutter.plugins.camera.media.ImageStreamReader; import io.flutter.plugins.camera.media.MediaRecorderBuilder; import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureMode; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; @@ -68,6 +69,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.concurrent.Executors; @FunctionalInterface @@ -207,7 +209,12 @@ public Camera( this.resolutionPreset = resolutionPreset; this.cameraFeatures = CameraFeatures.init( - cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + cameraFeatureFactory, + cameraProperties, + activity, + dartMessenger, + resolutionPreset, + CaptureMode.video); // Create capture callback. captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); @@ -326,7 +333,8 @@ public void onOpened(@NonNull CameraDevice device) { cameraFeatures.getExposureLock().getValue(), cameraFeatures.getAutoFocus().getValue(), cameraFeatures.getExposurePoint().checkIsSupported(), - cameraFeatures.getFocusPoint().checkIsSupported()); + cameraFeatures.getFocusPoint().checkIsSupported(), + resolutionFeature.getCaptureMode()); } } catch (Exception e) { if (BuildConfig.DEBUG) { @@ -409,7 +417,6 @@ private void createCaptureSession( resolutionFeature.getPreviewSize().getHeight()); Surface flutterSurface = new Surface(surfaceTexture); previewRequestBuilder.addTarget(flutterSurface); - List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets @@ -502,7 +509,7 @@ private void createCaptureSession( cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); } - // Send a repeating request to refresh capture session. + // Send a repeating request to refresh capture session. void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { Log.i(TAG, "refreshPreviewCaptureSession"); @@ -972,6 +979,36 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) { } } + /** + * Sets new capture mode from dart + * + * @param result Flutter result. + * @param newMode New mode. + */ + public void setCaptureMode(@NonNull final Result result, @NonNull CaptureMode newMode) { + // Re-initalises the camera with the appropriate capture mode. + stopAndReleaseCamera(); + cameraFeatures = + CameraFeatures.init( + cameraFeatureFactory, + cameraProperties, + activity, + dartMessenger, + resolutionPreset, + newMode); + try { + open(imageFormatGroup); + } catch (CameraAccessException e) { + result.error("setDescriptionWhileRecordingFailed", e.getMessage(), null); + } + + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + Map previewSize = new HashMap(); + previewSize.put("previewWidth", resolutionFeature.getPreviewSize().getWidth()); + previewSize.put("previewHeight", resolutionFeature.getPreviewSize().getHeight()); + result.success(previewSize); + } + /** * Sets new focus point from dart. * @@ -1309,7 +1346,12 @@ public void setDescriptionWhileRecording( cameraProperties = properties; cameraFeatures = CameraFeatures.init( - cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + cameraFeatureFactory, + cameraProperties, + activity, + dartMessenger, + resolutionPreset, + cameraFeatures.getResolution().getCaptureMode()); cameraFeatures.setAutoFocus( cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); try { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 2c3eda880deb..c64ade792f26 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -273,4 +273,16 @@ public interface CameraProperties { */ @NonNull int[] getAvailableNoiseReductionModes(); + + /** + * Returns a list of available output sizes for a particular format. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP key. + * + * @param format int The format for which available output sizes will be returned. + * @return Size[] List of available output sizes for a particular format. + */ + @NonNull + Size[] getAvailableOutputSizes(int format); } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java index dc8e9b736f2c..9fc1b790e275 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java @@ -164,4 +164,12 @@ public int[] getAvailableNoiseReductionModes() { return cameraCharacteristics.get( CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); } + + @NonNull + @Override + public Size[] getAvailableOutputSizes(int imageFormat) { + return cameraCharacteristics + .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + .getOutputSizes(imageFormat); + } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index e2b2ef80820d..7aa694c9c4f9 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -13,6 +13,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugins.camera.features.autofocus.FocusMode; import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.types.CaptureMode; import java.util.HashMap; import java.util.Map; @@ -95,6 +96,7 @@ public void sendDeviceOrientationChangeEvent( * @param focusMode describes the current focus mode that is set on the camera. * @param exposurePointSupported indicates if the camera supports setting an exposure point. * @param focusPointSupported indicates if the camera supports setting a focus point. + * @param captureMode describes the current capture mode that is set on the camera. */ void sendCameraInitializedEvent( Integer previewWidth, @@ -102,13 +104,15 @@ void sendCameraInitializedEvent( ExposureMode exposureMode, FocusMode focusMode, Boolean exposurePointSupported, - Boolean focusPointSupported) { + Boolean focusPointSupported, + CaptureMode captureMode) { assert (previewWidth != null); assert (previewHeight != null); assert (exposureMode != null); assert (focusMode != null); assert (exposurePointSupported != null); assert (focusPointSupported != null); + assert (captureMode != null); this.send( CameraEventType.INITIALIZED, new HashMap() { @@ -119,6 +123,7 @@ void sendCameraInitializedEvent( put("focusMode", focusMode.toString()); put("exposurePointSupported", exposurePointSupported); put("focusPointSupported", focusPointSupported); + put("captureMode", captureMode.toString()); } }); } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index aad62bbaba85..be3581d4185c 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -23,6 +23,7 @@ import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import io.flutter.plugins.camera.features.flash.FlashMode; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.types.CaptureMode; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; @@ -374,6 +375,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) result.success(null); break; } + case "setCaptureMode": + { + String modeStr = call.argument("mode"); + CaptureMode mode = CaptureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setCaptureModeFailed", "Unknown capture mode " + modeStr, null); + return; + } + try { + camera.setCaptureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } default: result.notImplemented(); break; @@ -387,6 +403,7 @@ void stopListening() { private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { String cameraName = call.argument("cameraName"); String preset = call.argument("resolutionPreset"); + String mode = call.argument("captureMode"); boolean enableAudio = call.argument("enableAudio"); TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = @@ -397,6 +414,7 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce CameraProperties cameraProperties = new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + CaptureMode captureMode = CaptureMode.valueOf(mode); camera = new Camera( diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java index 573bf7b5bf45..0daef2f04e1f 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -20,6 +20,7 @@ import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.types.CaptureMode; /** * Factory for creating the supported feature implementation controlling different aspects of the @@ -82,7 +83,8 @@ AutoFocusFeature createAutoFocusFeature( ResolutionFeature createResolutionFeature( @NonNull CameraProperties cameraProperties, @NonNull ResolutionPreset initialSetting, - @NonNull String cameraName); + @NonNull String cameraName, + @NonNull CaptureMode captureMode); /** * Creates a new instance of the focus point feature. @@ -106,7 +108,8 @@ FocusPointFeature createFocusPointFeature( * @return newly created instance of the FpsRangeFeature class. */ @NonNull - FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties); + FpsRangeFeature createFpsRangeFeature( + @NonNull CameraProperties cameraProperties, @NonNull CaptureMode captureMode); /** * Creates a new instance of the sensor orientation feature. diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java index a15a0dca4017..910370e13012 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -20,6 +20,7 @@ import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.types.CaptureMode; /** * Implementation of the {@link CameraFeatureFactory} interface creating the supported feature @@ -58,8 +59,9 @@ public FlashFeature createFlashFeature(@NonNull CameraProperties cameraPropertie public ResolutionFeature createResolutionFeature( @NonNull CameraProperties cameraProperties, @NonNull ResolutionPreset initialSetting, - @NonNull String cameraName) { - return new ResolutionFeature(cameraProperties, initialSetting, cameraName); + @NonNull String cameraName, + @NonNull CaptureMode captureMode) { + return new ResolutionFeature(cameraProperties, initialSetting, cameraName, captureMode); } @NonNull @@ -72,8 +74,9 @@ public FocusPointFeature createFocusPointFeature( @NonNull @Override - public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { - return new FpsRangeFeature(cameraProperties); + public FpsRangeFeature createFpsRangeFeature( + @NonNull CameraProperties cameraProperties, @NonNull CaptureMode captureMode) { + return new FpsRangeFeature(cameraProperties, captureMode); } @NonNull diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index 6c8c1f17a2f2..469eeafd16b0 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -20,6 +20,7 @@ import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.types.CaptureMode; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -49,7 +50,8 @@ public static CameraFeatures init( @NonNull CameraProperties cameraProperties, @NonNull Activity activity, @NonNull DartMessenger dartMessenger, - @NonNull ResolutionPreset resolutionPreset) { + @NonNull ResolutionPreset resolutionPreset, + @NonNull CaptureMode captureMode) { CameraFeatures cameraFeatures = new CameraFeatures(); cameraFeatures.setAutoFocus( cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); @@ -67,12 +69,13 @@ public static CameraFeatures init( cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); cameraFeatures.setFocusPoint( cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature)); - cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); + cameraFeatures.setFpsRange( + cameraFeatureFactory.createFpsRangeFeature(cameraProperties, captureMode)); cameraFeatures.setNoiseReduction( cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); cameraFeatures.setResolution( cameraFeatureFactory.createResolutionFeature( - cameraProperties, resolutionPreset, cameraProperties.getCameraName())); + cameraProperties, resolutionPreset, cameraProperties.getCameraName(), captureMode)); cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); return cameraFeatures; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java index 408e7a16b564..bd226f7b7c04 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -12,6 +12,7 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DeviceInfo; import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.types.CaptureMode; /** * Controls the frames per seconds (FPS) range configuration on the {@link android.hardware.camera2} @@ -20,14 +21,16 @@ public class FpsRangeFeature extends CameraFeature> { private static final Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); @Nullable private Range currentSetting; - + @NonNull private CaptureMode captureMode; /** * Creates a new instance of the {@link FpsRangeFeature}. * * @param cameraProperties Collection of characteristics for the current camera device. */ - public FpsRangeFeature(@NonNull CameraProperties cameraProperties) { + public FpsRangeFeature( + @NonNull CameraProperties cameraProperties, @NonNull CaptureMode captureMode) { super(cameraProperties); + this.captureMode = captureMode; if (isPixel4A()) { // HACK: There is a bug in the Pixel 4A where it cannot support 60fps modes @@ -43,8 +46,9 @@ public FpsRangeFeature(@NonNull CameraProperties cameraProperties) { if (ranges != null) { for (Range range : ranges) { int upper = range.getUpper(); - - if (upper >= 10) { + // When in photo mode, the upper bound is 30 fps or the aspect ratio/resolution will be + // changed by the camera session. + if (upper >= 10 && (upper < 60 || captureMode != CaptureMode.photo)) { if (currentSetting == null || upper > currentSetting.getUpper()) { currentSetting = range; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index 57ceddb39630..e152c4dc4d71 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -6,6 +6,7 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.graphics.ImageFormat; import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; import android.media.EncoderProfiles; @@ -17,6 +18,8 @@ import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.SdkCapabilityChecker; import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.types.CaptureMode; +import java.util.ArrayList; import java.util.List; /** @@ -33,6 +36,7 @@ public class ResolutionFeature extends CameraFeature { private EncoderProfiles recordingProfile; @NonNull private ResolutionPreset currentSetting; private int cameraId; + @NonNull private CaptureMode captureMode; /** * Creates a new instance of the {@link ResolutionFeature}. @@ -40,20 +44,27 @@ public class ResolutionFeature extends CameraFeature { * @param cameraProperties Collection of characteristics for the current camera device. * @param resolutionPreset Platform agnostic enum containing resolution information. * @param cameraName Camera identifier of the camera for which to configure the resolution. + * @param captureMode Capture mode to configure the appropriate resolution and aspect ratio. */ public ResolutionFeature( @NonNull CameraProperties cameraProperties, @NonNull ResolutionPreset resolutionPreset, - @NonNull String cameraName) { + @NonNull String cameraName, + @NonNull CaptureMode captureMode) { super(cameraProperties); this.currentSetting = resolutionPreset; + this.captureMode = captureMode; try { this.cameraId = Integer.parseInt(cameraName, 10); } catch (NumberFormatException e) { this.cameraId = -1; return; } - configureResolution(resolutionPreset, cameraId); + configureResolution( + resolutionPreset, + cameraId, + captureMode, + cameraProperties.getAvailableOutputSizes(ImageFormat.JPEG)); } /** @@ -92,6 +103,15 @@ public Size getCaptureSize() { return this.captureSize; } + /** + * Gets the current capture mode for the configured resolution. + * + * @return The current capture mode. + */ + public CaptureMode getCaptureMode() { + return this.captureMode; + } + @NonNull @Override public String getDebugName() { @@ -108,7 +128,11 @@ public ResolutionPreset getValue() { @Override public void setValue(@NonNull ResolutionPreset value) { this.currentSetting = value; - configureResolution(currentSetting, cameraId); + configureResolution( + currentSetting, + cameraId, + captureMode, + cameraProperties.getAvailableOutputSizes(ImageFormat.JPEG)); } @Override @@ -122,12 +146,18 @@ public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { } @VisibleForTesting - static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) + static Size computeBestPreviewSize( + int cameraId, ResolutionPreset preset, CaptureMode captureMode, Size[] availableOutputSizes) throws IndexOutOfBoundsException { + // Using max resolution for the preview is not a good use of system resources. + // Limiting the max resolution used for the preview to 720p is a good balance. if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } - if (SdkCapabilityChecker.supportsEncoderProfiles()) { + if (captureMode == CaptureMode.photo) { + return getBestAvailableCameraSizeForResolutionPreset(preset, availableOutputSizes); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { EncoderProfiles profile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); List videoProfiles = profile.getVideoProfiles(); @@ -262,28 +292,121 @@ public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPrese } } - private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) + // All of these cases deliberately fall through to get the best available camera profile. + @SuppressWarnings("fallthrough") + @NonNull + public static Size getBestAvailableCameraSizeForResolutionPreset( + @NonNull ResolutionPreset preset, Size[] availableOutputSizes) { + List availableStandardOutputSizes = new ArrayList<>(); + for (Size outputSize : availableOutputSizes) { + if ((Math.abs((double) outputSize.getWidth() / outputSize.getHeight() - (double) 4 / 3) + < 0.01)) { + availableStandardOutputSizes.add(outputSize); + } + } + Size selectedSize = null; + switch (preset) { + case max: + selectedSize = selectPhotoCaptureSize(null, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + // fall through + case ultraHigh: + selectedSize = selectPhotoCaptureSize(2160, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + // fall through + case veryHigh: + selectedSize = selectPhotoCaptureSize(1080, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + // fall through + case high: + // Both 768 and 720 are common HD picture heights. + selectedSize = selectPhotoCaptureSize(768, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + selectedSize = selectPhotoCaptureSize(720, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + // fall through + case medium: + selectedSize = selectPhotoCaptureSize(480, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + // fall through + case low: + selectedSize = selectPhotoCaptureSize(240, availableStandardOutputSizes); + if (selectedSize != null) { + return selectedSize; + } + // fall through + default: + // default to lowest available 4:3 resolution. + if (availableStandardOutputSizes.size() > 0) { + return availableStandardOutputSizes.get(availableStandardOutputSizes.size() - 1); + } + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + + private static Size selectPhotoCaptureSize( + Integer resolutionWidth, List availableStandardOutputSizes) { + Size selectedPreviewResolution = null; + int currentHighestPixel = 0; + for (Size standardOutputSize : availableStandardOutputSizes) { + // When no resolutionWidth is provided, the highest resolution should be selected. + if ((resolutionWidth != null && standardOutputSize.getHeight() == resolutionWidth) + || resolutionWidth == null) { + if (standardOutputSize.getWidth() * standardOutputSize.getHeight() > currentHighestPixel) { + selectedPreviewResolution = standardOutputSize; + currentHighestPixel = standardOutputSize.getWidth() * standardOutputSize.getHeight(); + } + } + } + return selectedPreviewResolution; + } + + private void configureResolution( + ResolutionPreset resolutionPreset, + int cameraId, + CaptureMode captureMode, + Size[] availableOutputSizes) throws IndexOutOfBoundsException { if (!checkIsSupported()) { return; } - boolean captureSizeCalculated = false; + // Attempt to select the highest resolution from the available ones when in photo mode. + if (captureMode == CaptureMode.photo) { + captureSize = + getBestAvailableCameraSizeForResolutionPreset(resolutionPreset, availableOutputSizes); + } - if (SdkCapabilityChecker.supportsEncoderProfiles()) { - recordingProfileLegacy = null; - recordingProfile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); - List videoProfiles = recordingProfile.getVideoProfiles(); + if (captureSize == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + boolean captureSizeCalculated = false; - EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); + if (SdkCapabilityChecker.supportsEncoderProfiles()) { + recordingProfileLegacy = null; + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + List videoProfiles = recordingProfile.getVideoProfiles(); - if (defaultVideoProfile != null) { - captureSizeCalculated = true; - captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); + + if (defaultVideoProfile != null) { + captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } } } - if (!captureSizeCalculated) { + if (captureSize == null) { recordingProfile = null; CamcorderProfile camcorderProfile = getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset); @@ -292,6 +415,7 @@ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight); } - previewSize = computeBestPreviewSize(cameraId, resolutionPreset); + previewSize = + computeBestPreviewSize(cameraId, resolutionPreset, captureMode, availableOutputSizes); } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java index 1a9cf18307f9..5c8d8814e1d7 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java @@ -104,7 +104,6 @@ public void onImageAvailable( } else { imageBuffer.put("planes", parsePlanesForYuvOrJpeg(image)); } - imageBuffer.put("width", image.getWidth()); imageBuffer.put("height", image.getHeight()); imageBuffer.put("format", dartImageFormat); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureMode.java new file mode 100644 index 000000000000..6cb90786e4f8 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureMode.java @@ -0,0 +1,33 @@ +// 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. + +package io.flutter.plugins.camera.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +// Mirrors camera.dart +public enum CaptureMode { + photo("photo"), + video("video"); + + private final String strValue; + + CaptureMode(String strValue) { + this.strValue = strValue; + } + + @Nullable + public static CaptureMode getValueForString(@NonNull String modeStr) { + for (CaptureMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java index 4dc68af94ca3..0d21c2d1d596 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -16,6 +16,7 @@ import android.util.Range; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DeviceInfo; +import io.flutter.plugins.camera.types.CaptureMode; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -35,22 +36,30 @@ public void after() { @Test public void ctor_shouldInitializeFpsRangeWithHighestUpperValueFromRangeArray() { - FpsRangeFeature fpsRangeFeature = createTestInstance(); + FpsRangeFeature fpsRangeFeature = createTestInstance(CaptureMode.video); assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); } @Test public void getDebugName_shouldReturnTheNameOfTheFeature() { - FpsRangeFeature fpsRangeFeature = createTestInstance(); + FpsRangeFeature fpsRangeFeature = createTestInstance(CaptureMode.video); assertEquals("FpsRangeFeature", fpsRangeFeature.getDebugName()); } @Test public void getValue_shouldReturnHighestUpperRangeIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FpsRangeFeature fpsRangeFeature = createTestInstance(); + FpsRangeFeature fpsRangeFeature = createTestInstance(CaptureMode.video); - assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); + assertEquals(60, (int) fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getValue_shouldReturnNoHigherThan60fpsInPhotoMode() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(CaptureMode.photo); + + assertEquals(30, (int) fpsRangeFeature.getValue().getUpper()); } @Test @@ -68,7 +77,7 @@ public void getValue_shouldEchoTheSetValue() { @Test public void checkIsSupported_shouldReturnTrue() { - FpsRangeFeature fpsRangeFeature = createTestInstance(); + FpsRangeFeature fpsRangeFeature = createTestInstance(CaptureMode.video); assertTrue(fpsRangeFeature.checkIsSupported()); } @@ -76,34 +85,37 @@ public void checkIsSupported_shouldReturnTrue() { @SuppressWarnings("unchecked") public void updateBuilder_shouldSetAeTargetFpsRange() { CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); - FpsRangeFeature fpsRangeFeature = createTestInstance(); + FpsRangeFeature fpsRangeFeature = createTestInstance(CaptureMode.video); fpsRangeFeature.updateBuilder(mockBuilder); verify(mockBuilder).set(eq(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE), any(Range.class)); } - private static FpsRangeFeature createTestInstance() { + private static FpsRangeFeature createTestInstance(CaptureMode captureMode) { @SuppressWarnings("unchecked") Range rangeOne = mock(Range.class); @SuppressWarnings("unchecked") Range rangeTwo = mock(Range.class); @SuppressWarnings("unchecked") Range rangeThree = mock(Range.class); - + @SuppressWarnings("unchecked") + Range rangeFour = mock(Range.class); when(rangeOne.getUpper()).thenReturn(11); when(rangeTwo.getUpper()).thenReturn(12); when(rangeThree.getUpper()).thenReturn(13); + when(rangeFour.getUpper()).thenReturn(60); // Use a wildcard, since `new Range[] {rangeOne, rangeTwo, rangeThree}` // results in a 'Generic array creation' error. @SuppressWarnings("unchecked") - Range[] ranges = (Range[]) new Range[] {rangeOne, rangeTwo, rangeThree}; + Range[] ranges = + (Range[]) new Range[] {rangeOne, rangeTwo, rangeThree, rangeFour}; CameraProperties cameraProperties = mock(CameraProperties.class); when(cameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(ranges); - return new FpsRangeFeature(cameraProperties); + return new FpsRangeFeature(cameraProperties, captureMode); } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index fe4dcd795fed..6ce4c2cc60bc 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -18,6 +18,7 @@ import android.media.EncoderProfiles; import android.util.Size; import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.types.CaptureMode; import java.util.ArrayList; import java.util.List; import org.junit.After; @@ -231,7 +232,8 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThroug @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMaxLegacy() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.max, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @@ -240,7 +242,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMaxLegacy() @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { before(); - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.max, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); } @@ -249,7 +252,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHighLegacy() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.ultraHigh, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @@ -258,7 +262,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHighLeg @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { before(); - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.ultraHigh, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); } @@ -267,7 +272,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHighLegacy() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.veryHigh, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @@ -277,7 +283,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHighLega @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { before(); - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.veryHigh, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); } @@ -286,7 +293,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHighLegacy() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.high, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @@ -295,7 +303,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHighLegacy() @Test public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { before(); - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.high, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_720P)); } @@ -304,7 +313,8 @@ public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMediumLegacy() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.medium, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); } @@ -313,7 +323,8 @@ public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMediumLegacy @Test public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { before(); - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.medium, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_480P)); } @@ -322,7 +333,8 @@ public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { @SuppressWarnings("deprecation") @Test public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLowLegacy() { - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.low, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); } @@ -331,7 +343,8 @@ public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLowLegacy() @Test public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { before(); - ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.low, CaptureMode.video, new Size[0]); mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)); } @@ -373,10 +386,15 @@ public void computeBestPreviewSize_shouldUseLegacyBehaviorWhenEncoderProfilesNul return mockCamcorderProfile; }); mockedResolutionFeature - .when(() -> ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max)) + .when( + () -> + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.max, CaptureMode.video, new Size[0])) .thenCallRealMethod(); - Size testPreviewSize = ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + Size testPreviewSize = + ResolutionFeature.computeBestPreviewSize( + 1, ResolutionPreset.max, CaptureMode.video, new Size[0]); assertEquals(testPreviewSize.getWidth(), 10); assertEquals(testPreviewSize.getHeight(), 50); } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/CaptureModeTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/CaptureModeTest.java new file mode 100644 index 000000000000..ee1d5a3bf28a --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/types/CaptureModeTest.java @@ -0,0 +1,35 @@ +// 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. + +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class CaptureModeTest { + @Test + public void getValueForString_returnsCorrectValues() { + assertEquals( + "Returns CaptureMode.image for 'photo'", + CaptureMode.getValueForString("photo"), + CaptureMode.photo); + assertEquals( + "Returns CaptureMode.video for 'video'", + CaptureMode.getValueForString("video"), + CaptureMode.video); + } + + @Test + public void getValueForString_returnsNullForNonexistantValue() { + assertEquals( + "Returns null for 'nonexistant'", CaptureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returnsCorrectValue() { + assertEquals("Returns 'image' for CaptureMode.photo", CaptureMode.photo.toString(), "photo"); + assertEquals("Returns 'video' for CaptureMode.video", CaptureMode.video.toString(), "video"); + } +} diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 8d663074df72..9705a4e00405 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -40,6 +40,16 @@ void main() { // Don't bother checking for max here since it could be anything. }; + final Map presetImageExpectedSizes = + { + ResolutionPreset.low: const Size(240, 320), + ResolutionPreset.medium: const Size(480, 720), + ResolutionPreset.high: const Size(768, 1024), + ResolutionPreset.veryHigh: const Size(1080, 1440), + ResolutionPreset.ultraHigh: const Size(2160, 2880), + // Don't bother checking for max here since it could be anything. + }; + /// Verify that [actual] has dimensions that are at least as large as /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns /// whether the dimensions exactly match. @@ -81,7 +91,7 @@ void main() { for (final CameraDescription cameraDescription in cameras) { bool previousPresetExactlySupported = true; for (final MapEntry preset - in presetExpectedSizes.entries) { + in presetImageExpectedSizes.entries) { final CameraController controller = CameraController(cameraDescription, preset.key); await controller.initialize(); diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 40f0eedbd821..0f4bb709e663 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -32,3 +32,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_android: {path: ../../../camera/camera_android}, camera_platform_interface: {path: ../../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index eca1003247c6..da1a954bef6b 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -369,6 +369,23 @@ class AndroidCamera extends CameraPlatform { }, ); + @override + Future setCaptureMode(int cameraId, CaptureMode mode) async { + final Map? reply = + await _channel.invokeMethod>( + 'setCaptureMode', + { + 'cameraId': cameraId, + 'mode': mode.name, + }, + ); + if (reply == null) { + return null; + } + return Size((reply['previewWidth']! as int).toDouble(), + (reply['previewHeight']! as int).toDouble()); + } + @override Future setExposurePoint(int cameraId, Point? point) { assert(point == null || point.x >= 0 && point.x <= 1); @@ -597,6 +614,7 @@ class AndroidCamera extends CameraPlatform { arguments['exposurePointSupported']! as bool, deserializeFocusMode(arguments['focusMode']! as String), arguments['focusPointSupported']! as bool, + CaptureMode.deserialize(arguments['captureMode']! as String), )); break; case 'resolution_changed': diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index f920e2336fa2..5ded6046a930 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.8+13 +version: 0.10.9 environment: sdk: ">=2.19.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.5.0 + camera_platform_interface: ^2.5.3 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 @@ -32,3 +32,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index fb6f6a71639a..2b00ce204fc9 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -204,6 +204,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; @@ -249,6 +250,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; @@ -297,6 +299,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; }); @@ -317,6 +320,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, ); await camera.handleCameraMethodCall( MethodCall('initialized', event.toJson()), cameraId); @@ -470,6 +474,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, ), ); await initializeFuture; diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index 663a58b8470b..a4963ffa6670 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -28,3 +28,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../../camera/camera_platform_interface}} 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 044778fc20f2..22078a8ad1b2 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 @@ -314,14 +314,18 @@ class AndroidCameraCameraX extends CameraPlatform { const FocusMode focusMode = FocusMode.auto; const bool focusPointSupported = false; + const CaptureMode captureMode = CaptureMode.video; + cameraEventStreamController.add(CameraInitializedEvent( - cameraId, - previewResolutionInfo.width.toDouble(), - previewResolutionInfo.height.toDouble(), - exposureMode, - exposurePointSupported, - focusMode, - focusPointSupported)); + cameraId, + previewResolutionInfo.width.toDouble(), + previewResolutionInfo.height.toDouble(), + exposureMode, + exposurePointSupported, + focusMode, + focusPointSupported, + captureMode, + )); } /// Releases the resources of the accessed camera. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index d7500c09d3ae..275e2dc1c021 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -36,3 +36,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} 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 1b2b7c50a84f..4be184302eec 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 @@ -485,13 +485,15 @@ void main() { // https://github.com/flutter/flutter/issues/120467 final CameraInitializedEvent testCameraInitializedEvent = CameraInitializedEvent( - cameraId, - resolutionWidth.toDouble(), - resolutionHeight.toDouble(), - ExposureMode.auto, - false, - FocusMode.auto, - false); + cameraId, + resolutionWidth.toDouble(), + resolutionHeight.toDouble(), + ExposureMode.auto, + false, + FocusMode.auto, + false, + CaptureMode.video, + ); camera.processCameraProvider = mockProcessCameraProvider; @@ -552,7 +554,15 @@ void main() { final StreamQueue streamQueue = StreamQueue(eventStream); const CameraInitializedEvent testEvent = CameraInitializedEvent( - cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false); + cameraId, + 320, + 80, + ExposureMode.auto, + false, + FocusMode.auto, + false, + CaptureMode.video, + ); camera.cameraEventStreamController.add(testEvent); diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index cac28692b704..f01b00e91738 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.14 + +* Adds `CaptureMode` as a parameter to allow configuring the camera for taking photos or videos. `CaptureMode.photo` selects an appropriate 4:3 aspect ratio depending on the `ResolutionPreset`. + ## 0.9.13+7 * Fixes inverted orientation strings. diff --git a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart index 315a34e9f9b1..a3422ab17106 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -30,7 +30,7 @@ void main() { await testDir.delete(recursive: true); }); - final Map presetExpectedSizes = + final Map presetVideoExpectedSizes = { ResolutionPreset.low: const Size(288, 352), ResolutionPreset.medium: const Size(480, 640), @@ -40,6 +40,16 @@ void main() { // Don't bother checking for max here since it could be anything. }; + final Map presetPhotoExpectedSizes = + { + ResolutionPreset.low: const Size(288, 352), + ResolutionPreset.medium: const Size(480, 640), + ResolutionPreset.high: const Size(768, 1024), + ResolutionPreset.veryHigh: const Size(1080, 1440), + ResolutionPreset.ultraHigh: const Size(2160, 4096), + // Don't bother checking for max here since it could be anything. + }; + /// Verify that [actual] has dimensions that are at least as large as /// [expectedSize]. Allows for a mismatch in portrait vs landscape. Returns /// whether the dimensions exactly match. @@ -53,9 +63,53 @@ void main() { // This tests that the capture is no bigger than the preset, since we have // automatic code to fall back to smaller sizes when we need to. Returns // whether the image is exactly the desired resolution. - Future testCaptureImageResolution( + Future testCaptureImageInVideoModeResolution( + CameraController controller, ResolutionPreset preset) async { + final Size expectedSize = presetVideoExpectedSizes[preset]!; + + // Take Picture + final XFile file = await controller.takePicture(); + + // Load picture + final File fileImage = File(file.path); + final Image image = await decodeImageFromList(fileImage.readAsBytesSync()); + + // Verify image dimensions are as expected + expect(image, isNotNull); + return assertExpectedDimensions( + expectedSize, Size(image.height.toDouble(), image.width.toDouble())); + } + + testWidgets('Capture specific image resolutions', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + for (final CameraDescription cameraDescription in cameras) { + bool previousPresetExactlySupported = true; + for (final MapEntry preset + in presetVideoExpectedSizes.entries) { + final CameraController controller = + CameraController(cameraDescription, preset.key); + await controller.initialize(); + final bool presetExactlySupported = + await testCaptureImageInVideoModeResolution(controller, preset.key); + assert(!(!previousPresetExactlySupported && presetExactlySupported), + 'The camera took higher resolution pictures at a lower resolution.'); + previousPresetExactlySupported = presetExactlySupported; + await controller.dispose(); + } + } + }); + + // This tests that the capture is no bigger than the preset, since we have + // automatic code to fall back to smaller sizes when we need to. Returns + // whether the image is exactly the desired resolution. + Future testCaptureImageInPhotoModeResolution( CameraController controller, ResolutionPreset preset) async { - final Size expectedSize = presetExpectedSizes[preset]!; + final Size expectedSize = presetPhotoExpectedSizes[preset]!; // Take Picture final XFile file = await controller.takePicture(); @@ -80,12 +134,13 @@ void main() { for (final CameraDescription cameraDescription in cameras) { bool previousPresetExactlySupported = true; for (final MapEntry preset - in presetExpectedSizes.entries) { + in presetVideoExpectedSizes.entries) { final CameraController controller = CameraController(cameraDescription, preset.key); await controller.initialize(); + await controller.setCaptureMode(CaptureMode.photo); final bool presetExactlySupported = - await testCaptureImageResolution(controller, preset.key); + await testCaptureImageInPhotoModeResolution(controller, preset.key); assert(!(!previousPresetExactlySupported && presetExactlySupported), 'The camera took higher resolution pictures at a lower resolution.'); previousPresetExactlySupported = presetExactlySupported; @@ -99,7 +154,7 @@ void main() { // whether the image is exactly the desired resolution. Future testCaptureVideoResolution( CameraController controller, ResolutionPreset preset) async { - final Size expectedSize = presetExpectedSizes[preset]!; + final Size expectedSize = presetVideoExpectedSizes[preset]!; // Take Video await controller.startVideoRecording(); @@ -129,7 +184,7 @@ void main() { for (final CameraDescription cameraDescription in cameras) { bool previousPresetExactlySupported = true; for (final MapEntry preset - in presetExpectedSizes.entries) { + in presetVideoExpectedSizes.entries) { final CameraController controller = CameraController(cameraDescription, preset.key); await controller.initialize(); diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 6006b9f2b4ba..88edce27fec9 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + CD52577A2B19827700ED0301 /* CameraCaptureModeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD5257792B19827700ED0301 /* CameraCaptureModeTests.m */; }; E01EE4A82799F3A5008C1950 /* QueueUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */; }; E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; }; E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; }; @@ -89,6 +90,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9C5CC6CAD53AD388B2694F3A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; A24F9E418BA48BCC7409B117 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + CD5257792B19827700ED0301 /* CameraCaptureModeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = CameraCaptureModeTests.m; path = RunnerTests/CameraCaptureModeTests.m; sourceTree = ""; }; E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueUtilsTests.m; sourceTree = ""; }; E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = ""; }; E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; @@ -178,6 +180,7 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + CD5257792B19827700ED0301 /* CameraCaptureModeTests.m */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 03BB76692665316900CE5A93 /* RunnerTests */, @@ -417,6 +420,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CD52577A2B19827700ED0301 /* CameraCaptureModeTests.m in Sources */, 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, @@ -495,7 +499,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.plugins.cameraExample.camera-exampleTests"; @@ -521,7 +529,11 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = RunnerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "dev.flutter.plugins.cameraExample.camera-exampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -648,7 +660,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -670,7 +685,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureModeTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureModeTests.m new file mode 100644 index 000000000000..e48841355f41 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureModeTests.m @@ -0,0 +1,42 @@ +// 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 camera_avfoundation; +@import camera_avfoundation.Test; +@import XCTest; +@import AVFoundation; +#import +#import "CameraTestUtils.h" +#import "MockFLTThreadSafeFlutterResult.h" + +@interface CameraCaptureModeTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@end + +@implementation CameraCaptureModeTests + +- (void)setUp { + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + + _camera = FLTCreateCamWithCaptureSessionQueue(captureSessionQueue); +} + +- (void)testCaptureMode_shouldBeVideoByDefault { + XCTAssertEqual(_camera.captureMode, FLTCaptureModeVideo); +} + +- (void)testCaptureMode_shouldBeVideoAfterSettingVideo { + [_camera setCaptureMode:FLTCaptureModePhoto]; + [_camera setCaptureMode:FLTCaptureModeVideo]; + XCTAssertEqual(_camera.captureMode, FLTCaptureModeVideo); +} + +- (void)testCaptureMode_shouldBePhotoAfterSettingPhoto { + [_camera setCaptureMode:FLTCaptureModePhoto]; + XCTAssertEqual(_camera.captureMode, FLTCaptureModePhoto); +} + +@end \ No newline at end of file diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m index c429d2d9751d..82b482467a85 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPropertiesTests.m @@ -104,4 +104,18 @@ - (void)testFLTGetStringForUIDeviceOrientation { XCTAssertEqualObjects(@"portraitUp", FLTGetStringForUIDeviceOrientation(-1)); } +#pragma mark - capture mode tests + +- (void)testFLTGetCaptureModeForString { + XCTAssertEqual(FLTCaptureModeVideo, FLTGetFLTCaptureModeForString(@"video")); + XCTAssertEqual(FLTCaptureModePhoto, FLTGetFLTCaptureModeForString(@"photo")); + XCTAssertEqual(FLTCaptureModeInvalid, FLTGetFLTCaptureModeForString(@"unknown")); +} + +- (void)testFLTGetStringForCaptureMode { + XCTAssertEqualObjects(@"video", FLTGetStringForFLTCaptureMode(FLTCaptureModeVideo)); + XCTAssertEqualObjects(@"photo", FLTGetStringForFLTCaptureMode(FLTCaptureModePhoto)); + XCTAssertNil(FLTGetStringForFLTCaptureMode(-1)); +} + @end diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart index 36b9ac68cfdd..13d7c5cb46e8 100644 --- a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -25,6 +25,7 @@ class CameraValue { required this.focusMode, required this.deviceOrientation, required this.description, + required this.captureMode, this.lockedCaptureOrientation, this.recordingOrientation, this.isPreviewPaused = false, @@ -45,6 +46,7 @@ class CameraValue { deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, description: description, + captureMode: CaptureMode.video, ); /// True after [CameraController.initialize] has completed successfully. @@ -97,6 +99,9 @@ class CameraValue { /// The properties of the camera device controlled by this controller. final CameraDescription description; + /// The capture mode of the camera device controlled by this controller. + final CaptureMode captureMode; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -119,6 +124,7 @@ class CameraValue { bool? isPreviewPaused, CameraDescription? description, Optional? previewPauseOrientation, + CaptureMode? captureMode, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -142,6 +148,7 @@ class CameraValue { previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, + captureMode: captureMode ?? this.captureMode, ); } @@ -159,7 +166,8 @@ class CameraValue { 'lockedCaptureOrientation: $lockedCaptureOrientation, ' 'recordingOrientation: $recordingOrientation, ' 'isPreviewPaused: $isPreviewPaused, ' - 'previewPausedOrientation: $previewPauseOrientation)'; + 'previewPausedOrientation: $previewPauseOrientation, ' + 'captureMode: $captureMode)'; } } @@ -437,6 +445,12 @@ class CameraController extends ValueNotifier { value = value.copyWith(focusMode: mode); } + /// Sets the capture mode for the camera. + Future setCaptureMode(CaptureMode mode) async { + await CameraPlatform.instance.setCaptureMode(_cameraId, mode); + value = value.copyWith(captureMode: mode); + } + /// Releases the resources of this camera. @override Future dispose() async { diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index 3680ea2419c1..39393cae940c 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -30,3 +30,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_avfoundation: {path: ../../../camera/camera_avfoundation}, camera_platform_interface: {path: ../../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m index 874f37b6c4fc..4f58a830ce22 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m @@ -179,6 +179,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call @"exposurePointSupported" : @([_camera.captureDevice isExposurePointOfInterestSupported]), @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), + @"captureMode" : FLTGetStringForFLTCaptureMode([_camera captureMode]), }]; [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; [_camera start]; @@ -225,6 +226,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call y = ((NSNumber *)call.arguments[@"y"]).doubleValue; } [_camera setExposurePointWithResult:result x:x y:y]; + } else if ([@"setCaptureMode" isEqualToString:call.method]) { + [_camera setCaptureModeWithResult:result mode:call.arguments[@"mode"]]; } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { [result sendSuccessWithData:@(_camera.captureDevice.minExposureTargetBias)]; } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h index 4d0818dc8164..199e5fc187ee 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.h @@ -120,6 +120,31 @@ typedef NS_ENUM(NSInteger, FLTResolutionPreset) { */ extern FLTResolutionPreset FLTGetFLTResolutionPresetForString(NSString *preset); +#pragma mark - capture mode + +/** + * Represents camera's capture mode. Mirrors CaptureMode in camera.dart. + */ +typedef NS_ENUM(NSInteger, FLTCaptureMode) { + FLTCaptureModePhoto, + FLTCaptureModeVideo, + // This should never occur; it indicates an unknown value was received over + // the platform channel. + FLTCaptureModeInvalid, +}; + +/** + * Gets FLTCaptureMode from its string representation. + * @param mode a string representation of FLTCaptureMode. + */ +extern FLTCaptureMode FLTGetFLTCaptureModeForString(NSString *mode); + +/** + * Gets a string representation of capture mode. + * @param mode capture mode + */ +extern NSString *FLTGetStringForFLTCaptureMode(FLTCaptureMode mode); + #pragma mark - video format /** diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m index 69daa515e1a9..8f32878465a7 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraProperties.m @@ -59,6 +59,31 @@ FLTExposureMode FLTGetFLTExposureModeForString(NSString *mode) { } } +#pragma mark - capture mode + +NSString *FLTGetStringForFLTCaptureMode(FLTCaptureMode mode) { + switch (mode) { + case FLTCaptureModePhoto: + return @"photo"; + case FLTCaptureModeVideo: + return @"video"; + case FLTCaptureModeInvalid: + // This value should never actually be used. + return nil; + } + return nil; +} + +FLTCaptureMode FLTGetFLTCaptureModeForString(NSString *mode) { + if ([mode isEqualToString:@"photo"]) { + return FLTCaptureModePhoto; + } else if ([mode isEqualToString:@"video"]) { + return FLTCaptureModeVideo; + } else { + return FLTCaptureModeInvalid; + } +} + #pragma mark - focus mode NSString *FLTGetStringForFLTFocusMode(FLTFocusMode mode) { diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h index fbf4ef4882cb..852fb6a54a28 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -25,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy) void (^onFrameAvailable)(void); @property(nonatomic) FLTThreadSafeMethodChannel *methodChannel; @property(assign, nonatomic) FLTResolutionPreset resolutionPreset; +@property(assign, nonatomic) FLTCaptureMode captureMode; @property(assign, nonatomic) FLTExposureMode exposureMode; @property(assign, nonatomic) FLTFocusMode focusMode; @property(assign, nonatomic) FLTFlashMode flashMode; @@ -67,6 +68,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)unlockCaptureOrientationWithResult:(FLTThreadSafeFlutterResult *)result; - (void)setFlashModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; - (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; +- (void)setCaptureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; - (void)applyFocusMode; diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index 1f9404033204..07060e6449dd 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -139,6 +139,7 @@ - (instancetype)initWithCameraName:(NSString *)cameraName _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName]; _flashMode = _captureDevice.hasFlash ? FLTFlashModeAuto : FLTFlashModeOff; _exposureMode = FLTExposureModeAuto; + _captureMode = FLTCaptureModeVideo; _focusMode = FLTFocusModeAuto; _lockedCaptureOrientation = UIDeviceOrientationUnknown; _deviceOrientation = orientation; @@ -168,7 +169,9 @@ - (instancetype)initWithCameraName:(NSString *)cameraName _motionManager = [[CMMotionManager alloc] init]; [_motionManager startAccelerometerUpdates]; - if (![self setCaptureSessionPreset:_resolutionPreset withError:error]) { + if (![self setCaptureSessionWithResolutionPreset:_resolutionPreset + withCaptureMode:_captureMode + withError:error]) { return nil; } [self updateOrientation]; @@ -345,7 +348,8 @@ - (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension return file; } -- (BOOL)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset withError:(NSError **)error { +- (BOOL)setCaptureSessionForVideo:(FLTResolutionPreset)resolutionPreset + withError:(NSError **)error { switch (resolutionPreset) { case FLTResolutionPresetMax: case FLTResolutionPresetUltraHigh: @@ -399,6 +403,97 @@ - (BOOL)setCaptureSessionPreset:(FLTResolutionPreset)resolutionPreset withError: return NO; } } + return YES; +} + +- (BOOL)setCaptureSessionForPhoto:(FLTResolutionPreset)resolutionPreset + withError:(NSError **)error { + switch (resolutionPreset) { + case FLTResolutionPresetMax: + case FLTResolutionPresetUltraHigh: + if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPresetPhoto]) { + _videoCaptureSession.sessionPreset = AVCaptureSessionPresetPhoto; + break; + } + case FLTResolutionPresetVeryHigh: + // Selects the appropriate 1080p resolution to match the desired aspect ratio for photos. + if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPresetInputPriority]) { + for (AVCaptureDeviceFormat *format in _captureDevice.formats) { + CMVideoDimensions dimensions = + CMVideoFormatDescriptionGetDimensions(format.formatDescription); + if (dimensions.height == 1080) { + if (fabs((double)dimensions.width / dimensions.height - (double)4 / 3) < 0.01) { + if ([_captureDevice lockForConfiguration:nil]) { + _captureDevice.activeFormat = format; + [_captureDevice unlockForConfiguration]; + return YES; + } + } + } + } + } + case FLTResolutionPresetHigh: + // Selects the appropriate 720p or 768p resolution to match the desired aspect ratio for + // photos. + if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPresetInputPriority]) { + for (AVCaptureDeviceFormat *format in _captureDevice.formats) { + CMVideoDimensions dimensions = + CMVideoFormatDescriptionGetDimensions(format.formatDescription); + if (dimensions.height == 720 || dimensions.height == 768) { + if (fabs((double)dimensions.width / dimensions.height - (double)4 / 3) < 0.01) { + if ([_captureDevice lockForConfiguration:nil]) { + _captureDevice.activeFormat = format; + [_captureDevice unlockForConfiguration]; + return YES; + } + } + } + } + } + // Most device format resolutions lower than 720/768p are generally 4:3 resolutions. Meaning + // the aspect ratio is the same regardless of the preset chosen. + case FLTResolutionPresetMedium: + if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) { + _videoCaptureSession.sessionPreset = AVCaptureSessionPreset640x480; + break; + } + case FLTResolutionPresetLow: + if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) { + _videoCaptureSession.sessionPreset = AVCaptureSessionPreset352x288; + break; + } + default: + if ([_videoCaptureSession canSetSessionPreset:AVCaptureSessionPresetLow]) { + _videoCaptureSession.sessionPreset = AVCaptureSessionPresetLow; + } else { + *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + @"No capture session available for current capture session." + }]; + return NO; + } + } + return YES; +} + +- (BOOL)setCaptureSessionWithResolutionPreset:(FLTResolutionPreset)resolutionPreset + withCaptureMode:(FLTCaptureMode)captureMode + withError:(NSError **)error { + // NSArray *formats = _captureDevice.formats; + if (captureMode == FLTCaptureModeVideo) { + if (![self setCaptureSessionForVideo:resolutionPreset withError:error]) { + return NO; + } + } else if (captureMode == FLTCaptureModePhoto) { + if (![self setCaptureSessionForPhoto:resolutionPreset withError:error]) { + return NO; + } + } + CMVideoDimensions previewDimensions = + CMVideoFormatDescriptionGetDimensions(_captureDevice.activeFormat.formatDescription); + _previewSize = CGSizeMake(previewDimensions.width, previewDimensions.height); _audioCaptureSession.sessionPreset = _videoCaptureSession.sessionPreset; return YES; } @@ -854,6 +949,28 @@ - (void)applyExposureMode { [_captureDevice unlockForConfiguration]; } +- (void)setCaptureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { + FLTCaptureMode mode = FLTGetFLTCaptureModeForString(modeStr); + if (mode == FLTCaptureModeInvalid) { + [result sendError:[NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown capture mode %@", modeStr] + }]]; + return; + } + NSError *error; + _captureMode = mode; + [self setCaptureSessionWithResolutionPreset:_resolutionPreset + withCaptureMode:_captureMode + withError:&error]; + [result sendSuccessWithData:@{ + @"previewWidth" : @(_previewSize.width), + @"previewHeight" : @(_previewSize.height), + }]; +} + - (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr { FLTFocusMode mode = FLTGetFLTFocusModeForString(modeStr); if (mode == FLTFocusModeInvalid) { diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index 33f550b3211b..fdf8b36f53b8 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -375,6 +375,23 @@ class AVFoundationCamera extends CameraPlatform { }, ); + @override + Future setCaptureMode(int cameraId, CaptureMode mode) async { + final Map? reply = + await _channel.invokeMethod>( + 'setCaptureMode', + { + 'cameraId': cameraId, + 'mode': mode.name, + }, + ); + if (reply == null) { + return null; + } + return Size( + reply['previewWidth']! as double, reply['previewHeight']! as double); + } + @override Future setExposurePoint(int cameraId, Point? point) { assert(point == null || point.x >= 0 && point.x <= 1); @@ -603,6 +620,7 @@ class AVFoundationCamera extends CameraPlatform { arguments['exposurePointSupported']! as bool, deserializeFocusMode(arguments['focusMode']! as String), arguments['focusPointSupported']! as bool, + CaptureMode.deserialize(arguments['captureMode']! as String), )); break; case 'resolution_changed': diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 3de45b68908e..e541bcdba2b4 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.13+7 +version: 0.9.14 environment: sdk: ">=2.19.0 <4.0.0" @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.4.0 + camera_platform_interface: ^2.5.3 flutter: sdk: flutter stream_transform: ^2.0.0 @@ -29,3 +29,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 17b06cddc1ae..83929954dea0 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -205,6 +205,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; @@ -250,6 +251,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; @@ -298,6 +300,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; }); @@ -318,6 +321,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, ); await camera.handleCameraMethodCall( MethodCall('initialized', event.toJson()), cameraId); @@ -472,6 +476,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, ), ); await initializeFuture; @@ -1145,6 +1150,27 @@ void main() { isMethodCall('stopImageStream', arguments: null), ]); }); + + test('Should set the capture mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: { + 'setCaptureMode': null, + }, + ); + + // Act + await camera.setCaptureMode(cameraId, CaptureMode.photo); + + // Assert + expect(channel.log, [ + isMethodCall('setCaptureMode', arguments: { + 'cameraId': cameraId, + 'mode': CaptureMode.photo.name + }), + ]); + }); }); } diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index eaac4bc7ecc8..b02896e2a168 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.1 + +* Adds `CaptureMode` as a parameter to allow configuring the camera for taking photos or videos. `CaptureMode.photo` selects an appropriate 4:3 aspect ratio depending on the `ResolutionPreset`. + ## 2.6.0 * Adds support to control video fps and bitrate. See `CameraPlatform.createCameraWithSettings`. diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart index 1c76c341d9b1..cb876e0735a4 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -58,6 +58,7 @@ class CameraInitializedEvent extends CameraEvent { this.exposurePointSupported, this.focusMode, this.focusPointSupported, + this.captureMode, ); /// Converts the supplied [Map] to an instance of the [CameraInitializedEvent] @@ -70,6 +71,9 @@ class CameraInitializedEvent extends CameraEvent { (json['exposurePointSupported'] as bool?) ?? false, focusMode = deserializeFocusMode(json['focusMode']! as String), focusPointSupported = (json['focusPointSupported'] as bool?) ?? false, + captureMode = (json['captureMode'] != null) + ? CaptureMode.deserialize(json['captureMode']! as String) + : CaptureMode.video, super(json['cameraId']! as int); /// The width of the preview in pixels. @@ -84,6 +88,9 @@ class CameraInitializedEvent extends CameraEvent { /// The default focus mode final FocusMode focusMode; + /// The default capture mode + final CaptureMode captureMode; + /// Whether setting exposure points is supported. final bool exposurePointSupported; @@ -100,6 +107,7 @@ class CameraInitializedEvent extends CameraEvent { 'exposurePointSupported': exposurePointSupported, 'focusMode': serializeFocusMode(focusMode), 'focusPointSupported': focusPointSupported, + 'captureMode': captureMode.name, }; @override @@ -113,7 +121,8 @@ class CameraInitializedEvent extends CameraEvent { exposureMode == other.exposureMode && exposurePointSupported == other.exposurePointSupported && focusMode == other.focusMode && - focusPointSupported == other.focusPointSupported; + focusPointSupported == other.focusPointSupported && + captureMode == other.captureMode; @override int get hashCode => Object.hash( @@ -124,6 +133,7 @@ class CameraInitializedEvent extends CameraEvent { exposurePointSupported, focusMode, focusPointSupported, + captureMode, ); } diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index 35ece04fdf52..74ae5be67c65 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -398,6 +398,16 @@ class MethodChannelCamera extends CameraPlatform { ); } + @override + Future setCaptureMode(int cameraId, CaptureMode mode) => + _channel.invokeMethod( + 'setCaptureMode', + { + 'cameraId': cameraId, + 'mode': mode.name, + }, + ); + @override Future getMinExposureOffset(int cameraId) async { final double? minExposureOffset = await _channel.invokeMethod( @@ -599,6 +609,7 @@ class MethodChannelCamera extends CameraPlatform { arguments['exposurePointSupported']! as bool, deserializeFocusMode(arguments['focusMode']! as String), arguments['focusPointSupported']! as bool, + CaptureMode.deserialize(arguments['captureMode']! as String), )); break; case 'resolution_changed': diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index b74cb4203324..5cacf8628183 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -273,6 +273,13 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('setZoomLevel() is not implemented.'); } + /// Set the capture mode for the selected camera. + /// + /// See [CaptureMode] for more information. + Future setCaptureMode(int cameraId, CaptureMode mode) { + throw UnimplementedError('setCaptureMode() is not implemented.'); + } + /// Pause the active preview on the current frame for the selected camera. Future pausePreview(int cameraId) { throw UnimplementedError('pausePreview() is not implemented.'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/capture_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/capture_mode.dart new file mode 100644 index 000000000000..83f252591339 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/capture_mode.dart @@ -0,0 +1,30 @@ +// 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. + +/// The mode the controller should operate in. +/// +/// This capture mode determines whether the capture session is optimized for +/// video recording or photo capture. +/// +/// Defaults to [CaptureMode.video] as the camera plugin configuration is +/// currently geared towards video recording. +enum CaptureMode { + /// Capture a photo. + photo, + + /// Capture a video, however this allows the user to take photos while recording. + video; + + /// Deserializes the [captureMode] string argument to the corresponding CaptureMode enum. + factory CaptureMode.deserialize(String captureMode) { + switch (captureMode) { + case 'photo': + return CaptureMode.photo; + case 'video': + return CaptureMode.video; + } + + throw ArgumentError('"$captureMode" is not a valid CaptureMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart index fcb6b83bbf14..48549ddfb2bf 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart @@ -12,15 +12,17 @@ enum ResolutionPreset { /// 480p (640x480 on iOS, 720x480 on Android and Web) medium, - /// 720p (1280x720) + /// 720p or 768p (1280x720, 1024x768 or 960x720) high, - /// 1080p (1920x1080) + /// 1080p (1920x1080 or 1440x1080) veryHigh, - /// 2160p (3840x2160 on Android and iOS, 4096x2160 on Web) + /// 2160p (3840x2160 on Android and iOS, 4096x2160 on Web or 2880x2160) ultraHigh, - /// The highest resolution available. + /// The highest resolution available. When CaptureMode is video, this is the same as [ultraHigh]. + /// When CaptureMode is photo, this is the maximum resolution of the camera which is typically + /// a 4:3 resolution. max, } diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index f9a81559d680..349fa7735d83 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -5,6 +5,7 @@ export 'camera_description.dart'; export 'camera_exception.dart'; export 'camera_image_data.dart'; +export 'capture_mode.dart'; export 'exposure_mode.dart'; export 'flash_mode.dart'; export 'focus_mode.dart'; diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index b0c3a1d8bff0..370480718b1d 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.6.0 +version: 2.6.1 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart index 074f203bea21..facb2e7a052f 100644 --- a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -10,8 +10,8 @@ void main() { group('CameraInitializedEvent tests', () { test('Constructor should initialize all properties', () { - const CameraInitializedEvent event = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + const CameraInitializedEvent event = CameraInitializedEvent(1, 1024, 640, + ExposureMode.auto, true, FocusMode.auto, true, CaptureMode.video); expect(event.cameraId, 1); expect(event.previewWidth, 1024); @@ -20,6 +20,7 @@ void main() { expect(event.focusMode, FocusMode.auto); expect(event.exposurePointSupported, true); expect(event.focusPointSupported, true); + expect(event.captureMode, CaptureMode.video); }); test('fromJson should initialize all properties', () { @@ -45,11 +46,19 @@ void main() { test('toJson should return a map with all fields', () { const CameraInitializedEvent event = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); final Map jsonMap = event.toJson(); - expect(jsonMap.length, 7); + expect(jsonMap.length, 8); expect(jsonMap['cameraId'], 1); expect(jsonMap['previewWidth'], 1024); expect(jsonMap['previewHeight'], 640); @@ -57,49 +66,130 @@ void main() { expect(jsonMap['exposurePointSupported'], true); expect(jsonMap['focusMode'], 'auto'); expect(jsonMap['focusPointSupported'], true); + expect(jsonMap['captureMode'], 'video'); }); test('equals should return true if objects are the same', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, true); }); test('equals should return false if cameraId is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 2, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 2, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, false); }); test('equals should return false if previewWidth is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 2048, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 2048, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, false); }); test('equals should return false if previewHeight is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 1024, 980, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 980, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, false); }); test('equals should return false if exposureMode is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.locked, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.locked, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, false); }); @@ -107,34 +197,115 @@ void main() { test('equals should return false if exposurePointSupported is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, false, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + false, + FocusMode.auto, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, false); }); test('equals should return false if focusMode is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.locked, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.locked, + true, + CaptureMode.video, + ); expect(firstEvent == secondEvent, false); }); test('equals should return false if focusPointSupported is different', () { const CameraInitializedEvent firstEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); const CameraInitializedEvent secondEvent = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, false); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + false, + CaptureMode.video, + ); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureMode is different', () { + const CameraInitializedEvent firstEvent = CameraInitializedEvent( + 1, + 1024, + 640, + ExposureMode.locked, + true, + FocusMode.locked, + true, + CaptureMode.video, + ); + const CameraInitializedEvent secondEvent = CameraInitializedEvent( + 1, + 1024, + 640, + ExposureMode.locked, + true, + FocusMode.locked, + true, + CaptureMode.photo, + ); expect(firstEvent == secondEvent, false); }); test('hashCode should match hashCode of all properties', () { const CameraInitializedEvent event = CameraInitializedEvent( - 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + 1, + 1024, + 640, + ExposureMode.auto, + true, + FocusMode.auto, + true, + CaptureMode.video, + ); final int expectedHashCode = Object.hash( event.cameraId.hashCode, event.previewWidth, @@ -142,7 +313,8 @@ void main() { event.exposureMode, event.exposurePointSupported, event.focusMode, - event.focusPointSupported); + event.focusPointSupported, + event.captureMode); expect(event.hashCode, expectedHashCode); }); diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 8159cdfe26f5..5e46c475d272 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -212,6 +212,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; @@ -263,6 +264,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; @@ -317,6 +319,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, )); await initializeFuture; }); @@ -337,6 +340,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, ); await camera.handleCameraMethodCall( MethodCall('initialized', event.toJson()), cameraId); @@ -494,6 +498,7 @@ void main() { true, FocusMode.auto, true, + CaptureMode.video, ), ); await initializeFuture; @@ -808,6 +813,30 @@ void main() { ]); }); + test('Should set the capture mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setCaptureMode': null}, + ); + + // Act + await camera.setCaptureMode(cameraId, CaptureMode.video); + await camera.setCaptureMode(cameraId, CaptureMode.photo); + + // Assert + expect(channel.log, [ + isMethodCall('setCaptureMode', arguments: { + 'cameraId': cameraId, + 'mode': 'video' + }), + isMethodCall('setCaptureMode', arguments: { + 'cameraId': cameraId, + 'mode': 'photo' + }), + ]); + }); + test('Should get the min exposure offset', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 6c73bcf76bdc..b3672a95abe1 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -2375,6 +2375,7 @@ void main() { false, FocusMode.auto, false, + CaptureMode.video, ), ), ); diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml index fef2a773262b..05cc94f79033 100644 --- a/packages/camera/camera_web/example/pubspec.yaml +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -20,3 +20,8 @@ dev_dependencies: integration_test: sdk: flutter mocktail: 0.3.0 + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index be41e7b9a874..7fc9bf634b5f 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -316,6 +316,7 @@ class CameraPlugin extends CameraPlatform { // TODO(bselwe): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). FocusMode.auto, false, + CaptureMode.video, ), ); } on html.DomException catch (e) { diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index f2f8f7caec4d..f77e54853169 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -30,3 +30,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_windows/example/pubspec.yaml b/packages/camera/camera_windows/example/pubspec.yaml index 1dd6f634e95a..e4d35f006680 100644 --- a/packages/camera/camera_windows/example/pubspec.yaml +++ b/packages/camera/camera_windows/example/pubspec.yaml @@ -27,3 +27,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 4b0c1586f433..adc60a2a0e71 100644 --- a/packages/camera/camera_windows/lib/camera_windows.dart +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -126,6 +126,7 @@ class CameraWindows extends CameraPlatform { false, FocusMode.auto, false, + CaptureMode.video, ), ); } diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index c78633fbe2f4..24ae342aed2d 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -30,3 +30,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}}