From c0ee9c04092ad875efb8d7e01ba4d4711e21f5c6 Mon Sep 17 00:00:00 2001 From: Ludwig Bolling Date: Thu, 30 Apr 2026 16:10:32 +0200 Subject: [PATCH 1/4] [camera] Add setJpegImageQuality for JPEG compression control Adds setJpegImageQuality(int quality) across all camera platform implementations (Android, Android CameraX, iOS AVFoundation) and the app-facing camera package. This allows controlling the JPEG compression quality (1-100) for still image capture. Platform interface changes were landed separately in #11454. --- packages/camera/camera/CHANGELOG.md | 3 +- packages/camera/camera/example/pubspec.yaml | 5 + .../camera/lib/src/camera_controller.dart | 18 +++ packages/camera/camera/pubspec.yaml | 13 ++- .../camera/test/camera_preview_test.dart | 3 + packages/camera/camera/test/camera_test.dart | 83 +++++++++++++ packages/camera/camera_android/CHANGELOG.md | 3 +- .../io/flutter/plugins/camera/Camera.java | 15 +++ .../flutter/plugins/camera/CameraApiImpl.java | 5 + .../io/flutter/plugins/camera/Messages.java | 28 +++++ .../camera/features/CameraFeatureFactory.java | 11 ++ .../features/CameraFeatureFactoryImpl.java | 7 ++ .../camera/features/CameraFeatures.java | 22 ++++ .../jpegquality/JpegQualityFeature.java | 53 +++++++++ .../io/flutter/plugins/camera/CameraTest.java | 6 + .../CameraTest_getRecordingProfileTest.java | 6 + .../jpegquality/JpegQualityFeatureTest.java | 73 ++++++++++++ .../camera_android/example/pubspec.yaml | 2 +- .../lib/src/android_camera.dart | 4 + .../camera_android/lib/src/messages.g.dart | 27 +++++ .../camera_android/pigeons/messages.dart | 3 + packages/camera/camera_android/pubspec.yaml | 4 +- .../test/android_camera_test.dart | 9 ++ .../test/android_camera_test.mocks.dart | 13 ++- .../camera_android_camerax/CHANGELOG.md | 1 + .../plugins/camerax/CameraXLibrary.g.kt | 6 +- .../plugins/camerax/ImageCaptureProxyApi.java | 6 +- .../plugins/camerax/ImageCaptureTest.java | 19 ++- .../example/pubspec.yaml | 3 +- .../lib/src/android_camera_camerax.dart | 34 ++++++ .../lib/src/camerax_library.g.dart | 6 + .../pigeons/camerax_library.dart | 6 +- .../camera_android_camerax/pubspec.yaml | 2 +- .../test/android_camera_camerax_test.dart | 109 ++++++++++++++++++ .../test/preview_rotation_test.dart | 1 + .../camera/camera_avfoundation/CHANGELOG.md | 3 +- .../CameraPluginDelegatingMethodTests.swift | 22 ++++ .../ios/RunnerTests/Mocks/MockCamera.swift | 5 + .../camera_avfoundation/example/pubspec.yaml | 2 +- .../Sources/camera_avfoundation/Camera.swift | 1 + .../camera_avfoundation/CameraPlugin.swift | 9 ++ .../camera_avfoundation/DefaultCamera.swift | 16 +++ .../camera_avfoundation/Messages.swift | 23 ++++ .../lib/src/avfoundation_camera.dart | 5 + .../lib/src/messages.g.dart | 25 ++++ .../camera_avfoundation/pigeons/messages.dart | 5 + .../camera/camera_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_camera_test.dart | 6 + .../test/avfoundation_camera_test.mocks.dart | 10 ++ 49 files changed, 723 insertions(+), 22 deletions(-) create mode 100644 packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeature.java create mode 100644 packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/jpegquality/JpegQualityFeatureTest.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 41077b35b449..2ec12e21f112 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.12.1 +* Adds `setJpegImageQuality` for controlling JPEG compression quality. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 0.12.0+1 diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index d94d8bef8cfa..d33a9042f196 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -31,3 +31,8 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + camera_android_camerax: {path: ../../../../packages/camera/camera_android_camerax} + camera_avfoundation: {path: ../../../../packages/camera/camera_avfoundation} diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index d941c2e85f66..1683ac8c2f0c 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -994,6 +994,24 @@ class CameraController extends ValueNotifier { } } + /// Sets the JPEG compression quality for still image capture. + /// + /// The [quality] must be between 1 (lowest) and 100 (highest). + Future setJpegImageQuality(int quality) async { + if (quality < 1 || quality > 100) { + throw ArgumentError.value( + quality, + 'quality', + 'Must be between 1 and 100.', + ); + } + try { + await CameraPlatform.instance.setJpegImageQuality(_cameraId, quality); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Check whether the camera platform supports image streaming. bool supportsImageStreaming() => CameraPlatform.instance.supportsImageStreaming(); diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f2cecaf6a7bc..276931375187 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.12.0+1 +version: 0.12.1 environment: sdk: ^3.10.0 @@ -21,9 +21,9 @@ flutter: default_package: camera_web dependencies: - camera_android_camerax: ^0.7.0 - camera_avfoundation: ^0.10.0 - camera_platform_interface: ^2.12.0 + camera_android_camerax: ^0.7.1 + camera_avfoundation: ^0.10.1 + camera_platform_interface: ^2.13.0 camera_web: ^0.3.3 flutter: sdk: flutter @@ -38,3 +38,8 @@ dev_dependencies: topics: - camera +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + camera_android_camerax: {path: ../../../packages/camera/camera_android_camerax} + camera_avfoundation: {path: ../../../packages/camera/camera_avfoundation} diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 912a583e9255..cfd3af86bc6a 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -149,6 +149,9 @@ class FakeController extends ValueNotifier Future> getSupportedVideoStabilizationModes() async => []; + @override + Future setJpegImageQuality(int quality) async {} + @override bool supportsImageStreaming() => true; } diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 913d3391cd9e..fe95180cccd0 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -885,6 +885,83 @@ void main() { }, ); + test('setJpegImageQuality() calls CameraPlatform', () async { + final cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + await cameraController.setJpegImageQuality(50); + + verify( + CameraPlatform.instance.setJpegImageQuality(cameraController.cameraId, 50), + ).called(1); + }); + + test( + 'setJpegImageQuality() throws CameraException on PlatformException', + () async { + final cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.setJpegImageQuality( + cameraController.cameraId, + 50, + ), + ).thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + ), + ); + + expect( + cameraController.setJpegImageQuality(50), + throwsA( + isA().having( + (CameraException error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ), + ), + ); + }, + ); + + test('setJpegImageQuality() throws ArgumentError for invalid values', () async { + final cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + expect( + () => cameraController.setJpegImageQuality(0), + throwsA(isA()), + ); + expect( + () => cameraController.setJpegImageQuality(101), + throwsA(isA()), + ); + }); + test('setExposureMode() calls $CameraPlatform', () async { final cameraController = CameraController( const CameraDescription( @@ -4152,6 +4229,12 @@ class MockCameraPlatform extends Mock ) async => super.noSuchMethod( Invocation.method(#setVideoStabilizationMode, [cameraId, mode]), ); + + @override + Future setJpegImageQuality(int? cameraId, int? quality) async => + super.noSuchMethod( + Invocation.method(#setJpegImageQuality, [cameraId, quality]), + ); } class MockCameraDescription extends CameraDescription { diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index b7bb3e538c37..eb853990ccfa 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.10.11 +* Adds `setJpegImageQuality` for controlling JPEG compression quality. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 0.10.10+17 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 1d543bd86559..95149befabdd 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 @@ -54,6 +54,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; @@ -1447,6 +1448,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 3cf677190a2a..52392e882ff7 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; @@ -1528,5 +1529,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 de76b088a7f3..7f683825c765 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 0d95eb81fa70..bda26510ce31 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+17 +version: 0.10.11 environment: sdk: ^3.10.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 954d384e8c86..9bfa27ea77f1 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -4,6 +4,7 @@ ## 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 04df4cb6830f..4d7f853b96c7 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 dca2b8d08f80..2686772b380c 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 { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; 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 a1d93d1210dd..f7f417d1ebc0 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 f4f30dd13301..03cdabfb6a44 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.10.2 +* Adds `setJpegImageQuality` for controlling JPEG compression quality. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 0.10.1 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 e7450716a9d5..a038f12b1935 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 f5065fb411b9..26f63f894fb4 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.10.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); } From 7a53aeeea63d56c11754c765a6c239b0698f7b3b Mon Sep 17 00:00:00 2001 From: Ludwig Bolling Date: Thu, 30 Apr 2026 16:27:36 +0200 Subject: [PATCH 2/4] Remove camera app-facing package changes The app-facing camera package should be submitted as a separate follow-up PR after the implementation packages are published. This removes the dependency_overrides that were blocking merge. --- packages/camera/camera/CHANGELOG.md | 3 +- packages/camera/camera/example/pubspec.yaml | 5 -- .../camera/lib/src/camera_controller.dart | 18 ---- packages/camera/camera/pubspec.yaml | 13 +-- .../camera/test/camera_preview_test.dart | 3 - packages/camera/camera/test/camera_test.dart | 83 ------------------- 6 files changed, 5 insertions(+), 120 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 2ec12e21f112..41077b35b449 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,6 +1,5 @@ -## 0.12.1 +## NEXT -* Adds `setJpegImageQuality` for controlling JPEG compression quality. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 0.12.0+1 diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index d33a9042f196..d94d8bef8cfa 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -31,8 +31,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - camera_android_camerax: {path: ../../../../packages/camera/camera_android_camerax} - camera_avfoundation: {path: ../../../../packages/camera/camera_avfoundation} diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 1683ac8c2f0c..d941c2e85f66 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -994,24 +994,6 @@ class CameraController extends ValueNotifier { } } - /// Sets the JPEG compression quality for still image capture. - /// - /// The [quality] must be between 1 (lowest) and 100 (highest). - Future setJpegImageQuality(int quality) async { - if (quality < 1 || quality > 100) { - throw ArgumentError.value( - quality, - 'quality', - 'Must be between 1 and 100.', - ); - } - try { - await CameraPlatform.instance.setJpegImageQuality(_cameraId, quality); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - /// Check whether the camera platform supports image streaming. bool supportsImageStreaming() => CameraPlatform.instance.supportsImageStreaming(); diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 276931375187..f2cecaf6a7bc 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.12.1 +version: 0.12.0+1 environment: sdk: ^3.10.0 @@ -21,9 +21,9 @@ flutter: default_package: camera_web dependencies: - camera_android_camerax: ^0.7.1 - camera_avfoundation: ^0.10.1 - camera_platform_interface: ^2.13.0 + camera_android_camerax: ^0.7.0 + camera_avfoundation: ^0.10.0 + camera_platform_interface: ^2.12.0 camera_web: ^0.3.3 flutter: sdk: flutter @@ -38,8 +38,3 @@ dev_dependencies: topics: - camera -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - camera_android_camerax: {path: ../../../packages/camera/camera_android_camerax} - camera_avfoundation: {path: ../../../packages/camera/camera_avfoundation} diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index cfd3af86bc6a..912a583e9255 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -149,9 +149,6 @@ class FakeController extends ValueNotifier Future> getSupportedVideoStabilizationModes() async => []; - @override - Future setJpegImageQuality(int quality) async {} - @override bool supportsImageStreaming() => true; } diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index fe95180cccd0..913d3391cd9e 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -885,83 +885,6 @@ void main() { }, ); - test('setJpegImageQuality() calls CameraPlatform', () async { - final cameraController = CameraController( - const CameraDescription( - name: 'cam', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90, - ), - ResolutionPreset.max, - ); - await cameraController.initialize(); - - await cameraController.setJpegImageQuality(50); - - verify( - CameraPlatform.instance.setJpegImageQuality(cameraController.cameraId, 50), - ).called(1); - }); - - test( - 'setJpegImageQuality() throws CameraException on PlatformException', - () async { - final cameraController = CameraController( - const CameraDescription( - name: 'cam', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90, - ), - ResolutionPreset.max, - ); - await cameraController.initialize(); - - when( - CameraPlatform.instance.setJpegImageQuality( - cameraController.cameraId, - 50, - ), - ).thenThrow( - PlatformException( - code: 'TEST_ERROR', - message: 'This is a test error message', - ), - ); - - expect( - cameraController.setJpegImageQuality(50), - throwsA( - isA().having( - (CameraException error) => error.description, - 'TEST_ERROR', - 'This is a test error message', - ), - ), - ); - }, - ); - - test('setJpegImageQuality() throws ArgumentError for invalid values', () async { - final cameraController = CameraController( - const CameraDescription( - name: 'cam', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90, - ), - ResolutionPreset.max, - ); - await cameraController.initialize(); - - expect( - () => cameraController.setJpegImageQuality(0), - throwsA(isA()), - ); - expect( - () => cameraController.setJpegImageQuality(101), - throwsA(isA()), - ); - }); - test('setExposureMode() calls $CameraPlatform', () async { final cameraController = CameraController( const CameraDescription( @@ -4229,12 +4152,6 @@ class MockCameraPlatform extends Mock ) async => super.noSuchMethod( Invocation.method(#setVideoStabilizationMode, [cameraId, mode]), ); - - @override - Future setJpegImageQuality(int? cameraId, int? quality) async => - super.noSuchMethod( - Invocation.method(#setJpegImageQuality, [cameraId, quality]), - ); } class MockCameraDescription extends CameraDescription { From 6c1ecccb091be0d972d2cf55ee9ee8077cdc7e29 Mon Sep 17 00:00:00 2001 From: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Date: Tue, 26 May 2026 13:28:53 -0400 Subject: [PATCH 3/4] fix version bump for camrax --- packages/camera/camera_android_camerax/CHANGELOG.md | 4 ++-- packages/camera/camera_android_camerax/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 9bfa27ea77f1..0fd18ca9a954 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,10 +1,10 @@ -## NEXT +## 0.7.3 +* Adds `setJpegImageQuality` for controlling JPEG compression quality. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 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/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f7f417d1ebc0..a720402d2be0 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.7.2 +version: 0.7.3 environment: sdk: ^3.10.0 From 6f8ec01bacb7aaeef956deaafa0f2f9ce9c17ec1 Mon Sep 17 00:00:00 2001 From: Ludwig Bolling Date: Thu, 28 May 2026 09:08:25 +0200 Subject: [PATCH 4/4] - CameraTest.java: Added setJpegImageQuality_shouldSetQualityOnFeature test - android_camera_camerax.dart: Made lockedCaptureOrientation private - android_camera_camerax_test.dart: Updated locked rotation test to use lockCaptureOrientation, added takePicture-after-setJpegImageQuality test --- .../io/flutter/plugins/camera/CameraTest.java | 10 ++ .../lib/src/android_camera_camerax.dart | 11 +- .../test/android_camera_camerax_test.dart | 105 +++++++++++++++++- 3 files changed, 118 insertions(+), 8 deletions(-) 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 52392e882ff7..538f3142e675 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 @@ -1385,6 +1385,16 @@ public void startVideoRecording_shouldApplySettingsToMediaRecorder() } } + @Test + public void setJpegImageQuality_shouldSetQualityOnFeature() { + JpegQualityFeature mockJpegQualityFeature = + mockCameraFeatureFactory.createJpegQualityFeature(mockCameraProperties); + + camera.setJpegImageQuality(75L); + + verify(mockJpegQualityFeature, times(1)).setValue(75); + } + @Test public void pausePreview_doesNotCallStopRepeatingWhenCameraClosed() throws CameraAccessException { ArrayList mockRequestBuilders = new ArrayList<>(); 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 bffcee28b3ec..d30d62aefbd1 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 @@ -207,10 +207,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// 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; + int? _lockedCaptureOrientation; /// Whether or not the default rotation for [UseCase]s needs to be set /// manually because the capture orientation was previously locked. @@ -595,7 +592,7 @@ class AndroidCameraCameraX extends CameraPlatform { final int targetLockedRotation = _getRotationConstantFromDeviceOrientation( orientation, ); - lockedCaptureOrientation = targetLockedRotation; + _lockedCaptureOrientation = targetLockedRotation; // Update UseCases to use target device orientation. await imageCapture!.setTargetRotation(targetLockedRotation); @@ -608,7 +605,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; + _lockedCaptureOrientation = null; } /// Sets the exposure point for automatically determining the exposure values for @@ -1151,7 +1148,7 @@ class AndroidCameraCameraX extends CameraPlatform { // Recreate ImageCapture with the requested JPEG quality. // Preserve locked orientation if set, otherwise use default display rotation. final int targetRotation = - lockedCaptureOrientation ?? + _lockedCaptureOrientation ?? await deviceOrientationManager.getDefaultDisplayRotation(); imageCapture = ImageCapture( resolutionSelector: _presetResolutionSelector, 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 5f68b48b3802..c687c7aba842 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 @@ -3973,7 +3973,11 @@ void main() { 'setJpegImageQuality preserves locked target rotation when recreating ImageCapture', () async { final camera = AndroidCameraCameraX(); + final mockProcessCameraProvider = MockProcessCameraProvider(); final mockDeviceOrientationManager = MockDeviceOrientationManager(); + final mockImageCapture = MockImageCapture(); + final mockImageAnalysis = MockImageAnalysis(); + final mockVideoCapture = MockVideoCapture(); final mockNewImageCapture = MockImageCapture(); const int lockedTargetRotation = Surface.rotation270; const jpegQuality = 64; @@ -3981,7 +3985,19 @@ void main() { int? actualTargetRotation; int? actualJpegQuality; - camera.lockedCaptureOrientation = lockedTargetRotation; + camera.processCameraProvider = mockProcessCameraProvider; + camera.imageCapture = mockImageCapture; + camera.imageAnalysis = mockImageAnalysis; + camera.videoCapture = mockVideoCapture; + + await camera.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeRight, + ); + + when( + mockProcessCameraProvider.isBound(mockImageCapture), + ).thenAnswer((_) async => true); PigeonOverrides.deviceOrientationManager_new = ({ @@ -4014,6 +4030,93 @@ void main() { }, ); + test( + 'setJpegImageQuality followed by takePicture binds the new ImageCapture to the ProcessCameraProvider', + () async { + final camera = AndroidCameraCameraX(); + final mockProcessCameraProvider = MockProcessCameraProvider(); + final mockDeviceOrientationManager = MockDeviceOrientationManager(); + final mockCamera = MockCamera(); + final mockCameraInfo = MockCameraInfo(); + final mockOldImageCapture = MockImageCapture(); + final mockNewImageCapture = MockImageCapture(); + const jpegQuality = 73; + const cameraId = 9; + const int defaultTargetRotation = Surface.rotation90; + const testPicturePath = 'test/absolute/path/to/picture'; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.imageCapture = mockOldImageCapture; + camera.cameraSelector = MockCameraSelector(); + camera.captureOrientationLocked = true; + + 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, + }) { + return mockNewImageCapture; + }; + + GenericsPigeonOverrides.observerNew = + ({required void Function(Observer, T) onChanged}) { + return Observer.detached(onChanged: onChanged); + }; + PigeonOverrides.systemServicesManager_new = + ({ + required void Function(SystemServicesManager, String) onCameraError, + }) { + return MockSystemServicesManager(); + }; + + when( + mockProcessCameraProvider.isBound(mockOldImageCapture), + ).thenAnswer((_) async => true); + when( + mockProcessCameraProvider.isBound(mockNewImageCapture), + ).thenAnswer((_) async => false); + when( + mockProcessCameraProvider.bindToLifecycle( + camera.cameraSelector, + [mockNewImageCapture], + ), + ).thenAnswer((_) async => mockCamera); + when(mockCamera.getCameraInfo()).thenAnswer((_) async => mockCameraInfo); + when( + mockCameraInfo.getCameraState(), + ).thenAnswer((_) async => MockLiveCameraState()); + when( + mockNewImageCapture.takePicture(argThat(isA())), + ).thenAnswer((_) async => testPicturePath); + + await camera.setJpegImageQuality(cameraId, jpegQuality); + final XFile imageFile = await camera.takePicture(cameraId); + + verify( + mockProcessCameraProvider.unbind([mockOldImageCapture]), + ).called(1); + verify( + mockProcessCameraProvider.bindToLifecycle( + camera.cameraSelector, + [mockNewImageCapture], + ), + ).called(1); + expect(imageFile.path, equals(testPicturePath)); + }, + ); + test( 'takePicture turns non-torch flash mode off when torch mode enabled', () async {