diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 0bf9721e6ab9..6276e41013da 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.11 + +* Adds `setJpegImageQuality` for controlling JPEG compression quality. + ## 0.10.10+16 * Updates build files from Groovy to Kotlin. 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 818151d754fe..5f59bc425495 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 @@ -53,6 +53,7 @@ import io.flutter.plugins.camera.features.flash.FlashMode; import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.jpegquality.JpegQualityFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; @@ -1416,6 +1417,20 @@ public void setDescriptionWhileRecording(CameraProperties properties) { } } + /** + * Sets the JPEG compression quality for still image capture. + * + * @param quality JPEG quality value between 1 and 100. + */ + public void setJpegImageQuality(@NonNull Long quality) { + JpegQualityFeature jpegQualityFeature = cameraFeatures.getJpegQuality(); + if (jpegQualityFeature == null) { + jpegQualityFeature = cameraFeatureFactory.createJpegQualityFeature(cameraProperties); + cameraFeatures.setJpegQuality(jpegQualityFeature); + } + jpegQualityFeature.setValue(quality.intValue()); + } + public void dispose() { Log.i(TAG, "dispose"); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraApiImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraApiImpl.java index b3b04d5309e5..28a8d9a17a27 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraApiImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraApiImpl.java @@ -343,6 +343,11 @@ public void setDescriptionWhileRecording(@NonNull String cameraName) { } } + @Override + public void setJpegImageQuality(@NonNull Long quality) { + camera.setJpegImageQuality(quality); + } + @Override public void dispose() { if (camera != null) { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java index e61e0c48c199..0670586bd782 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Messages.java @@ -1079,6 +1079,9 @@ void create( */ void setDescriptionWhileRecording(@NonNull String description); + /** Sets the JPEG compression quality for still image capture. */ + void setJpegImageQuality(@NonNull Long quality); + /** The codec used by CameraApi. */ static @NonNull MessageCodec getCodec() { return PigeonCodec.INSTANCE; @@ -1809,6 +1812,31 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.camera_android.CameraApi.setJpegImageQuality" + + messageChannelSuffix, + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + ArrayList args = (ArrayList) message; + Long qualityArg = (Long) args.get(0); + try { + api.setJpegImageQuality(qualityArg); + wrapped.add(0, null); + } catch (Throwable exception) { + wrapped = wrapError(exception); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } 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 4027e665e71a..7f5ad2e6b9dd 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 @@ -15,6 +15,7 @@ import io.flutter.plugins.camera.features.flash.FlashFeature; import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.jpegquality.JpegQualityFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; @@ -157,4 +158,14 @@ ExposurePointFeature createExposurePointFeature( */ @NonNull NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the JPEG quality feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the JpegQualityFeature class. + */ + @NonNull + JpegQualityFeature createJpegQualityFeature(@NonNull CameraProperties cameraProperties); } 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 c333d8f485ad..91ff9d991a48 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 @@ -15,6 +15,7 @@ import io.flutter.plugins.camera.features.flash.FlashFeature; import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.jpegquality.JpegQualityFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; @@ -105,4 +106,10 @@ public NoiseReductionFeature createNoiseReductionFeature( @NonNull CameraProperties cameraProperties) { return new NoiseReductionFeature(cameraProperties); } + + @NonNull + @Override + public JpegQualityFeature createJpegQualityFeature(@NonNull CameraProperties cameraProperties) { + return new JpegQualityFeature(cameraProperties); + } } 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 c700e3b2c184..69068cec7050 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 @@ -6,6 +6,7 @@ import android.app.Activity; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; @@ -15,6 +16,7 @@ import io.flutter.plugins.camera.features.flash.FlashFeature; import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.jpegquality.JpegQualityFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; @@ -41,6 +43,7 @@ public class CameraFeatures { private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; private static final String RESOLUTION = "RESOLUTION"; private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; + private static final String JPEG_QUALITY = "JPEG_QUALITY"; private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; @NonNull @@ -297,4 +300,23 @@ public ZoomLevelFeature getZoomLevel() { public void setZoomLevel(@NonNull ZoomLevelFeature zoomLevel) { this.featureMap.put(ZOOM_LEVEL, zoomLevel); } + + /** + * Gets the JPEG quality feature if it has been set. + * + * @return the JPEG quality feature, or null if not set. + */ + @Nullable + public JpegQualityFeature getJpegQuality() { + return (JpegQualityFeature) featureMap.get(JPEG_QUALITY); + } + + /** + * Sets the instance of the JPEG quality feature. + * + * @param jpegQuality the {@link JpegQualityFeature} instance to set. + */ + public void setJpegQuality(@NonNull JpegQualityFeature jpegQuality) { + this.featureMap.put(JPEG_QUALITY, jpegQuality); + } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeature.java new file mode 100644 index 000000000000..5b07b82fccda --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeature.java @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors +// 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.features.jpegquality; + +import android.annotation.SuppressLint; +import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Controls the JPEG compression quality on the {@link android.hardware.camera2} API. */ +public class JpegQualityFeature extends CameraFeature { + private int currentSetting = 100; + + /** + * Creates a new instance of the {@link JpegQualityFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + */ + public JpegQualityFeature(@NonNull CameraProperties cameraProperties) { + super(cameraProperties); + } + + @NonNull + @Override + public String getDebugName() { + return "JpegQualityFeature"; + } + + @SuppressLint("KotlinPropertyAccess") + @NonNull + @Override + public Integer getValue() { + return currentSetting; + } + + @Override + public void setValue(@NonNull Integer value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { + requestBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) currentSetting); + } +} diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index 85e317cd0c0b..108225e06055 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -45,6 +45,7 @@ import io.flutter.plugins.camera.features.flash.FlashMode; import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.jpegquality.JpegQualityFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; @@ -1468,5 +1469,10 @@ public NoiseReductionFeature createNoiseReductionFeature( @NonNull CameraProperties cameraProperties) { return mockNoiseReductionFeature; } + + @Override + public JpegQualityFeature createJpegQualityFeature(@NonNull CameraProperties cameraProperties) { + return mock(JpegQualityFeature.class); + } } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java index 4094be54f4d4..c6998199ae61 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraTest_getRecordingProfileTest.java @@ -26,6 +26,7 @@ import io.flutter.plugins.camera.features.flash.FlashFeature; import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.jpegquality.JpegQualityFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionPreset; @@ -200,5 +201,10 @@ public NoiseReductionFeature createNoiseReductionFeature( @NonNull CameraProperties cameraProperties) { return mockNoiseReductionFeature; } + + @Override + public JpegQualityFeature createJpegQualityFeature(@NonNull CameraProperties cameraProperties) { + return mock(JpegQualityFeature.class); + } } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeatureTest.java new file mode 100644 index 000000000000..6c873e7ca589 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeatureTest.java @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors +// 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.features.jpegquality; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class JpegQualityFeatureTest { + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + JpegQualityFeature jpegQualityFeature = new JpegQualityFeature(mockCameraProperties); + + assertEquals("JpegQualityFeature", jpegQualityFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturn100IfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + JpegQualityFeature jpegQualityFeature = new JpegQualityFeature(mockCameraProperties); + + assertEquals(Integer.valueOf(100), jpegQualityFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + JpegQualityFeature jpegQualityFeature = new JpegQualityFeature(mockCameraProperties); + + jpegQualityFeature.setValue(50); + assertEquals(Integer.valueOf(50), jpegQualityFeature.getValue()); + } + + @Test + public void checkIsSupported_shouldReturnTrue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + JpegQualityFeature jpegQualityFeature = new JpegQualityFeature(mockCameraProperties); + + assertTrue(jpegQualityFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldSetJpegQualityOnBuilder() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + JpegQualityFeature jpegQualityFeature = new JpegQualityFeature(mockCameraProperties); + + jpegQualityFeature.setValue(75); + jpegQualityFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.JPEG_QUALITY, (byte) 75); + } + + @Test + public void updateBuilder_shouldSetDefaultQualityWhenNotExplicitlySet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + JpegQualityFeature jpegQualityFeature = new JpegQualityFeature(mockCameraProperties); + + jpegQualityFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.JPEG_QUALITY, (byte) 100); + } +} diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index f4d614defd67..505e8dcdd0ca 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.13.0 flutter: sdk: flutter path_provider: ^2.0.0 diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index 9f7d580f2364..e79d4ef1f55b 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -380,6 +380,10 @@ class AndroidCamera extends CameraPlatform { await _hostApi.setDescriptionWhileRecording(description.name); } + @override + Future setJpegImageQuality(int cameraId, int quality) => + _hostApi.setJpegImageQuality(quality); + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_android/lib/src/messages.g.dart b/packages/camera/camera_android/lib/src/messages.g.dart index e4c047452511..f6ae1293e4d9 100644 --- a/packages/camera/camera_android/lib/src/messages.g.dart +++ b/packages/camera/camera_android/lib/src/messages.g.dart @@ -1255,6 +1255,33 @@ class CameraApi { return; } } + + Future setJpegImageQuality(int quality) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_android.CameraApi.setJpegImageQuality$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [quality], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } /// Handles calls from native side to Dart that are not camera-specific. diff --git a/packages/camera/camera_android/pigeons/messages.dart b/packages/camera/camera_android/pigeons/messages.dart index 4489fe3f6fb8..a8f1bd582270 100644 --- a/packages/camera/camera_android/pigeons/messages.dart +++ b/packages/camera/camera_android/pigeons/messages.dart @@ -207,6 +207,9 @@ abstract class CameraApi { /// /// This should be called only while video recording is active. void setDescriptionWhileRecording(String description); + + /// Sets the JPEG compression quality for still image capture. + void setJpegImageQuality(int quality); } /// Handles calls from native side to Dart that are not camera-specific. diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index 34ffc900e502..d532e9fd41b2 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.10+16 +version: 0.10.11 environment: sdk: ^3.9.0 @@ -19,7 +19,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.9.0 + camera_platform_interface: ^2.13.0 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index 8321001ed954..de6c4eee7057 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -873,5 +873,14 @@ void main() { verify(mockCameraApi.startImageStream()).called(1); verify(mockCameraApi.stopImageStream()).called(1); }); + + test('Should set the image quality', () async { + // Arrange + // Act + await camera.setJpegImageQuality(cameraId, 50); + + // Assert + verify(mockCameraApi.setJpegImageQuality(50)).called(1); + }); }); } diff --git a/packages/camera/camera_android/test/android_camera_test.mocks.dart b/packages/camera/camera_android/test/android_camera_test.mocks.dart index 53cd6e9489a1..e03db33b106d 100644 --- a/packages/camera/camera_android/test/android_camera_test.mocks.dart +++ b/packages/camera/camera_android/test/android_camera_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in camera_android/test/android_camera_test.dart. // Do not manually edit this file. @@ -17,10 +17,12 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [CameraApi]. /// @@ -316,4 +318,13 @@ class MockCameraApi extends _i1.Mock implements _i2.CameraApi { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future setJpegImageQuality(int? quality) => + (super.noSuchMethod( + Invocation.method(#setJpegImageQuality, [quality]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); } diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 191d67e7a1c8..db6f5da368ce 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.7.2 +* Adds `setJpegImageQuality` for controlling JPEG compression quality. * Bumps camerax_version from 1.5.3 to 1.6.0. ## 0.7.1+2 diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt index 2bdf4371dfed..8945b77c0101 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt @@ -4247,7 +4247,8 @@ abstract class PigeonApiImageCapture( abstract fun pigeon_defaultConstructor( resolutionSelector: androidx.camera.core.resolutionselector.ResolutionSelector?, targetRotation: Long?, - flashMode: CameraXFlashMode? + flashMode: CameraXFlashMode?, + jpegQuality: Long? ): androidx.camera.core.ImageCapture abstract fun resolutionSelector( @@ -4288,11 +4289,12 @@ abstract class PigeonApiImageCapture( args[1] as androidx.camera.core.resolutionselector.ResolutionSelector? val targetRotationArg = args[2] as Long? val flashModeArg = args[3] as CameraXFlashMode? + val jpegQualityArg = args[4] as Long? val wrapped: List = try { api.pigeonRegistrar.instanceManager.addDartCreatedInstance( api.pigeon_defaultConstructor( - resolutionSelectorArg, targetRotationArg, flashModeArg), + resolutionSelectorArg, targetRotationArg, flashModeArg, jpegQualityArg), pigeon_identifierArg) listOf(null) } catch (exception: Throwable) { diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java index 78ba586b2314..eeb4731b227e 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java @@ -40,7 +40,8 @@ public ProxyApiRegistrar getPigeonRegistrar() { public ImageCapture pigeon_defaultConstructor( @Nullable ResolutionSelector resolutionSelector, @Nullable Long targetRotation, - @Nullable CameraXFlashMode flashMode) { + @Nullable CameraXFlashMode flashMode, + @Nullable Long jpegQuality) { final ImageCapture.Builder builder = new ImageCapture.Builder(); if (targetRotation != null) { builder.setTargetRotation(targetRotation.intValue()); @@ -62,6 +63,9 @@ public ImageCapture pigeon_defaultConstructor( if (resolutionSelector != null) { builder.setResolutionSelector(resolutionSelector); } + if (jpegQuality != null) { + builder.setJpegQuality(jpegQuality.intValue()); + } return builder.build(); } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java index 5c84dff947df..13f4397fbe29 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java @@ -39,13 +39,30 @@ public void pigeon_defaultConstructor_createsImageCaptureWithCorrectConfiguratio final long targetResolution = Surface.ROTATION_0; final ImageCapture imageCapture = api.pigeon_defaultConstructor( - mockResolutionSelector, targetResolution, CameraXFlashMode.OFF); + mockResolutionSelector, targetResolution, CameraXFlashMode.OFF, null); assertEquals(imageCapture.getResolutionSelector(), mockResolutionSelector); assertEquals(imageCapture.getTargetRotation(), Surface.ROTATION_0); assertEquals(imageCapture.getFlashMode(), ImageCapture.FLASH_MODE_OFF); } + @Test + public void pigeon_defaultConstructor_setsJpegQualityWhenProvided() { + final PigeonApiImageCapture api = new TestProxyApiRegistrar().getPigeonApiImageCapture(); + + final ResolutionSelector mockResolutionSelector = new ResolutionSelector.Builder().build(); + final long targetRotation = Surface.ROTATION_0; + final long jpegQuality = 75; + final ImageCapture imageCapture = + api.pigeon_defaultConstructor( + mockResolutionSelector, targetRotation, CameraXFlashMode.OFF, jpegQuality); + + assertEquals(imageCapture.getResolutionSelector(), mockResolutionSelector); + assertEquals(imageCapture.getTargetRotation(), Surface.ROTATION_0); + assertEquals(imageCapture.getFlashMode(), ImageCapture.FLASH_MODE_OFF); + assertEquals(imageCapture.getJpegQuality(), 75); + } + @Test public void resolutionSelector_returnsExpectedResolutionSelector() { final PigeonApiImageCapture api = new TestProxyApiRegistrar().getPigeonApiImageCapture(); diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index 81a4e079b2ae..8fc5e2380436 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.13.0 flutter: sdk: flutter video_player: ^2.7.0 @@ -28,4 +28,3 @@ dev_dependencies: flutter: uses-material-design: true - 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 e52e4bd09cf1..bffcee28b3ec 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 @@ -205,6 +205,13 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting bool captureOrientationLocked = false; + /// The target rotation set by [lockCaptureOrientation], if any. + /// + /// Used to preserve the locked rotation when recreating use cases (e.g., + /// in [setJpegImageQuality]). + @visibleForTesting + int? lockedCaptureOrientation; + /// Whether or not the default rotation for [UseCase]s needs to be set /// manually because the capture orientation was previously locked. /// @@ -588,6 +595,7 @@ class AndroidCameraCameraX extends CameraPlatform { final int targetLockedRotation = _getRotationConstantFromDeviceOrientation( orientation, ); + lockedCaptureOrientation = targetLockedRotation; // Update UseCases to use target device orientation. await imageCapture!.setTargetRotation(targetLockedRotation); @@ -600,6 +608,7 @@ class AndroidCameraCameraX extends CameraPlatform { Future unlockCaptureOrientation(int cameraId) async { // Flag that default rotation should be set for UseCases as needed. captureOrientationLocked = false; + lockedCaptureOrientation = null; } /// Sets the exposure point for automatically determining the exposure values for @@ -1126,6 +1135,31 @@ class AndroidCameraCameraX extends CameraPlatform { } } + /// Sets the JPEG compression quality for still image capture. + /// + /// CameraX only supports setting JPEG quality via `ImageCapture.Builder` + /// at construction time, so this recreates the `ImageCapture` use case + /// with the requested quality. The next call to [takePicture] will bind + /// the new instance automatically. + @override + Future setJpegImageQuality(int cameraId, int quality) async { + // Unbind the current ImageCapture if it exists and is bound. + if (imageCapture != null) { + await _unbindUseCaseFromLifecycle(imageCapture!); + } + + // Recreate ImageCapture with the requested JPEG quality. + // Preserve locked orientation if set, otherwise use default display rotation. + final int targetRotation = + lockedCaptureOrientation ?? + await deviceOrientationManager.getDefaultDisplayRotation(); + imageCapture = ImageCapture( + resolutionSelector: _presetResolutionSelector, + targetRotation: targetRotation, + jpegQuality: quality, + ); + } + /// Prepare the capture session for video recording. /// /// This optimization is not used on Android, so this implementation is a diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index 7b642695e0b0..6d06b565b09c 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -123,6 +123,7 @@ class PigeonOverrides { ResolutionSelector? resolutionSelector, int? targetRotation, CameraXFlashMode? flashMode, + int? jpegQuality, })? imageCapture_new; @@ -5118,12 +5119,14 @@ class ImageCapture extends UseCase { ResolutionSelector? resolutionSelector, int? targetRotation, CameraXFlashMode? flashMode, + int? jpegQuality, }) { if (PigeonOverrides.imageCapture_new != null) { return PigeonOverrides.imageCapture_new!( resolutionSelector: resolutionSelector, targetRotation: targetRotation, flashMode: flashMode, + jpegQuality: jpegQuality, ); } return ImageCapture.pigeon_new( @@ -5132,6 +5135,7 @@ class ImageCapture extends UseCase { resolutionSelector: resolutionSelector, targetRotation: targetRotation, flashMode: flashMode, + jpegQuality: jpegQuality, ); } @@ -5142,6 +5146,7 @@ class ImageCapture extends UseCase { this.resolutionSelector, int? targetRotation, CameraXFlashMode? flashMode, + int? jpegQuality, }) : super.pigeon_detached() { final int pigeonVar_instanceIdentifier = pigeon_instanceManager .addDartCreatedInstance(this); @@ -5161,6 +5166,7 @@ class ImageCapture extends UseCase { resolutionSelector, targetRotation, flashMode, + jpegQuality, ], ); () async { diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 611157f65248..479dc0a6829d 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -598,7 +598,11 @@ enum CameraXFlashMode { ), ) abstract class ImageCapture extends UseCase { - ImageCapture(int? targetRotation, CameraXFlashMode? flashMode); + ImageCapture( + int? targetRotation, + CameraXFlashMode? flashMode, + int? jpegQuality, + ); late final ResolutionSelector? resolutionSelector; diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 6e928e2518aa..45dd2812c1ca 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -19,7 +19,7 @@ flutter: dependencies: async: ^2.5.0 - camera_platform_interface: ^2.12.0 + camera_platform_interface: ^2.13.0 flutter: sdk: flutter meta: ^1.7.0 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 71ed3d58ccde..5f68b48b3802 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 @@ -197,6 +197,7 @@ void main() { int? targetRotation, CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, + int? jpegQuality, }) { final mockImageCapture = MockImageCapture(); when( @@ -630,6 +631,7 @@ void main() { int? targetRotation, CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, + int? jpegQuality, }) { return mockImageCapture; }; @@ -1281,6 +1283,7 @@ void main() { int? targetRotation, CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, + int? jpegQuality, }) { return mockImageCapture; }; @@ -1771,6 +1774,7 @@ void main() { int? targetRotation, CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, + int? jpegQuality, }) { return mockImageCapture; }; @@ -2195,6 +2199,7 @@ void main() { int? targetRotation, CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, + int? jpegQuality, }) => mockImageCapture; PigeonOverrides.recorder_new = ({ @@ -3364,6 +3369,7 @@ void main() { CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, int? targetRotation, + int? jpegQuality, }) { return mockImageCapture; }; @@ -3627,6 +3633,7 @@ void main() { CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, int? targetRotation, + int? jpegQuality, }) { return mockImageCapture; }; @@ -3905,6 +3912,108 @@ void main() { }, ); + test( + 'setJpegImageQuality unbinds and recreates ImageCapture with requested quality', + () async { + final camera = AndroidCameraCameraX(); + final mockProcessCameraProvider = MockProcessCameraProvider(); + final mockDeviceOrientationManager = MockDeviceOrientationManager(); + final mockImageCapture = MockImageCapture(); + final mockNewImageCapture = MockImageCapture(); + const int defaultTargetRotation = Surface.rotation90; + const jpegQuality = 73; + const cameraId = 9; + int? actualTargetRotation; + int? actualJpegQuality; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.imageCapture = mockImageCapture; + + PigeonOverrides.deviceOrientationManager_new = + ({ + required void Function(DeviceOrientationManager, String) + onDeviceOrientationChanged, + }) { + when( + mockDeviceOrientationManager.getDefaultDisplayRotation(), + ).thenAnswer((_) async => defaultTargetRotation); + return mockDeviceOrientationManager; + }; + PigeonOverrides.imageCapture_new = + ({ + int? targetRotation, + CameraXFlashMode? flashMode, + ResolutionSelector? resolutionSelector, + int? jpegQuality, + }) { + actualTargetRotation = targetRotation; + actualJpegQuality = jpegQuality; + return mockNewImageCapture; + }; + + when( + mockProcessCameraProvider.isBound(mockImageCapture), + ).thenAnswer((_) async => true); + + await camera.setJpegImageQuality(cameraId, jpegQuality); + + verify( + mockProcessCameraProvider.unbind([mockImageCapture]), + ).called(1); + verify( + mockDeviceOrientationManager.getDefaultDisplayRotation(), + ).called(1); + expect(actualTargetRotation, defaultTargetRotation); + expect(actualJpegQuality, jpegQuality); + expect(camera.imageCapture, same(mockNewImageCapture)); + }, + ); + + test( + 'setJpegImageQuality preserves locked target rotation when recreating ImageCapture', + () async { + final camera = AndroidCameraCameraX(); + final mockDeviceOrientationManager = MockDeviceOrientationManager(); + final mockNewImageCapture = MockImageCapture(); + const int lockedTargetRotation = Surface.rotation270; + const jpegQuality = 64; + const cameraId = 11; + int? actualTargetRotation; + int? actualJpegQuality; + + camera.lockedCaptureOrientation = lockedTargetRotation; + + PigeonOverrides.deviceOrientationManager_new = + ({ + required void Function(DeviceOrientationManager, String) + onDeviceOrientationChanged, + }) { + when( + mockDeviceOrientationManager.getDefaultDisplayRotation(), + ).thenAnswer((_) async => Surface.rotation0); + return mockDeviceOrientationManager; + }; + PigeonOverrides.imageCapture_new = + ({ + int? targetRotation, + CameraXFlashMode? flashMode, + ResolutionSelector? resolutionSelector, + int? jpegQuality, + }) { + actualTargetRotation = targetRotation; + actualJpegQuality = jpegQuality; + return mockNewImageCapture; + }; + + await camera.setJpegImageQuality(cameraId, jpegQuality); + + verifyNever(mockDeviceOrientationManager.getDefaultDisplayRotation()); + expect(actualTargetRotation, lockedTargetRotation); + expect(actualJpegQuality, jpegQuality); + expect(camera.imageCapture, same(mockNewImageCapture)); + }, + ); + test( 'takePicture turns non-torch flash mode off when torch mode enabled', () async { diff --git a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart index cfc6b2db103b..9d7b2531ba09 100644 --- a/packages/camera/camera_android_camerax/test/preview_rotation_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_rotation_test.dart @@ -123,6 +123,7 @@ void main() { int? targetRotation, CameraXFlashMode? flashMode, ResolutionSelector? resolutionSelector, + int? jpegQuality, }) => MockImageCapture(); PigeonOverrides.recorder_new = ({ diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index ca97b101df8b..fe3789d174a1 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.2 + +* Adds `setJpegImageQuality` for controlling JPEG compression quality. + ## 0.10.1 * Fixes fatal crash on iPhone 17 when using `ResolutionPreset.max`. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift index 34a306b511d7..f01da07aa12f 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginDelegatingMethodTests.swift @@ -251,6 +251,28 @@ final class CameraPluginDelegatingMethodTests: XCTestCase { XCTAssertTrue(setImageFileFormatCalled) } + func testSetImageQuality_callsCameraSetImageQuality() { + let (cameraPlugin, mockCamera) = createCameraPlugin() + let expectation = expectation(description: "Call completed") + + let targetQuality: Int64 = 50 + + var setJpegImageQualityCalled = false + mockCamera.setJpegImageQualityStub = { quality in + XCTAssertEqual(quality, targetQuality) + setJpegImageQualityCalled = true + } + + cameraPlugin.setJpegImageQuality(quality: targetQuality) { result in + let _ = self.assertSuccess(result) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(setJpegImageQualityCalled) + } + func testStartImageStream_callsCameraStartImageStream() { let (cameraPlugin, mockCamera) = createCameraPlugin() let expectation = expectation(description: "Call completed") diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift index ec6e6fd88f9c..569f55630155 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift @@ -27,6 +27,7 @@ final class MockCamera: NSObject, Camera { var lockCaptureOrientationStub: ((PlatformDeviceOrientation) -> Void)? var unlockCaptureOrientationStub: (() -> Void)? var setImageFileFormatStub: ((PlatformImageFileFormat) -> Void)? + var setJpegImageQualityStub: ((Int64) -> Void)? var setExposureModeStub: ((PlatformExposureMode) -> Void)? var setExposureOffsetStub: ((Double) -> Void)? var setExposurePointStub: ((PlatformPoint?, @escaping (Result) -> Void) -> Void)? @@ -144,6 +145,10 @@ final class MockCamera: NSObject, Camera { setImageFileFormatStub?(fileFormat) } + func setJpegImageQuality(_ quality: Int64) { + setJpegImageQualityStub?(quality) + } + func setExposureMode(_ mode: PlatformExposureMode) { setExposureModeStub?(mode) } diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index 4b9a5b5a5e53..d9cce5808257 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.10.0 + camera_platform_interface: ^2.13.0 flutter: sdk: flutter path_provider: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift index 117fd909d32e..02e9503f6adc 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift @@ -62,6 +62,7 @@ protocol Camera: FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate, func unlockCaptureOrientation() func setImageFileFormat(_ fileFormat: PlatformImageFileFormat) + func setJpegImageQuality(_ quality: Int64) func setExposureMode(_ mode: PlatformExposureMode) func setExposureOffset(_ offset: Double) diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift index 43ef5f48916c..136c05f18d58 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift @@ -555,4 +555,13 @@ extension CameraPlugin: CameraApi { completion(.success(())) } } + + func setJpegImageQuality( + quality: Int64, completion: @escaping (Result) -> Void + ) { + captureSessionQueue.async { [weak self] in + self?.camera?.setJpegImageQuality(quality) + completion(.success(())) + } + } } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index 16d1637d23fa..d702248a6d7b 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -121,6 +121,7 @@ final class DefaultCamera: NSObject, Camera { private var maxStreamingPendingFramesCount = 4 private var fileFormat = PlatformImageFileFormat.jpeg + private var imageQuality: Int64 = 100 private var lockedCaptureOrientation = UIDeviceOrientation.unknown private var exposureMode = PlatformExposureMode.auto private var focusMode = PlatformFocusMode.auto @@ -719,6 +720,17 @@ final class DefaultCamera: NSObject, Camera { fileExtension = "heif" } else { fileExtension = "jpg" + if imageQuality < 100 { + settings = AVCapturePhotoSettings(format: [ + AVVideoCodecKey: AVVideoCodecType.jpeg, + AVVideoCompressionPropertiesKey: [ + AVVideoQualityKey: CGFloat(imageQuality) / 100.0 + ], + ]) + if mediaSettings.resolutionPreset == .max { + settings.isHighResolutionPhotoEnabled = true + } + } } if flashMode != .torch { @@ -842,6 +854,10 @@ final class DefaultCamera: NSObject, Camera { self.fileFormat = fileFormat } + func setJpegImageQuality(_ quality: Int64) { + self.imageQuality = quality + } + func setExposureMode(_ mode: PlatformExposureMode) { exposureMode = mode applyExposureMode() diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Messages.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Messages.swift index 47e6abbbe750..e170c2acb97a 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Messages.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Messages.swift @@ -723,6 +723,9 @@ protocol CameraApi { /// Sets the file format used for taking pictures. func setImageFileFormat( format: PlatformImageFileFormat, completion: @escaping (Result) -> Void) + /// Sets the JPEG compression quality for still image capture. + func setJpegImageQuality( + quality: Int64, completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -1363,6 +1366,26 @@ class CameraApiSetup { } else { setImageFileFormatChannel.setMessageHandler(nil) } + /// Sets the JPEG compression quality for still image capture. + let setJpegImageQualityChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.camera_avfoundation.CameraApi.setJpegImageQuality\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setJpegImageQualityChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let qualityArg = args[0] is Int64 ? args[0] as! Int64 : Int64(args[0] as! Int32) + api.setJpegImageQuality(quality: qualityArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + setJpegImageQualityChannel.setMessageHandler(nil) + } } } diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index 3907ed89219b..2327e66314c5 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -442,6 +442,11 @@ class AVFoundationCamera extends CameraPlatform { await _hostApi.setImageFileFormat(_pigeonImageFileFormat(format)); } + @override + Future setJpegImageQuality(int cameraId, int quality) async { + await _hostApi.setJpegImageQuality(quality); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); diff --git a/packages/camera/camera_avfoundation/lib/src/messages.g.dart b/packages/camera/camera_avfoundation/lib/src/messages.g.dart index 46c94d58f8a1..8371f6481f9f 100644 --- a/packages/camera/camera_avfoundation/lib/src/messages.g.dart +++ b/packages/camera/camera_avfoundation/lib/src/messages.g.dart @@ -1487,6 +1487,31 @@ class CameraApi { return; } } + + Future setJpegImageQuality(int quality) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setJpegImageQuality$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [quality], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } Stream imageDataStream({String instanceName = ''}) { diff --git a/packages/camera/camera_avfoundation/pigeons/messages.dart b/packages/camera/camera_avfoundation/pigeons/messages.dart index b4976758cf47..2d0e3ceec187 100644 --- a/packages/camera/camera_avfoundation/pigeons/messages.dart +++ b/packages/camera/camera_avfoundation/pigeons/messages.dart @@ -349,6 +349,11 @@ abstract class CameraApi { @async @ObjCSelector('setImageFileFormat:') void setImageFileFormat(PlatformImageFileFormat format); + + /// Sets the JPEG compression quality for still image capture. + @async + @ObjCSelector('setJpegImageQuality:') + void setJpegImageQuality(int quality); } @EventChannelApi() diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 8a10d0d21f72..7ff9cd278609 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.10.1 +version: 0.10.2 environment: sdk: ^3.9.0 @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.12.0 + camera_platform_interface: ^2.13.0 flutter: sdk: flutter stream_transform: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 53d7965c6bac..733d5e0d8e05 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -992,5 +992,11 @@ void main() { verify(mockApi.setImageFileFormat(PlatformImageFileFormat.jpeg)); }); + + test('Should set the image quality', () async { + await camera.setJpegImageQuality(cameraId, 50); + + verify(mockApi.setJpegImageQuality(50)); + }); }); } diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart index 225eac9931c6..7e946cdf89f5 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart @@ -22,6 +22,7 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [CameraApi]. /// @@ -360,4 +361,13 @@ class MockCameraApi extends _i1.Mock implements _i2.CameraApi { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future setJpegImageQuality(int? quality) => + (super.noSuchMethod( + Invocation.method(#setJpegImageQuality, [quality]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); }