From 8294234b0fae396ea3443e937b56cdcd8aa080c4 Mon Sep 17 00:00:00 2001 From: ruicraveiro Date: Fri, 12 Jul 2024 11:59:19 +0100 Subject: [PATCH] Adds video stabilization to camera - Adds support for video stabilization to camera: Adds getVideoStabilizationSupportedModes() and setVideoStabilizationMode() methods to CameraController. - Adds support for video stabilization to camera_avfoundation - Adds support for video stabilization to camera_android_camerax - Adds support for video stabilization to camera_platform_interface: - Adds getVideoStabilizationSupportedModes() and setVideoStabilizationMode() methods to CameraPlatform. - Adds VideoStabilizationMode enum to represent an abstraction of the available video stabilization modes, meant for Android and iOS, mapped as follows: /// Video stabilization is disabled. off, /// Standard video stabilization is enabled. /// Maps to CONTROL_VIDEO_STABILIZATION_MODE_ON on Android /// and to AVCaptureVideoStabilizationModeStandard on iOS. standard, /// Cinematic video stabilization is enabled. /// Maps to CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION on Android /// and to AVCaptureVideoStabilizationModeCinematic on iOS. cinematic, /// Extended cinematic video stabilization is enabled. /// The same as cinematic on Android, so maps to CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION. /// Maps to AVCaptureVideoStabilizationModeCinematicExtended on iOS. cinematicExtended, --- packages/camera/camera/AUTHORS | 1 + packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/lib/camera.dart | 1 + .../camera/lib/src/camera_controller.dart | 43 ++++ packages/camera/camera/pubspec.yaml | 8 +- .../camera/test/camera_preview_test.dart | 7 + packages/camera/camera/test/camera_test.dart | 142 ++++++++++++ .../camera/camera/test/camera_value_test.dart | 8 +- packages/camera/camera_android/CHANGELOG.md | 4 + packages/camera/camera_android/pubspec.yaml | 4 +- .../camera/camera_android_camerax/AUTHORS | 1 + .../camera_android_camerax/CHANGELOG.md | 4 + .../camerax/Camera2CameraInfoHostApiImpl.java | 27 ++- .../CaptureRequestOptionsHostApiImpl.java | 7 + .../camerax/GeneratedCameraXLibrary.java | 33 ++- .../camerax/Camera2CameraInfoTest.java | 51 +++++ .../camerax/CaptureRequestOptionsTest.java | 8 + .../lib/src/android_camera_camerax.dart | 80 +++++++ .../lib/src/camera2_camera_info.dart | 17 ++ .../lib/src/camera_info.dart | 11 + .../lib/src/camerax_library.g.dart | 30 +++ .../lib/src/capture_request_options.dart | 6 + .../pigeons/camerax_library.dart | 3 + .../camera_android_camerax/pubspec.yaml | 4 +- .../test/android_camera_camerax_test.dart | 211 ++++++++++++++++++ .../android_camera_camerax_test.mocks.dart | 11 + .../test/camera2_camera_info_test.dart | 70 ++++++ .../test/camera2_camera_info_test.mocks.dart | 10 + .../test/test_camerax_library.g.dart | 26 +++ packages/camera/camera_avfoundation/AUTHORS | 1 + .../camera/camera_avfoundation/CHANGELOG.md | 4 + .../camera_avfoundation/CameraPlugin.m | 23 ++ .../Sources/camera_avfoundation/FLTCam.m | 58 +++++ .../include/camera_avfoundation/FLTCam.h | 3 + .../include/camera_avfoundation/messages.g.h | 24 +- .../Sources/camera_avfoundation/messages.g.m | 73 +++++- .../lib/src/avfoundation_camera.dart | 49 ++++ .../lib/src/messages.g.dart | 72 +++++- .../camera_avfoundation/pigeons/messages.dart | 19 +- .../camera/camera_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_camera_test.dart | 97 ++++++++ .../test/avfoundation_camera_test.mocks.dart | 24 ++ .../camera/camera_platform_interface/AUTHORS | 1 + .../camera_platform_interface/CHANGELOG.md | 5 + .../method_channel/method_channel_camera.dart | 38 ++++ .../platform_interface/camera_platform.dart | 24 ++ .../lib/src/types/types.dart | 1 + .../src/types/video_stabilization_mode.dart | 55 +++++ .../camera_platform_interface/pubspec.yaml | 2 +- .../method_channel_camera_test.dart | 143 ++++++++++++ .../types/video_stabilization_mode_test.dart | 46 ++++ packages/camera/camera_web/CHANGELOG.md | 4 + packages/camera/camera_web/pubspec.yaml | 4 +- packages/camera/camera_windows/CHANGELOG.md | 4 + packages/camera/camera_windows/pubspec.yaml | 4 +- 55 files changed, 1584 insertions(+), 30 deletions(-) create mode 100644 packages/camera/camera_platform_interface/lib/src/types/video_stabilization_mode.dart create mode 100644 packages/camera/camera_platform_interface/test/types/video_stabilization_mode_test.dart diff --git a/packages/camera/camera/AUTHORS b/packages/camera/camera/AUTHORS index 493a0b4ef9c2..605414ab7dcf 100644 --- a/packages/camera/camera/AUTHORS +++ b/packages/camera/camera/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Rui Craveiro diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 53a6c4ca1ad8..d94b79cffece 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.12.0 + +* Adds support for video stabilization. + ## 0.11.0+1 * Updates minimum supported SDK version to Flutter 3.16/Dart 3.2. diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 507d6c7cded3..3778c870071d 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -12,6 +12,7 @@ export 'package:camera_platform_interface/camera_platform_interface.dart' FocusMode, ImageFormatGroup, ResolutionPreset, + VideoStabilizationMode, XFile; export 'src/camera_controller.dart'; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 5f90a0a740a9..7e544323511a 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -54,6 +54,7 @@ class CameraValue { this.recordingOrientation, this.isPreviewPaused = false, this.previewPauseOrientation, + this.videoStabilizationMode = VideoStabilizationMode.off, }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. @@ -72,6 +73,7 @@ class CameraValue { deviceOrientation: DeviceOrientation.portraitUp, isPreviewPaused: false, description: description, + videoStabilizationMode: VideoStabilizationMode.off, ); /// True after [CameraController.initialize] has completed successfully. @@ -148,6 +150,9 @@ class CameraValue { /// The properties of the camera device controlled by this controller. final CameraDescription description; + /// The video stabilization mode in + final VideoStabilizationMode videoStabilizationMode; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -171,6 +176,7 @@ class CameraValue { bool? isPreviewPaused, CameraDescription? description, Optional? previewPauseOrientation, + VideoStabilizationMode? videoStabilizationMode, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -198,6 +204,8 @@ class CameraValue { previewPauseOrientation: previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, + videoStabilizationMode: + videoStabilizationMode ?? this.videoStabilizationMode, ); } @@ -219,6 +227,7 @@ class CameraValue { 'recordingOrientation: $recordingOrientation, ' 'isPreviewPaused: $isPreviewPaused, ' 'previewPausedOrientation: $previewPauseOrientation, ' + 'videoStabilizationMode: $videoStabilizationMode, ' 'description: $description)'; } } @@ -689,6 +698,40 @@ class CameraController extends ValueNotifier { } } + /// Set the video stabilization mode for the selected camera. + /// + /// On Android (when using camera_android_camerax) and on iOS + /// the supplied [mode] value should be a mode in the list returned + /// by [getVideoStabilizationSupportedModes]. + /// + /// Throws a [CameraException] when a not supported video stabilization + /// mode is supplied. + Future setVideoStabilizationMode(VideoStabilizationMode mode) async { + _throwIfNotInitialized('setVideoStabilizationMode'); + try { + await CameraPlatform.instance.setVideoStabilizationMode(_cameraId, mode); + value = value.copyWith(videoStabilizationMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets a list of video stabilization modes that are supported for the selected camera. + /// + /// Will return the list of supported video stabilization modes + /// on Android (when using camera_android_camerax package) and + /// on iOS. Will return an empty list on all other platforms. + Future> + getVideoStabilizationSupportedModes() { + _throwIfNotInitialized('isVideoStabilizationModeSupported'); + try { + return CameraPlatform.instance + .getVideoStabilizationSupportedModes(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Sets the flash mode for taking pictures. Future setFlashMode(FlashMode mode) async { try { diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f00a7e798f01..7f2ec933fe97 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.11.0+1 +version: 0.12.0 environment: sdk: ^3.2.3 @@ -21,9 +21,9 @@ flutter: default_package: camera_web dependencies: - camera_android_camerax: ^0.6.5 - camera_avfoundation: ^0.9.15 - camera_platform_interface: ^2.6.0 + camera_android_camerax: ^0.7.0 + camera_avfoundation: ^0.10.0 + camera_platform_interface: ^2.9.0 camera_web: ^0.3.3 flutter: sdk: flutter diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 4b5a2a5b5163..74d6c6a1fddd 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -132,6 +132,13 @@ class FakeController extends ValueNotifier @override CameraDescription get description => value.description; + + @override + Future setVideoStabilizationMode(VideoStabilizationMode mode) async {} + + @override + Future> + getVideoStabilizationSupportedModes() async => []; } void main() { diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 0c6a319397e0..1f268b83e137 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1190,6 +1190,131 @@ void main() { .called(4); }); + test('getVideoStabilizationSupportedModes() returns empty list', () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance + .getVideoStabilizationSupportedModes(mockInitializeCamera)) + .thenAnswer((_) async => []); + + // act + final Iterable modes = + await cameraController.getVideoStabilizationSupportedModes(); + + // assert + expect(modes, []); + }); + + test('getVideoStabilizationSupportedModes() returns off', () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance + .getVideoStabilizationSupportedModes(mockInitializeCamera)) + .thenAnswer((_) async => + [VideoStabilizationMode.off]); + + // act + final Iterable modes = + await cameraController.getVideoStabilizationSupportedModes(); + + // assert + expect(modes, [VideoStabilizationMode.off]); + }); + + test('getVideoStabilizationSupportedModes() returns all modes', () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance + .getVideoStabilizationSupportedModes(mockInitializeCamera)) + .thenAnswer((_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.standard, + VideoStabilizationMode.cinematic, + VideoStabilizationMode.cinematicExtended, + ]); + + // act + final Iterable modes = + await cameraController.getVideoStabilizationSupportedModes(); + + // assert + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.standard, + VideoStabilizationMode.cinematic, + VideoStabilizationMode.cinematicExtended, + ]); + }); + + test('setVideoStabilizationMode() calls $CameraPlatform', () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController + .setVideoStabilizationMode(VideoStabilizationMode.off); + + verify(CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, VideoStabilizationMode.off)) + .called(1); + }); + + test( + 'setVideoStabilizationMode() throws $CameraException on $PlatformException', + () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.cinematicExtended)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + ), + ); + + expect( + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.cinematicExtended), + throwsA(isA().having( + (CameraException error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + test('pausePreview() calls $CameraPlatform', () async { final CameraController cameraController = CameraController( const CameraDescription( @@ -1602,6 +1727,23 @@ class MockCameraPlatform extends Mock Invocation.method(#setExposureOffset, [cameraId, offset]), returnValue: Future.value(1.0), ) as Future; + + @override + Future> getVideoStabilizationSupportedModes( + int cameraId) { + return super.noSuchMethod( + Invocation.method( + #getVideoStabilizationSupportedModes, [cameraId]), + returnValue: Future>.value( + []), + ) as Future>; + } + + @override + Future setVideoStabilizationMode( + int cameraId, VideoStabilizationMode mode) async => + super.noSuchMethod(Invocation.method( + #setVideoStabilizationMode, [cameraId, mode])); } class MockCameraDescription extends CameraDescription { diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index dbb1ddcbf789..835dd59b55b4 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -28,6 +28,7 @@ void main() { focusPointSupported: true, previewPauseOrientation: DeviceOrientation.portraitUp, description: FakeController.fakeDescription, + videoStabilizationMode: VideoStabilizationMode.cinematic, ); expect(cameraValue, isA()); @@ -47,6 +48,8 @@ void main() { expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); expect(cameraValue.isPreviewPaused, false); expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp); + expect( + cameraValue.videoStabilizationMode, VideoStabilizationMode.cinematic); }); test('Can be created as uninitialized', () { @@ -70,6 +73,7 @@ void main() { expect(cameraValue.recordingOrientation, null); expect(cameraValue.isPreviewPaused, isFalse); expect(cameraValue.previewPauseOrientation, null); + expect(cameraValue.videoStabilizationMode, VideoStabilizationMode.off); }); test('Can be copied with isInitialized', () { @@ -94,6 +98,7 @@ void main() { expect(cameraValue.recordingOrientation, null); expect(cameraValue.isPreviewPaused, isFalse); expect(cameraValue.previewPauseOrientation, null); + expect(cameraValue.videoStabilizationMode, VideoStabilizationMode.off); }); test('Has aspectRatio after setting size', () { @@ -144,10 +149,11 @@ void main() { isPreviewPaused: true, previewPauseOrientation: DeviceOrientation.portraitUp, description: FakeController.fakeDescription, + videoStabilizationMode: VideoStabilizationMode.cinematicExtended, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp, description: CameraDescription(, CameraLensDirection.back, 0))'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp, videoStabilizationMode: VideoStabilizationMode.cinematicExtended, description: CameraDescription(, CameraLensDirection.back, 0))'); }); }); } diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index fa7a742c1188..050d4d467446 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.9+9 + +* Updates camera_platform_interface lib to 2.9.0. + ## 0.10.9+8 * Removes unused code related to `maxVideoDuration`. diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index 1f157f0aefe5..f29bdb21d8a7 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.9+8 +version: 0.10.9+9 environment: sdk: ^3.4.0 @@ -19,7 +19,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.9.0 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera_android_camerax/AUTHORS b/packages/camera/camera_android_camerax/AUTHORS index 557dff97933b..7fc8bd920ead 100644 --- a/packages/camera/camera_android_camerax/AUTHORS +++ b/packages/camera/camera_android_camerax/AUTHORS @@ -4,3 +4,4 @@ # Name/Organization Google Inc. +Rui Craveiro \ No newline at end of file diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 9e8e4fc17c5d..3b3d24ece904 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0 + +* Adds video stabilization. + ## 0.6.7+1 * Updates README to remove references to `maxVideoDuration`, as it was never diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java index 76606b43efc0..917635fd9c64 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/Camera2CameraInfoHostApiImpl.java @@ -4,7 +4,6 @@ package io.flutter.plugins.camerax; -import android.content.Context; import android.hardware.camera2.CameraCharacteristics; import androidx.annotation.NonNull; import androidx.annotation.OptIn; @@ -14,6 +13,8 @@ import androidx.camera.core.CameraInfo; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Camera2CameraInfoHostApi; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -53,6 +54,15 @@ public Long getSensorOrientation(@NonNull Camera2CameraInfo camera2CameraInfo) { return Long.valueOf( camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION)); } + + @NonNull + @OptIn(markerClass = ExperimentalCamera2Interop.class) + public int[] getAvailableVideoStabilizationModes(@NonNull Camera2CameraInfo camera2CameraInfo) { + int[] modes = + camera2CameraInfo.getCameraCharacteristic( + CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES); + return modes == null ? new int[] {} : modes; + } } /** @@ -60,7 +70,6 @@ public Long getSensorOrientation(@NonNull Camera2CameraInfo camera2CameraInfo) { * * @param binaryMessenger used to communicate with Dart over asynchronous messages * @param instanceManager maintains instances stored to communicate with attached Dart objects - * @param context {@link Context} used to retrieve {@code Executor} */ public Camera2CameraInfoHostApiImpl( @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { @@ -86,6 +95,7 @@ public Camera2CameraInfoHostApiImpl( @Override @NonNull + @OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class) public Long createFrom(@NonNull Long cameraInfoIdentifier) { final CameraInfo cameraInfo = Objects.requireNonNull(instanceManager.getInstance(cameraInfoIdentifier)); @@ -115,6 +125,19 @@ public Long getSensorOrientation(@NonNull Long identifier) { return proxy.getSensorOrientation(getCamera2CameraInfoInstance(identifier)); } + @NonNull + @Override + public List getAvailableVideoStabilizationModes(@NonNull Long identifier) { + + int[] lst = proxy.getAvailableVideoStabilizationModes(getCamera2CameraInfoInstance(identifier)); + List ret = new ArrayList(lst.length); + for (int i : lst) { + ret.add((long) i); + } + return ret; + } + + @OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class) private Camera2CameraInfo getCamera2CameraInfoInstance(@NonNull Long identifier) { return Objects.requireNonNull(instanceManager.getInstance(identifier)); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java index f76dd5422e72..af05e045cce3 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsHostApiImpl.java @@ -51,6 +51,10 @@ public static class CaptureRequestOptionsProxy { builder.setCaptureRequestOption( (CaptureRequest.Key) optionKey, (Boolean) optionValue); break; + case CONTROL_VIDEO_STABILIZATION_MODE: + builder.setCaptureRequestOption( + (CaptureRequest.Key) optionKey, (int) optionValue); + break; default: throw new IllegalArgumentException( "The capture request key " @@ -69,6 +73,9 @@ private CaptureRequest.Key getCaptureRequestKey( case CONTROL_AE_LOCK: key = CaptureRequest.CONTROL_AE_LOCK; break; + case CONTROL_VIDEO_STABILIZATION_MODE: + key = CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE; + break; default: throw new IllegalArgumentException( "The capture request key is not currently supported by the plugin."); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 7adf02595ad9..ff0bd8290d8c 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -181,7 +181,8 @@ private VideoRecordEvent(final int index) { * Camera2. */ public enum CaptureRequestKeySupportedType { - CONTROL_AE_LOCK(0); + CONTROL_AE_LOCK(0), + CONTROL_VIDEO_STABILIZATION_MODE(1); final int index; @@ -4444,6 +4445,9 @@ public interface Camera2CameraInfoHostApi { @NonNull Long getSensorOrientation(@NonNull Long identifier); + @NonNull + List getAvailableVideoStabilizationModes(@NonNull Long identifier); + /** The codec used by Camera2CameraInfoHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -4563,6 +4567,33 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.Camera2CameraInfoHostApi.getAvailableVideoStabilizationModes", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + List output = + api.getAvailableVideoStabilizationModes( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java index a5ab10ff79bd..4d6aac853c30 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java @@ -4,6 +4,10 @@ package io.flutter.plugins.camerax; +import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF; +import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_ON; +import static android.hardware.camera2.CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -17,6 +21,7 @@ import androidx.camera.camera2.interop.Camera2CameraInfo; import androidx.camera.core.CameraInfo; import io.flutter.plugin.common.BinaryMessenger; +import java.util.List; import java.util.Objects; import org.junit.After; import org.junit.Before; @@ -86,6 +91,52 @@ public void getSupportedHardwareLevel_returnsExpectedLevel() { hostApi.getSupportedHardwareLevel(camera2CameraInfoIdentifier).intValue()); } + @Test + public void getAvailableVideoStabilizationModes_returnsNone() { + final Camera2CameraInfoHostApiImpl hostApi = + new Camera2CameraInfoHostApiImpl(mock(BinaryMessenger.class), testInstanceManager); + final long camera2CameraInfoIdentifier = 3; + + testInstanceManager.addDartCreatedInstance(mockCamera2CameraInfo, camera2CameraInfoIdentifier); + when(mockCamera2CameraInfo.getCameraCharacteristic( + CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES)) + .thenReturn(new int[] {}); + + List returned = hostApi.getAvailableVideoStabilizationModes(camera2CameraInfoIdentifier); + assertEquals(0, returned.size()); + } + + @Test + public void getAvailableVideoStabilizationModes_returnsAll() { + // arrange + final Camera2CameraInfoHostApiImpl hostApi = + new Camera2CameraInfoHostApiImpl(mock(BinaryMessenger.class), testInstanceManager); + final long camera2CameraInfoIdentifier = 3; + + int[] expected = + new int[] { + CONTROL_VIDEO_STABILIZATION_MODE_OFF, + CONTROL_VIDEO_STABILIZATION_MODE_ON, + CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION, + }; + + testInstanceManager.addDartCreatedInstance(mockCamera2CameraInfo, camera2CameraInfoIdentifier); + when(mockCamera2CameraInfo.getCameraCharacteristic( + CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES)) + .thenReturn(expected); + + // act + int[] returned = + hostApi + .getAvailableVideoStabilizationModes(camera2CameraInfoIdentifier) + .stream() + .mapToInt(value -> value.intValue()) + .toArray(); + + // assert + assertArrayEquals(expected, returned); + } + @Test public void getCameraId_returnsExpectedId() { final Camera2CameraInfoHostApiImpl hostApi = diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CaptureRequestOptionsTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CaptureRequestOptionsTest.java index 9c3329978d46..912207470fa8 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CaptureRequestOptionsTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CaptureRequestOptionsTest.java @@ -55,6 +55,7 @@ public void create_buildsExpectedCaptureKeyRequestOptionsWhenOptionsNonNull() { new HashMap() { { put(0L, false); + put(1L, 1); } }; @@ -71,6 +72,13 @@ public void create_buildsExpectedCaptureKeyRequestOptionsWhenOptionsNonNull() { .setCaptureRequestOption( eq(CaptureRequest.CONTROL_AE_LOCK), eq((Boolean) testValueForSupportedType)); break; + case CONTROL_VIDEO_STABILIZATION_MODE: + verify(mockBuilder) + .setCaptureRequestOption( + eq(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE), + eq((Integer) testValueForSupportedType)); + break; + default: throw new IllegalArgumentException( "The capture request key is not currently supported by the plugin."); 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 1d068eb24f1f..ecd192c6a769 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 @@ -816,6 +816,86 @@ class AndroidCameraCameraX extends CameraPlatform { await cameraControl.setZoomRatio(zoom); } + @override + Future> getVideoStabilizationSupportedModes( + int cameraId) async { + final List modeIndexes = + await _getAvailableVideoStabilizationModeIndexes(); + + final List list = []; + for (final int ix in modeIndexes) { + final VideoStabilizationMode? mode = switch (ix) { + 0 => VideoStabilizationMode.off, + 1 => VideoStabilizationMode.standard, + 2 => VideoStabilizationMode.cinematic, + _ => null, + }; + if (mode != null) { + list.add(mode); + } + } + return list; + } + + /// Set the video stabilization mode for the selected camera. + @override + Future setVideoStabilizationMode( + int cameraId, VideoStabilizationMode mode) async { + final int? controlMode = _getControlVideoStabilizationMode(mode); + + final List availableModes = + await _getAvailableVideoStabilizationModeIndexes(); + if (controlMode == null || !availableModes.contains(controlMode)) { + throw CameraException('VIDEO_STABILIIZATION_ERROR', + 'Unavailable video stabilization mode.'); + } + + final CaptureRequestOptions captureRequestOptions = proxy + .createCaptureRequestOptions(<( + CaptureRequestKeySupportedType, + Object? + )>[ + ( + CaptureRequestKeySupportedType.controlVideoStabilizationMode, + controlMode + ) + ]); + + final Camera2CameraControl camera2Control = + proxy.getCamera2CameraControl(cameraControl); + await camera2Control.addCaptureRequestOptions(captureRequestOptions); + } + + /// Gets from Camera2 API the available video stabilization + /// modes. + /// See https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES + Future> _getAvailableVideoStabilizationModeIndexes() async { + final CameraInfo? camInfo = cameraInfo; + if (camInfo == null) { + return []; + } + final Camera2CameraInfo cam2Info = + await proxy.getCamera2CameraInfo(camInfo); + + final List modeIndexes = + await cam2Info.getAvailableVideoStabilizationModes(); + + return modeIndexes; + } + + int? _getControlVideoStabilizationMode(VideoStabilizationMode mode) { + final int? controlMode = switch (mode) { + // https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_OFF + VideoStabilizationMode.off => 0, + // https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_ON + VideoStabilizationMode.standard => 1, + // https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION + VideoStabilizationMode.cinematic => 2, + VideoStabilizationMode.cinematicExtended => null, + }; + return controlMode; + } + /// The ui orientation changed. @override Stream onDeviceOrientationChanged() { diff --git a/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart index 88b44238f3b9..6f8cb2c7d389 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera2_camera_info.dart @@ -48,6 +48,14 @@ class Camera2CameraInfo extends JavaObject { Future getSupportedHardwareLevel() => _api.getSupportedHardwareLevelFromInstance(this); + /// Retrieves the value of `CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES` + /// for the device to which this instance pertains to. + /// + /// See https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES + /// for more information. + Future> getAvailableVideoStabilizationModes() => + _api.getAvailableVideoStabilizationModesFromInstance(this); + /// Gets the camera ID. /// /// The ID may change based on the internal configuration of the camera to which @@ -91,6 +99,15 @@ class _Camera2CameraInfoHostApiImpl extends Camera2CameraInfoHostApi { return getSupportedHardwareLevel(identifier!); } + Future> getAvailableVideoStabilizationModesFromInstance( + Camera2CameraInfo instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + final List modes = + await getAvailableVideoStabilizationModes(identifier!); + + return modes.where((int? e) => e != null).map((int? e) => e!).toList(); + } + Future getCameraIdFromInstance(Camera2CameraInfo instance) { final int? identifier = instanceManager.getIdentifier(instance); return getCameraId(identifier!); diff --git a/packages/camera/camera_android_camerax/lib/src/camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera_info.dart index 6c0a62f9fd5d..ea5d51f37b24 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_info.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_info.dart @@ -100,6 +100,17 @@ class _CameraInfoHostApiImpl extends CameraInfoHostApi { return instanceManager.getInstanceWithWeakReference>( zoomStateIdentifier)!; } + + // /// Returns a value indicating whether the given video + // /// stabilization mode is supported for [CameraInfo] instance. + // Future isVideoStabilizationModeSupportedFromInstance( + // CameraInfo instance, VideoStabilizationMode mode) async { + // final int identifier = instanceManager.getIdentifier(instance)!; + + // final bool? result = + // await isVideoStabilizationModeSupported(identifier, mode.index); + // return result; + // } } /// Flutter API implementation of [CameraInfo]. 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 a9461eaaae03..7919dc4e1e10 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 @@ -96,6 +96,7 @@ enum VideoRecordEvent { /// interoperability with Camera2. enum CaptureRequestKeySupportedType { controlAeLock, + controlVideoStabilizationMode, } class ResolutionInfo { @@ -3657,6 +3658,35 @@ class Camera2CameraInfoHostApi { return (replyList[0] as int?)!; } } + + Future> getAvailableVideoStabilizationModes( + int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.Camera2CameraInfoHostApi.getAvailableVideoStabilizationModes', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } } abstract class Camera2CameraInfoFlutterApi { diff --git a/packages/camera/camera_android_camerax/lib/src/capture_request_options.dart b/packages/camera/camera_android_camerax/lib/src/capture_request_options.dart index 777d4a43370f..00a13c28cccb 100644 --- a/packages/camera/camera_android_camerax/lib/src/capture_request_options.dart +++ b/packages/camera/camera_android_camerax/lib/src/capture_request_options.dart @@ -112,6 +112,12 @@ class _CaptureRequestOptionsHostApiImpl extends CaptureRequestOptionsHostApi { throw ArgumentError( 'A controlAeLock value must be specified as a bool, but a $valueRuntimeType was specified.'); } + case CaptureRequestKeySupportedType.controlVideoStabilizationMode: + if (valueRuntimeType != int) { + throw ArgumentError( + 'A controlVideoStabilizationMode value must be specified as an int, but a $valueRuntimeType was specified.'); + } + // This ignore statement is safe beause this error will be useful when // a new CaptureRequestKeySupportedType is being added, but the logic in // this method has not yet been updated. diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 949830db3e83..7107a4f9aca0 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -172,6 +172,7 @@ class MeteringPointInfo { /// interoperability with Camera2. enum CaptureRequestKeySupportedType { controlAeLock, + controlVideoStabilizationMode, } @HostApi(dartHostTestHandler: 'TestInstanceManagerHostApi') @@ -568,6 +569,8 @@ abstract class Camera2CameraInfoHostApi { String getCameraId(int identifier); int getSensorOrientation(int identifier); + + List getAvailableVideoStabilizationModes(int identifier); } @FlutterApi() diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index da774124a3ac..b2907329c765 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.7+1 +version: 0.7.0 environment: sdk: ^3.4.0 @@ -19,7 +19,7 @@ flutter: dependencies: async: ^2.5.0 - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.9.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 1f9ea762ce23..d03b05cd2373 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 @@ -2317,6 +2317,217 @@ void main() { verify(mockCameraControl.setZoomRatio(zoomRatio)); }); + test('setVideoStabilizationMode sets mode expected', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 78; + + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraControl mockCamera2CameraControl = + MockCamera2CameraControl(); + + // Set directly for test versus calling createCamera. + camera.camera = MockCamera(); + camera.cameraControl = mockCameraControl; + camera.cameraInfo = mockCameraInfo; + + when(mockCamera2CameraInfo.getAvailableVideoStabilizationModes()) + .thenAnswer( + (_) async { + return [0]; + }, + ); + + camera.proxy = CameraXProxy( + getCamera2CameraControl: (CameraControl cameraControl) => + cameraControl == mockCameraControl + ? mockCamera2CameraControl + : Camera2CameraControl.detached(cameraControl: cameraControl), + getCamera2CameraInfo: (CameraInfo cameraInfo) async { + return cameraInfo == mockCameraInfo + ? mockCamera2CameraInfo + : Camera2CameraInfo.detached(); + }, + createCaptureRequestOptions: + (List<(CaptureRequestKeySupportedType, Object?)> options) => + CaptureRequestOptions.detached(requestedOptions: options), + ); + + // Test off. + await camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.off); + + final VerificationResult verificationResult = + verify(mockCamera2CameraControl.addCaptureRequestOptions(captureAny)); + final CaptureRequestOptions capturedCaptureRequestOptions = + verificationResult.captured.single as CaptureRequestOptions; + final List<(CaptureRequestKeySupportedType, Object?)> requestedOptions = + capturedCaptureRequestOptions.requestedOptions; + expect(requestedOptions.length, equals(1)); + expect(requestedOptions.first.$1, + equals(CaptureRequestKeySupportedType.controlVideoStabilizationMode)); + expect(requestedOptions.first.$2, equals(0)); + }); + + test( + 'setVideoStabilizationMode throws CameraException when mode not available', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 78; + + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + final MockCameraControl mockCameraControl = MockCameraControl(); + + // Set directly for test versus calling createCamera. + camera.camera = MockCamera(); + camera.cameraInfo = mockCameraInfo; + + when(mockCamera2CameraInfo.getAvailableVideoStabilizationModes()) + .thenAnswer( + (_) async { + return [0]; + }, + ); + + camera.cameraControl = mockCameraControl; + + camera.proxy = CameraXProxy( + getCamera2CameraInfo: (CameraInfo cameraInfo) async { + return cameraInfo == mockCameraInfo + ? mockCamera2CameraInfo + : Camera2CameraInfo.detached(); + }, + ); + + expect( + () => camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.standard), + throwsA(isA())); + }); + + test('setVideoStabilizationMode throws CameraException when mode not mapped', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 78; + + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + final MockCameraControl mockCameraControl = MockCameraControl(); + + // Set directly for test versus calling createCamera. + camera.camera = MockCamera(); + camera.cameraInfo = mockCameraInfo; + + when(mockCamera2CameraInfo.getAvailableVideoStabilizationModes()) + .thenAnswer( + (_) async { + return [0, 1, 2]; + }, + ); + + camera.cameraControl = mockCameraControl; + + camera.proxy = CameraXProxy( + getCamera2CameraInfo: (CameraInfo cameraInfo) async { + return cameraInfo == mockCameraInfo + ? mockCamera2CameraInfo + : Camera2CameraInfo.detached(); + }, + ); + + expect( + () => camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.cinematicExtended), + throwsA(isA())); + }); + + test('getVideoStabilizationMode returns no available mode', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 78; + + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + final MockCameraControl mockCameraControl = MockCameraControl(); + + // Set directly for test versus calling createCamera. + camera.camera = MockCamera(); + camera.cameraInfo = mockCameraInfo; + + when(mockCamera2CameraInfo.getAvailableVideoStabilizationModes()) + .thenAnswer( + (_) async { + return []; + }, + ); + + camera.cameraControl = mockCameraControl; + + // Tell plugin to create detached Camera2CameraControl and + // CaptureRequestOptions instances for testing. + camera.proxy = CameraXProxy( + getCamera2CameraInfo: (CameraInfo cameraInfo) async { + return cameraInfo == mockCameraInfo + ? mockCamera2CameraInfo + : Camera2CameraInfo.detached(); + }, + ); + + final Iterable modes = + await camera.getVideoStabilizationSupportedModes(cameraId); + + expect(modes, isEmpty); + }); + + test('getVideoStabilizationMode returns all available modes', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 78; + + final MockCameraInfo mockCameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + final MockCameraControl mockCameraControl = MockCameraControl(); + + // Set directly for test versus calling createCamera. + camera.camera = MockCamera(); + camera.cameraInfo = mockCameraInfo; + + when(mockCamera2CameraInfo.getAvailableVideoStabilizationModes()) + .thenAnswer( + (_) async { + return [0, 1, 2]; + }, + ); + + camera.cameraControl = mockCameraControl; + + // Tell plugin to create detached Camera2CameraControl and + // CaptureRequestOptions instances for testing. + camera.proxy = CameraXProxy( + getCamera2CameraInfo: (CameraInfo cameraInfo) async { + return cameraInfo == mockCameraInfo + ? mockCamera2CameraInfo + : Camera2CameraInfo.detached(); + }, + ); + + final Iterable modes = + await camera.getVideoStabilizationSupportedModes(cameraId); + + expect( + modes, + orderedEquals([ + VideoStabilizationMode.off, + VideoStabilizationMode.standard, + VideoStabilizationMode.cinematic, + ])); + }); + test( 'onStreamedFrameAvailable emits CameraImageData when picked up from CameraImageData stream controller', () async { diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index 316b12759692..36936c304132 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -707,6 +707,17 @@ class MockCamera2CameraInfo extends _i1.Mock implements _i26.Camera2CameraInfo { returnValueForMissingStub: _i17.Future.value(0), ) as _i17.Future); + @override + _i17.Future> getAvailableVideoStabilizationModes() => + (super.noSuchMethod( + Invocation.method( + #getAvailableVideoStabilizationModes, + [], + ), + returnValue: _i17.Future>.value([]), + returnValueForMissingStub: _i17.Future>.value([]), + ) as _i17.Future>); + @override _i17.Future getCameraId() => (super.noSuchMethod( Invocation.method( diff --git a/packages/camera/camera_android_camerax/test/camera2_camera_info_test.dart b/packages/camera/camera_android_camerax/test/camera2_camera_info_test.dart index 0766eee37e83..0d5c0224a81e 100644 --- a/packages/camera/camera_android_camerax/test/camera2_camera_info_test.dart +++ b/packages/camera/camera_android_camerax/test/camera2_camera_info_test.dart @@ -110,6 +110,76 @@ void main() { verify(mockApi.getSupportedHardwareLevel(camera2CameraInfoId)); }); + test( + 'getAvailableVideoStabilizationModes makes call to retrieve no available video stabilization modes', + () async { + // Arrange + final MockTestCamera2CameraInfoHostApi mockApi = + MockTestCamera2CameraInfoHostApi(); + TestCamera2CameraInfoHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final Camera2CameraInfo camera2CameraInfo = Camera2CameraInfo.detached( + instanceManager: instanceManager, + ); + const int camera2CameraInfoId = 9; + + instanceManager.addHostCreatedInstance( + camera2CameraInfo, + camera2CameraInfoId, + onCopy: (_) => Camera2CameraInfo.detached(), + ); + + const List expectedModes = []; + when(mockApi.getAvailableVideoStabilizationModes(camera2CameraInfoId)) + .thenReturn(expectedModes); + + // Act + final List returnedModes = + await camera2CameraInfo.getAvailableVideoStabilizationModes(); + + // Assert + expect(returnedModes, equals(expectedModes)); + verify(mockApi.getAvailableVideoStabilizationModes(camera2CameraInfoId)); + }); + + test( + 'getAvailableVideoStabilizationModes makes call to retrieve all available video stabilization modes', + () async { + // Arrange + final MockTestCamera2CameraInfoHostApi mockApi = + MockTestCamera2CameraInfoHostApi(); + TestCamera2CameraInfoHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final Camera2CameraInfo camera2CameraInfo = Camera2CameraInfo.detached( + instanceManager: instanceManager, + ); + const int camera2CameraInfoId = 9; + + instanceManager.addHostCreatedInstance( + camera2CameraInfo, + camera2CameraInfoId, + onCopy: (_) => Camera2CameraInfo.detached(), + ); + + const List expectedModes = [0, 1, 2]; + when(mockApi.getAvailableVideoStabilizationModes(camera2CameraInfoId)) + .thenReturn(expectedModes); + + // Act + final List returnedModes = + await camera2CameraInfo.getAvailableVideoStabilizationModes(); + + // Assert + expect(returnedModes, equals(expectedModes)); + verify(mockApi.getAvailableVideoStabilizationModes(camera2CameraInfoId)); + }); + test('getCameraId makes call to retrieve camera ID', () async { final MockTestCamera2CameraInfoHostApi mockApi = MockTestCamera2CameraInfoHostApi(); diff --git a/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart index 88b219f28cf1..97a5140d0f9b 100644 --- a/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera2_camera_info_test.mocks.dart @@ -99,6 +99,16 @@ class MockTestCamera2CameraInfoHostApi extends _i1.Mock ), returnValue: 0, ) as int); + + @override + List getAvailableVideoStabilizationModes(int? identifier) => + (super.noSuchMethod( + Invocation.method( + #getAvailableVideoStabilizationModes, + [identifier], + ), + returnValue: [], + ) as List); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 32235b878f39..6d7f7f1b94f6 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -2487,6 +2487,8 @@ abstract class TestCamera2CameraInfoHostApi { int getSensorOrientation(int identifier); + List getAvailableVideoStabilizationModes(int identifier); + static void setup(TestCamera2CameraInfoHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2579,5 +2581,29 @@ abstract class TestCamera2CameraInfoHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.Camera2CameraInfoHostApi.getAvailableVideoStabilizationModes', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.Camera2CameraInfoHostApi.getAvailableVideoStabilizationModes was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.Camera2CameraInfoHostApi.getAvailableVideoStabilizationModes was null, expected non-null int.'); + final List output = + api.getAvailableVideoStabilizationModes(arg_identifier!); + return [output]; + }); + } + } } } diff --git a/packages/camera/camera_avfoundation/AUTHORS b/packages/camera/camera_avfoundation/AUTHORS index 493a0b4ef9c2..1d9825e2e03a 100644 --- a/packages/camera/camera_avfoundation/AUTHORS +++ b/packages/camera/camera_avfoundation/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Rui Craveiro \ No newline at end of file diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 767f9a75e0a2..2d35928d2b89 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0 + +* Adds video stabilization. + ## 0.9.17 * Adds Swift Package Manager compatibility. diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m index 151883c71e91..eb942b56cb2a 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m @@ -420,6 +420,29 @@ - (void)disposeCamera:(NSInteger)cameraId }); } +- (void)setVideoStabilizationMode:(FCPPlatformVideoStabilizationMode)mode + completion:(nonnull void (^)(FlutterError *_Nullable))completion { + __weak typeof(self) weakSelf = self; + dispatch_async(self.captureSessionQueue, ^{ + [weakSelf.camera setVideoStabilizationMode:mode withCompletion:completion]; + }); +} + +- (void)isVideoStabilizationModeSupported:(FCPPlatformVideoStabilizationMode)mode + completion:(nonnull void (^)(NSNumber *_Nullable, + FlutterError *_Nullable))completion { + __weak typeof(self) weakSelf = self; + + dispatch_async(self.captureSessionQueue, ^{ + bool isSupported = [weakSelf.camera isVideoStabilizationModeSupported:mode]; + if (isSupported) { + completion(@YES, nil); + } else { + completion(@NO, nil); + } + }); +} + #pragma mark Private // This must be called on captureSessionQueue. It is extracted from diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m index 6cfe47b86fb0..9f54d6e7f5e4 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m @@ -1199,6 +1199,64 @@ - (void)setZoomLevel:(CGFloat)zoom withCompletion:(void (^)(FlutterError *_Nulla completion(nil); } +- (void)setVideoStabilizationMode:(FCPPlatformVideoStabilizationMode)mode + withCompletion:(void (^)(FlutterError *_Nullable))completion { + AVCaptureVideoStabilizationMode avMode = getAvCaptureVideoStabilizationMode(mode); + bool isSupported = [self isVideoStabilizationAvModeSupported:avMode]; + + if (!isSupported) { + completion([FlutterError errorWithCode:@"VIDEO_STABILIIZATION_ERROR" + message:@"Unavailable video stabilization mode." + details:nil]); + return; + } + + AVCaptureConnection *connection = [_captureVideoOutput connectionWithMediaType:AVMediaTypeVideo]; + + connection.preferredVideoStabilizationMode = avMode; + + completion(nil); +} + +- (bool)isVideoStabilizationModeSupported:(FCPPlatformVideoStabilizationMode)mode { + AVCaptureVideoStabilizationMode avMode = getAvCaptureVideoStabilizationMode(mode); + + return [self isVideoStabilizationAvModeSupported:avMode]; +} + +- (bool)isVideoStabilizationAvModeSupported:(AVCaptureVideoStabilizationMode)avMode { + AVCaptureDeviceFormat *format = _captureDevice.activeFormat; + if (format == nil) { + return false; + } + + bool isSupported = [format isVideoStabilizationModeSupported:avMode]; + return isSupported; +} + +AVCaptureVideoStabilizationMode getAvCaptureVideoStabilizationMode( + FCPPlatformVideoStabilizationMode videoStabilizationMode) { + switch (videoStabilizationMode) { + case FCPPlatformVideoStabilizationModeOff: + return AVCaptureVideoStabilizationModeOff; + case FCPPlatformVideoStabilizationModeStandard: + return AVCaptureVideoStabilizationModeStandard; + + case FCPPlatformVideoStabilizationModeCinematic: + return AVCaptureVideoStabilizationModeCinematic; + + case FCPPlatformVideoStabilizationModeCinematicExtended: + if (@available(iOS 13.0, *)) { + return AVCaptureVideoStabilizationModeCinematicExtended; + } else { + return AVCaptureVideoStabilizationModeCinematic; + } + + default: + return AVCaptureVideoStabilizationModeOff; + } +} + - (CGFloat)minimumAvailableZoomFactor { return _captureDevice.minAvailableVideoZoomFactor; } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h index d8f97926b770..4fb685725af2 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h @@ -113,6 +113,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)startImageStreamWithMessenger:(NSObject *)messenger; - (void)stopImageStream; - (void)setZoomLevel:(CGFloat)zoom withCompletion:(void (^)(FlutterError *_Nullable))completion; +- (void)setVideoStabilizationMode:(FCPPlatformVideoStabilizationMode)mode + withCompletion:(void (^)(FlutterError *_Nullable))completion; +- (bool)isVideoStabilizationModeSupported:(FCPPlatformVideoStabilizationMode)mode; - (void)setUpCaptureSessionForAudio; @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/messages.g.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/messages.g.h index 8e3dd431443a..7939423c4235 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/messages.g.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v18.0.0), do not edit directly. +// Autogenerated from Pigeon (v18.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -114,6 +114,19 @@ typedef NS_ENUM(NSUInteger, FCPPlatformResolutionPreset) { - (instancetype)initWithValue:(FCPPlatformResolutionPreset)value; @end +typedef NS_ENUM(NSUInteger, FCPPlatformVideoStabilizationMode) { + FCPPlatformVideoStabilizationModeOff = 0, + FCPPlatformVideoStabilizationModeStandard = 1, + FCPPlatformVideoStabilizationModeCinematic = 2, + FCPPlatformVideoStabilizationModeCinematicExtended = 3, +}; + +/// Wrapper for FCPPlatformVideoStabilizationMode to allow for nullability. +@interface FCPPlatformVideoStabilizationModeBox : NSObject +@property(nonatomic, assign) FCPPlatformVideoStabilizationMode value; +- (instancetype)initWithValue:(FCPPlatformVideoStabilizationMode)value; +@end + @class FCPPlatformCameraDescription; @class FCPPlatformCameraState; @class FCPPlatformMediaSettings; @@ -263,9 +276,16 @@ NSObject *FCPCameraApiGetCodec(void); - (void)getMaximumZoomLevel:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; /// Sets the zoom factor. - (void)setZoomLevel:(double)zoom completion:(void (^)(FlutterError *_Nullable))completion; +/// Sets the video stabilization mode. +- (void)setVideoStabilizationMode:(FCPPlatformVideoStabilizationMode)mode + completion:(void (^)(FlutterError *_Nullable))completion; +/// Sets the video stabilization mode. +- (void)isVideoStabilizationModeSupported:(FCPPlatformVideoStabilizationMode)mode + completion:(void (^)(NSNumber *_Nullable, + FlutterError *_Nullable))completion; /// Pauses streaming of preview frames. - (void)pausePreviewWithCompletion:(void (^)(FlutterError *_Nullable))completion; -/// Resumes a previously paused preview stream. +/// Resumes a previously paused preview stream.ƒ - (void)resumePreviewWithCompletion:(void (^)(FlutterError *_Nullable))completion; /// Changes the camera used while recording video. /// diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/messages.g.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/messages.g.m index 3b4355f0709d..591b70b5a755 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/messages.g.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v18.0.0), do not edit directly. +// Autogenerated from Pigeon (v18.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "./include/camera_avfoundation/messages.g.h" @@ -120,6 +120,16 @@ - (instancetype)initWithValue:(FCPPlatformResolutionPreset)value { } @end +@implementation FCPPlatformVideoStabilizationModeBox +- (instancetype)initWithValue:(FCPPlatformVideoStabilizationMode)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + @interface FCPPlatformCameraDescription () + (FCPPlatformCameraDescription *)fromList:(NSArray *)list; + (nullable FCPPlatformCameraDescription *)nullableFromList:(NSArray *)list; @@ -191,7 +201,7 @@ + (instancetype)makeWithPreviewSize:(FCPPlatformSize *)previewSize } + (FCPPlatformCameraState *)fromList:(NSArray *)list { FCPPlatformCameraState *pigeonResult = [[FCPPlatformCameraState alloc] init]; - pigeonResult.previewSize = [FCPPlatformSize nullableFromList:(GetNullableObjectAtIndex(list, 0))]; + pigeonResult.previewSize = GetNullableObjectAtIndex(list, 0); pigeonResult.exposureMode = [GetNullableObjectAtIndex(list, 1) integerValue]; pigeonResult.focusMode = [GetNullableObjectAtIndex(list, 2) integerValue]; pigeonResult.exposurePointSupported = [GetNullableObjectAtIndex(list, 3) boolValue]; @@ -203,7 +213,7 @@ + (nullable FCPPlatformCameraState *)nullableFromList:(NSArray *)list { } - (NSArray *)toList { return @[ - (self.previewSize ? [self.previewSize toList] : [NSNull null]), + self.previewSize ?: [NSNull null], @(self.exposureMode), @(self.focusMode), @(self.exposurePointSupported), @@ -1014,6 +1024,61 @@ void SetUpFCPCameraApiWithSuffix(id binaryMessenger, [channel setMessageHandler:nil]; } } + /// Sets the video stabilization mode. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.camera_avfoundation." + @"CameraApi.setVideoStabilizationMode", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FCPCameraApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(setVideoStabilizationMode:completion:)], + @"FCPCameraApi api (%@) doesn't respond to " + @"@selector(setVideoStabilizationMode:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FCPPlatformVideoStabilizationMode arg_mode = + [GetNullableObjectAtIndex(args, 0) integerValue]; + [api setVideoStabilizationMode:arg_mode + completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Sets the video stabilization mode. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.camera_avfoundation." + @"CameraApi.isVideoStabilizationModeSupported", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FCPCameraApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(isVideoStabilizationModeSupported:completion:)], + @"FCPCameraApi api (%@) doesn't respond to " + @"@selector(isVideoStabilizationModeSupported:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FCPPlatformVideoStabilizationMode arg_mode = + [GetNullableObjectAtIndex(args, 0) integerValue]; + [api isVideoStabilizationModeSupported:arg_mode + completion:^(NSNumber *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } /// Pauses streaming of preview frames. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] @@ -1037,7 +1102,7 @@ void SetUpFCPCameraApiWithSuffix(id binaryMessenger, [channel setMessageHandler:nil]; } } - /// Resumes a previously paused preview stream. + /// Resumes a previously paused preview stream.ƒ { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:[NSString diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index d3d5bd8fe4f3..432d777d7262 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -368,6 +368,33 @@ class AVFoundationCamera extends CameraPlatform { } } + @override + Future setVideoStabilizationMode( + int cameraId, VideoStabilizationMode mode) async { + try { + await _hostApi + .setVideoStabilizationMode(_pigeonVideoStabilizationMode(mode)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future> getVideoStabilizationSupportedModes( + int cameraId) async { + final Set ret = {}; + + for (final VideoStabilizationMode mode in VideoStabilizationMode.values) { + final bool isSupported = await _hostApi.isVideoStabilizationModeSupported( + _pigeonVideoStabilizationMode(mode)); + if (isSupported) { + ret.add(mode); + } + } + + return ret; + } + @override Future pausePreview(int cameraId) async { await _hostApi.pausePreview(); @@ -480,6 +507,28 @@ class AVFoundationCamera extends CameraPlatform { return PlatformResolutionPreset.max; } + /// Returns a [ResolutionPreset]'s Pigeon representation. + PlatformVideoStabilizationMode _pigeonVideoStabilizationMode( + VideoStabilizationMode videoStabilizationMode) { + switch (videoStabilizationMode) { + case VideoStabilizationMode.off: + return PlatformVideoStabilizationMode.off; + case VideoStabilizationMode.standard: + return PlatformVideoStabilizationMode.standard; + case VideoStabilizationMode.cinematic: + return PlatformVideoStabilizationMode.cinematic; + case VideoStabilizationMode.cinematicExtended: + return PlatformVideoStabilizationMode.cinematicExtended; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return PlatformVideoStabilizationMode.cinematic; + } + /// Returns an [ImageFormatGroup]'s Pigeon representation. PlatformImageFormatGroup _pigeonImageFormat(ImageFormatGroup format) { switch (format) { diff --git a/packages/camera/camera_avfoundation/lib/src/messages.g.dart b/packages/camera/camera_avfoundation/lib/src/messages.g.dart index 4290eb02ed2e..860a5f086807 100644 --- a/packages/camera/camera_avfoundation/lib/src/messages.g.dart +++ b/packages/camera/camera_avfoundation/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v18.0.0), do not edit directly. +// Autogenerated from Pigeon (v18.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -84,6 +84,13 @@ enum PlatformResolutionPreset { max, } +enum PlatformVideoStabilizationMode { + off, + standard, + cinematic, + cinematicExtended, +} + class PlatformCameraDescription { PlatformCameraDescription({ required this.name, @@ -138,7 +145,7 @@ class PlatformCameraState { Object encode() { return [ - previewSize.encode(), + previewSize, exposureMode.index, focusMode.index, exposurePointSupported, @@ -149,7 +156,7 @@ class PlatformCameraState { static PlatformCameraState decode(Object result) { result as List; return PlatformCameraState( - previewSize: PlatformSize.decode(result[0]! as List), + previewSize: result[0]! as PlatformSize, exposureMode: PlatformExposureMode.values[result[1]! as int], focusMode: PlatformFocusMode.values[result[2]! as int], exposurePointSupported: result[3]! as bool, @@ -1003,6 +1010,63 @@ class CameraApi { } } + /// Sets the video stabilization mode. + Future setVideoStabilizationMode( + PlatformVideoStabilizationMode mode) async { + final String __pigeon_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setVideoStabilizationMode$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([mode.index]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else { + return; + } + } + + /// Sets the video stabilization mode. + Future isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode mode) async { + final String __pigeon_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.isVideoStabilizationModeSupported$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([mode.index]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as bool?)!; + } + } + /// Pauses streaming of preview frames. Future pausePreview() async { final String __pigeon_channelName = @@ -1028,7 +1092,7 @@ class CameraApi { } } - /// Resumes a previously paused preview stream. + /// Resumes a previously paused preview stream.ƒ Future resumePreview() async { final String __pigeon_channelName = 'dev.flutter.pigeon.camera_avfoundation.CameraApi.resumePreview$__pigeon_messageChannelSuffix'; diff --git a/packages/camera/camera_avfoundation/pigeons/messages.dart b/packages/camera/camera_avfoundation/pigeons/messages.dart index c50ecc33e408..fba8b2266acc 100644 --- a/packages/camera/camera_avfoundation/pigeons/messages.dart +++ b/packages/camera/camera_avfoundation/pigeons/messages.dart @@ -79,6 +79,13 @@ enum PlatformResolutionPreset { max, } +enum PlatformVideoStabilizationMode { + off, + standard, + cinematic, + cinematicExtended, +} + // Pigeon version of CameraDescription. class PlatformCameraDescription { PlatformCameraDescription({ @@ -289,11 +296,21 @@ abstract class CameraApi { @ObjCSelector('setZoomLevel:') void setZoomLevel(double zoom); + /// Sets the video stabilization mode. + @async + @ObjCSelector('setVideoStabilizationMode:') + void setVideoStabilizationMode(PlatformVideoStabilizationMode mode); + + /// Sets the video stabilization mode. + @async + @ObjCSelector('isVideoStabilizationModeSupported:') + bool isVideoStabilizationModeSupported(PlatformVideoStabilizationMode mode); + /// Pauses streaming of preview frames. @async void pausePreview(); - /// Resumes a previously paused preview stream. + /// Resumes a previously paused preview stream.ƒ @async void resumePreview(); diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index c00b8d68df03..47b9aa08a4b6 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.17 +version: 0.10.0 environment: sdk: ^3.2.3 @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.7.0 + camera_platform_interface: ^2.9.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 df04b1a8e8ed..5f5a0819229c 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -578,6 +578,103 @@ void main() { verify(mockApi.setFocusMode(PlatformFocusMode.locked)); }); + test('Should set video stabilization mode to off', () async { + await camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.off); + + verify(mockApi + .setVideoStabilizationMode(PlatformVideoStabilizationMode.off)); + }); + + test('Should set video stabilization mode to standard', () async { + await camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.standard); + + verify(mockApi + .setVideoStabilizationMode(PlatformVideoStabilizationMode.standard)); + }); + + test('Should set video stabilization mode to cinematic', () async { + await camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.cinematic); + + verify(mockApi + .setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematic)); + }); + + test('Should set video stabilization mode to cinematicExtended', () async { + await camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.cinematicExtended); + + verify(mockApi.setVideoStabilizationMode( + PlatformVideoStabilizationMode.cinematicExtended)); + }); + + test('Should get no video stabilization mode', () async { + when(mockApi.isVideoStabilizationModeSupported(any)) + .thenAnswer((_) async => false); + + final Iterable modes = + await camera.getVideoStabilizationSupportedModes(cameraId); + + expect(modes, isEmpty); + }); + + test('Should get off and standard video stabilization modes', () async { + when(mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.off)) + .thenAnswer((_) async => true); + when(mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.standard)) + .thenAnswer((_) async => true); + when(mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.cinematic)) + .thenAnswer((_) async => false); + when(mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.cinematicExtended)) + .thenAnswer((_) async => false); + + final List modes = + (await camera.getVideoStabilizationSupportedModes(cameraId)).toList(); + + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.standard, + ]); + }); + + test('Should get all video stabilization modes', () async { + when(mockApi.isVideoStabilizationModeSupported(any)) + .thenAnswer((_) async => true); + + final List modes = + (await camera.getVideoStabilizationSupportedModes(cameraId)).toList(); + + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.standard, + VideoStabilizationMode.cinematic, + VideoStabilizationMode.cinematicExtended, + ]); + }); + + test( + 'Should throw CameraException when unavailable video stabilization mode is set', + () async { + const String code = 'VIDEO_STABILIIZATION_ERROR'; + const String message = 'Unavailable video stabilization mode error'; + when(mockApi.setVideoStabilizationMode(any)).thenAnswer( + (_) async => throw PlatformException(code: code, message: message)); + + expect( + () => camera.setVideoStabilizationMode( + cameraId, VideoStabilizationMode.cinematic), + throwsA(isA() + .having((CameraException e) => e.code, 'code', code) + .having((CameraException e) => e.description, 'description', + message))); + }); + test('Should set the focus point to a value', () async { const Point point = Point(0.4, 0.6); await camera.setFocusPoint(cameraId, point); 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 729b39f7f283..8a5d4bf2c603 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart @@ -338,6 +338,30 @@ class MockCameraApi extends _i1.Mock implements _i2.CameraApi { returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + @override + _i3.Future setVideoStabilizationMode( + _i2.PlatformVideoStabilizationMode? mode) => + (super.noSuchMethod( + Invocation.method( + #setVideoStabilizationMode, + [mode], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future isVideoStabilizationModeSupported( + _i2.PlatformVideoStabilizationMode? mode) => + (super.noSuchMethod( + Invocation.method( + #isVideoStabilizationModeSupported, + [mode], + ), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) as _i3.Future); + @override _i3.Future pausePreview() => (super.noSuchMethod( Invocation.method( diff --git a/packages/camera/camera_platform_interface/AUTHORS b/packages/camera/camera_platform_interface/AUTHORS index 0d1bfa6a90c7..d21929d105fb 100644 --- a/packages/camera/camera_platform_interface/AUTHORS +++ b/packages/camera/camera_platform_interface/AUTHORS @@ -65,3 +65,4 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Mairramer +Rui Craveiro \ No newline at end of file diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 43d3e63c42b8..4fe3dbf32f17 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,9 +1,14 @@ +## 2.9.0 + +* Adds support for video stabilization. + ## 2.8.0 * Deprecates `maxVideoDuration`/`maxDuration`, as it was never implemented on most platforms, and there is no plan to implement it in the future. * Updates minimum supported SDK version to Flutter 3.16/Dart 3.2. + ## 2.7.4 * Updates minimum supported SDK version to Flutter 3.13/Dart 3.1. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index e38df4865331..229d36222c62 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -502,6 +502,44 @@ class MethodChannelCamera extends CameraPlatform { } } + @override + Future> getVideoStabilizationSupportedModes( + int cameraId) async { + try { + final List? modes = await _channel.invokeMethod>( + 'getVideoStabilizationSupportedModes', + { + 'cameraId': cameraId, + }, + ); + + if (modes == null) { + return []; + } + return modes + .map((Object? e) => deserializeVideoStabilizationMode(e! as String)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the video stabilization mode for the selected camera + @override + Future setVideoStabilizationMode( + int cameraId, VideoStabilizationMode mode) async { + try { + await _channel.invokeMethod( + 'setVideoStabilizationMode', + { + 'cameraId': cameraId, + 'mode': mode.index, + }, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + @override Future pausePreview(int cameraId) async { await _channel.invokeMethod( diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 218de02c6358..c991b323bb61 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -272,6 +272,30 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('setZoomLevel() is not implemented.'); } + /// Gets a list of video stabilization modes that are supported for the selected camera. + /// + /// Will return the list of supported video stabilization modes + /// on Android (when using camera_android_camerax package) and + /// on iOS. Will return an empty list on all other platforms. + Future> getVideoStabilizationSupportedModes( + int cameraId) async { + return []; + } + + /// Sets the video stabilization mode for the selected camera. + /// + /// On Android (when using camera_android_camerax) and on iOS + /// the supplied [mode] value should be a mode in the list returned + /// by [getVideoStabilizationSupportedModes]. + /// + /// Throws a [CameraException] when a not supported video stabilization + /// mode is supplied. + Future setVideoStabilizationMode( + int cameraId, VideoStabilizationMode mode) async { + throw CameraException( + 'not_supported', 'setVideoStabilizationMode() is not implemented.'); + } + /// Pause the active preview on the current frame for the selected camera. Future pausePreview(int cameraId) { throw UnimplementedError('pausePreview() is not implemented.'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index e56b9b4c49a1..34e906a2808a 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -13,3 +13,4 @@ export 'image_format_group.dart'; export 'media_settings.dart'; export 'resolution_preset.dart'; export 'video_capture_options.dart'; +export 'video_stabilization_mode.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/types/video_stabilization_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/video_stabilization_mode.dart new file mode 100644 index 000000000000..c722792e2ef8 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/video_stabilization_mode.dart @@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The possible video stabilization modes that can be capturing video. +enum VideoStabilizationMode { + /// Video stabilization is disabled. + off, + + /// Standard video stabilization is enabled. + /// Maps to CONTROL_VIDEO_STABILIZATION_MODE_ON on Android + /// and to AVCaptureVideoStabilizationModeStandard on iOS. + standard, + + /// Cinematic video stabilization is enabled. + /// Maps to CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION on Android + /// and to AVCaptureVideoStabilizationModeCinematic on iOS. + cinematic, + + /// Extended cinematic video stabilization is enabled. + /// The same as cinematic on Android, so maps to CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION. + /// Maps to AVCaptureVideoStabilizationModeCinematicExtended on iOS. + cinematicExtended, +} + +/// Returns the video stabilization mode as a String. +String serializeVideoStabilizationMode( + VideoStabilizationMode videoStabilizationMode) { + switch (videoStabilizationMode) { + case VideoStabilizationMode.off: + return 'off'; + case VideoStabilizationMode.standard: + return 'standard'; + case VideoStabilizationMode.cinematic: + return 'cinematic'; + case VideoStabilizationMode.cinematicExtended: + return 'cinematicExtended'; + } +} + +/// Returns the video stabilization mode for a given String. +VideoStabilizationMode deserializeVideoStabilizationMode(String str) { + switch (str) { + case 'off': + return VideoStabilizationMode.off; + case 'standard': + return VideoStabilizationMode.standard; + case 'cinematic': + return VideoStabilizationMode.cinematic; + case 'cinematicExtended': + return VideoStabilizationMode.cinematicExtended; + default: + throw ArgumentError('"$str" is not a valid VideoStabilizationMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 6691da0cb3a6..71993e0e18e7 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.8.0 +version: 2.9.0 environment: sdk: ^3.2.0 diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 063633255c99..28b08d5e6d31 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -1014,6 +1014,149 @@ void main() { 'Illegal zoom error'))); }); + test('Should get empty list from getVideoStabilizationSupportedModes', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'getVideoStabilizationSupportedModes': [], + }, + ); + + // Act + final Iterable modes = + await camera.getVideoStabilizationSupportedModes( + cameraId, + ); + + // Assert + expect(modes, []); + + expect(channel.log, [ + isMethodCall('getVideoStabilizationSupportedModes', + arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test( + 'Should get list containing off from getVideoStabilizationSupportedModes', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'getVideoStabilizationSupportedModes': ['off'], + }, + ); + + // Act + final Iterable modes = + await camera.getVideoStabilizationSupportedModes( + cameraId, + ); + + // Assert + expect(modes, [VideoStabilizationMode.off]); + + expect(channel.log, [ + isMethodCall('getVideoStabilizationSupportedModes', + arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test( + 'Should get list containing all from getVideoStabilizationSupportedModes', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'getVideoStabilizationSupportedModes': [ + 'off', + 'standard', + 'cinematic', + 'cinematicExtended', + ], + }, + ); + + // Act + final Iterable modes = + await camera.getVideoStabilizationSupportedModes( + cameraId, + ); + + // Assert + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.standard, + VideoStabilizationMode.cinematic, + VideoStabilizationMode.cinematicExtended, + ]); + + expect(channel.log, [ + isMethodCall('getVideoStabilizationSupportedModes', + arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the video stabilization mode', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setVideoStabilizationMode': null}, + ); + + // Act + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.off, + ); + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.standard, + ); + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.cinematic, + ); + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.cinematicExtended, + ); + + // Assert + expect(channel.log, [ + isMethodCall('setVideoStabilizationMode', + arguments: { + 'cameraId': cameraId, + 'mode': VideoStabilizationMode.off.index, + }), + isMethodCall('setVideoStabilizationMode', + arguments: { + 'cameraId': cameraId, + 'mode': VideoStabilizationMode.standard.index, + }), + isMethodCall('setVideoStabilizationMode', + arguments: { + 'cameraId': cameraId, + 'mode': VideoStabilizationMode.cinematic.index, + }), + isMethodCall('setVideoStabilizationMode', + arguments: { + 'cameraId': cameraId, + 'mode': VideoStabilizationMode.cinematicExtended.index, + }), + ]); + }); + test('Should lock the capture orientation', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/camera/camera_platform_interface/test/types/video_stabilization_mode_test.dart b/packages/camera/camera_platform_interface/test/types/video_stabilization_mode_test.dart new file mode 100644 index 000000000000..e0ba2b26acb4 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/video_stabilization_mode_test.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/src/types/video_stabilization_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('VideoStabilizationMode should contain 3 options', () { + const List values = VideoStabilizationMode.values; + + expect(values.length, 4); + }); + + test('VideoStabilizationMode enum should have items in correct index', () { + const List values = VideoStabilizationMode.values; + + expect(values[0], VideoStabilizationMode.off); + expect(values[1], VideoStabilizationMode.standard); + expect(values[2], VideoStabilizationMode.cinematic); + expect(values[3], VideoStabilizationMode.cinematicExtended); + }); + + test('serializeVideoStabilizationMode() should serialize correctly', () { + expect(serializeVideoStabilizationMode(VideoStabilizationMode.off), 'off'); + expect(serializeVideoStabilizationMode(VideoStabilizationMode.standard), + 'standard'); + expect(serializeVideoStabilizationMode(VideoStabilizationMode.cinematic), + 'cinematic'); + expect( + serializeVideoStabilizationMode( + VideoStabilizationMode.cinematicExtended), + 'cinematicExtended'); + }); + + test('deserializeVideoStabilizationMode() should deserialize correctly', () { + expect( + deserializeVideoStabilizationMode('off'), VideoStabilizationMode.off); + expect(deserializeVideoStabilizationMode('standard'), + VideoStabilizationMode.standard); + expect(deserializeVideoStabilizationMode('cinematic'), + VideoStabilizationMode.cinematic); + expect(deserializeVideoStabilizationMode('cinematicExtended'), + VideoStabilizationMode.cinematicExtended); + }); +} diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index c9e8e661da42..fa485ed85f69 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.4+1 + +* Updates camera_platform_interface lib to 2.9.0. + ## 0.3.4 * Removes `maxVideoDuration`/`maxDuration`, as the feature was never exposed at diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index e33a2e0fd286..afa94af13daf 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.3.4 +version: 0.3.4+1 environment: sdk: ^3.2.0 @@ -17,7 +17,7 @@ flutter: fileName: camera_web.dart dependencies: - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.9.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 4b164343ecd6..630d40d51d5f 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.4+1 + +* Updates camera_platform_interface lib to 2.9.0. + ## 0.2.4 * Removes `maxVideoDuration`/`maxDuration`, as the feature was never exposed at diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 7e3f7381b4b0..9bc006d7df80 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_windows description: A Flutter plugin for getting information about and controlling the camera on Windows. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.4 +version: 0.2.4+1 environment: sdk: ^3.2.0 @@ -17,7 +17,7 @@ flutter: dartPluginClass: CameraWindows dependencies: - camera_platform_interface: ^2.6.0 + camera_platform_interface: ^2.9.0 cross_file: ^0.3.1 flutter: sdk: flutter