diff --git a/packages/camera/camera/AUTHORS b/packages/camera/camera/AUTHORS index 493a0b4ef9c..605414ab7dc 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 fae21094cd7..b53610726d6 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -3,6 +3,10 @@ * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. * Updates README to reflect that only Android API 24+ is supported. +## 0.12.0 + +* Adds support for video stabilization. + ## 0.11.2 * Fixes overflowed toggles in the camera example. diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index e268268e320..30433099189 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -31,3 +31,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera: {path: ../../../camera/camera}, camera_android_camerax: {path: ../../../camera/camera_android_camerax}, camera_avfoundation: {path: ../../../camera/camera_avfoundation}, camera_platform_interface: {path: ../../../camera/camera_platform_interface}, camera_web: {path: ../../../camera/camera_web}} diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 2557b3c58cd..1558dbf687d 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -13,6 +13,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 4ecc6aae971..82c90e7e93c 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, @@ -201,6 +207,8 @@ class CameraValue { previewPauseOrientation == null ? this.previewPauseOrientation : previewPauseOrientation.orNull, + videoStabilizationMode: + videoStabilizationMode ?? this.videoStabilizationMode, ); } @@ -222,6 +230,7 @@ class CameraValue { 'recordingOrientation: $recordingOrientation, ' 'isPreviewPaused: $isPreviewPaused, ' 'previewPausedOrientation: $previewPauseOrientation, ' + 'videoStabilizationMode: $videoStabilizationMode, ' 'description: $description)'; } } @@ -701,6 +710,94 @@ class CameraController extends ValueNotifier { } } + /// Set the video stabilization mode for the selected camera. + /// + /// When [allowFallback] is true (default) the camera will be set to the best + /// video stabilization mode up to, and including, [mode]. + /// + /// When [allowFallback] is false and if [mode] is not one of the supported + /// modes (see [getSupportedVideoStabilizationModes]), then it throws an + /// [ArgumentError]. + /// + /// This feature is only available if [getSupportedVideoStabilizationModes] + /// returns at least one value other than [VideoStabilizationMode.off]. + Future setVideoStabilizationMode( + VideoStabilizationMode mode, { + bool allowFallback = true, + }) async { + _throwIfNotInitialized('setVideoStabilizationMode'); + try { + final VideoStabilizationMode? modeToSet = + await _getVideoStabilizationModeToSet(mode, allowFallback); + + // When _getVideoStabilizationModeToSet returns null + // it means that the device doesn't support any + // video stabilization mode and that doing nothing + // is valid because allowFallback is true or [mode] + // is [VideoStabilizationMode.off], so this results + // in a no-op. + if (modeToSet == null) { + return; + } + await CameraPlatform.instance.setVideoStabilizationMode( + _cameraId, + modeToSet, + ); + value = value.copyWith(videoStabilizationMode: modeToSet); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + Future _getVideoStabilizationModeToSet( + VideoStabilizationMode requestedMode, + bool allowFallback, + ) async { + final Iterable supportedModes = await CameraPlatform + .instance + .getSupportedVideoStabilizationModes(_cameraId); + + // If it can't fallback and the specific + // requested mode isn't available, then... + if (!allowFallback && !supportedModes.contains(requestedMode)) { + // if the request is off, it is a no-op + if (requestedMode == VideoStabilizationMode.off) { + return null; + } + // otherwise, it throws. + throw ArgumentError('Unavailable video stabilization mode.', 'mode'); + } + + VideoStabilizationMode? fallbackMode = requestedMode; + while (fallbackMode != null && !supportedModes.contains(fallbackMode)) { + fallbackMode = CameraPlatform.getFallbackVideoStabilizationMode( + fallbackMode, + ); + } + + return fallbackMode; + } + + /// Gets a list of video stabilization modes that are supported + /// for the selected camera. + /// + /// [VideoStabilizationMode.off] will always be listed. + Future> + getSupportedVideoStabilizationModes() async { + _throwIfNotInitialized('isVideoStabilizationModeSupported'); + try { + final Set modes = { + VideoStabilizationMode.off, + ...await CameraPlatform.instance.getSupportedVideoStabilizationModes( + _cameraId, + ), + }; + return modes; + } 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 073c8019872..598b882f53c 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.2 +version: 0.12.0 environment: sdk: ^3.7.0 @@ -21,9 +21,9 @@ flutter: default_package: camera_web dependencies: - camera_android_camerax: ^0.6.13 - camera_avfoundation: ^0.9.18 - camera_platform_interface: ^2.10.0 + camera_android_camerax: ^0.7.0 + camera_avfoundation: ^0.10.0 + camera_platform_interface: ^2.12.0 camera_web: ^0.3.3 flutter: sdk: flutter @@ -38,3 +38,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_android_camerax: {path: ../../camera/camera_android_camerax}, camera_avfoundation: {path: ../../camera/camera_avfoundation}, camera_platform_interface: {path: ../../camera/camera_platform_interface}, camera_web: {path: ../../camera/camera_web}} diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 139c5534124..1e4fdfb67ef 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -137,6 +137,16 @@ class FakeController extends ValueNotifier @override CameraDescription get description => value.description; + @override + Future setVideoStabilizationMode( + VideoStabilizationMode mode, { + bool allowFallback = true, + }) async {} + + @override + Future> + getSupportedVideoStabilizationModes() async => []; + @override bool supportsImageStreaming() => true; } diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index cc7983d10a3..33f410df34f 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1474,6 +1474,2136 @@ void main() { ).called(4); }); + test( + 'getSupportedVideoStabilizationModes() returns off when device supports no mode', + () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + + await cameraController.initialize(); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer((_) async => []); + + // act + final Iterable modes = + await cameraController.getSupportedVideoStabilizationModes(); + + // assert + expect(modes, [VideoStabilizationMode.off]); + }, + ); + + test( + 'getSupportedVideoStabilizationModes() returns off when device supports only off', + () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + + await cameraController.initialize(); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + // act + final Iterable modes = + await cameraController.getSupportedVideoStabilizationModes(); + + // assert + expect(modes, [VideoStabilizationMode.off]); + }, + ); + + test( + 'getSupportedVideoStabilizationModes() returns off and level1', + () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + + await cameraController.initialize(); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + // act + final Iterable modes = + await cameraController.getSupportedVideoStabilizationModes(); + + // assert + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ]); + }, + ); + + test( + 'getSupportedVideoStabilizationModes() returns off, level1 and level2', + () async { + // arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + + await cameraController.initialize(); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + ], + ); + + // act + final Iterable modes = + await cameraController.getSupportedVideoStabilizationModes(); + + // assert + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + ]); + }, + ); + + test('getSupportedVideoStabilizationModes() 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.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + // act + final Iterable modes = + await cameraController.getSupportedVideoStabilizationModes(); + + // assert + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ]); + }); + + 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.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + when( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + ), + ); + + expect( + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + ), + throwsA( + isA().having( + (CameraException error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ), + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback never calls CameraPlatform.instance.setVideoStabilizationMode when no supported mode is available', + () async { + //// The purpose of this test is to ensure that when no video stabilization mode is supported, + //// then all calls to setVideoStabilizationMode with fallback will not result in any + //// call to CameraPlatform.instance.setVideoStabilizationMode. + + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer((_) async => []); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + ); + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + ); + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + ); + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when off is requested and only off is available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when level1 is requested and only off is available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when level2 is requested and only off is available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when level3 is requested and only off is available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when off is requested and only off and level1 are available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level1 is requested and only off and level1 are available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level2 is requested and only off and level1 are available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level3 is requested and only off and level1 are available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when off is requested and all levels are available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level1 is requested and all levels are available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level2 when level2 is requested and all levels are available', + () async { + //arrange + 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.level2, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() with fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level3 when level3 is requested and all levels available', + () async { + //arrange + 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.level3, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls never calls CameraPlatform.instance.setVideoStabilizationMode when off is requested no supported mode is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer((_) async => []); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + allowFallback: false, + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level1 is requested and no supported mode is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer((_) async => []); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level2 is requested and no supported mode is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer((_) async => []); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level3 is requested and no supported mode is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer((_) async => []); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when off is requested and only off is available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level1 is requested and only off is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level2 is requested and only off is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level3 is requested and only off is available', + () async { + //arrange + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ), + ResolutionPreset.max, + ); + await cameraController.initialize(); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [VideoStabilizationMode.off], + ); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when off is requested and all level1 is available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level1 is requested and level1 is available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback throws $ArgumentError when level2 is requested and only off and level1 are available', + () async { + //arrange + 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.level2, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // assert + expect( + // act + cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + allowFallback: false, + ), + throwsA( + isA().having( + (ArgumentError error) => error.name, + 'name', + 'mode', + ), + ), + ); + + // assert + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.off when off is requested and all levels are available', + () async { + //arrange + 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.off, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.off, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level1 is requested and all levels are available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level1, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level2 when level2 is requested and all levels are available', + () async { + //arrange + 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.level2, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.setVideoStabilizationMode( + mockInitializeCamera, + VideoStabilizationMode.level2, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level2, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode() without fallback calls CameraPlatform.instance.setVideoStabilizationMode with VideoStabilizationMode.level1 when level3 is requested and all levels are available', + () async { + //arrange + 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.level1, + ), + ).thenAnswer((_) => Future(() {})); + + when( + CameraPlatform.instance.getSupportedVideoStabilizationModes( + mockInitializeCamera, + ), + ).thenAnswer( + (_) async => [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ], + ); + + clearInteractions(CameraPlatform.instance); + + // act + await cameraController.setVideoStabilizationMode( + VideoStabilizationMode.level3, + allowFallback: false, + ); + + // assert + verify( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level3, + ), + ).called(1); + + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.off, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level1, + ), + ); + verifyNever( + CameraPlatform.instance.setVideoStabilizationMode( + cameraController.cameraId, + VideoStabilizationMode.level2, + ), + ); + }, + ); + test('pausePreview() calls $CameraPlatform', () async { final CameraController cameraController = CameraController( const CameraDescription( @@ -1989,6 +4119,29 @@ class MockCameraPlatform extends Mock returnValue: Future.value(1.0), ) as Future; + + @override + Future> getSupportedVideoStabilizationModes( + int cameraId, + ) { + return super.noSuchMethod( + Invocation.method(#getSupportedVideoStabilizationModes, [ + 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 205ed6ce884..c4163c4d52a 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.level2, ); expect(cameraValue, isA()); @@ -49,6 +50,7 @@ void main() { expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); expect(cameraValue.isPreviewPaused, false); expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.videoStabilizationMode, VideoStabilizationMode.level2); }); test('Can be created as uninitialized', () { @@ -73,6 +75,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', () { @@ -98,6 +101,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', () { @@ -154,6 +158,7 @@ void main() { isPreviewPaused: true, previewPauseOrientation: DeviceOrientation.portraitUp, description: FakeController.fakeDescription, + videoStabilizationMode: VideoStabilizationMode.level3, ); expect( @@ -168,6 +173,7 @@ void main() { 'recordingOrientation: DeviceOrientation.portraitUp, ' 'isPreviewPaused: true, ' 'previewPausedOrientation: DeviceOrientation.portraitUp, ' + 'videoStabilizationMode: VideoStabilizationMode.level3, ' // CameraDescription.toString is defined in the platform interface // package, so don't assert a specific value for it, only that // whatever it returns is inserted as expected. diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index b9a0da19604..4c959fa4ed8 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -36,3 +36,9 @@ dev_dependencies: topics: - camera + + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} \ No newline at end of file diff --git a/packages/camera/camera_android_camerax/AUTHORS b/packages/camera/camera_android_camerax/AUTHORS index 557dff97933..7fc8bd920ea 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 dc1f168e948..92e30327f07 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.23 * Converts NV21-compatible streamed images to NV21 when requested. In doing so, diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraCharacteristicsProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraCharacteristicsProxyApi.java index e3311862f3b..d19d8bcf6bc 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraCharacteristicsProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraCharacteristicsProxyApi.java @@ -28,4 +28,10 @@ public CameraCharacteristics.Key infoSupportedHardwareLevel() { public CameraCharacteristics.Key sensorOrientation() { return CameraCharacteristics.SENSOR_ORIENTATION; } + + @NonNull + @Override + public CameraCharacteristics.Key controlAvailableVideoStabilizationModes() { + return CameraCharacteristics.CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES; + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt index c08f52606d5..50843d8c099 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt @@ -5382,14 +5382,12 @@ abstract class PigeonApiImageProxy( } } } -/** Utils for working with [ImageProxy]s. */ +/** Utilities for working with [ImageProxy]s. */ @Suppress("UNCHECKED_CAST") abstract class PigeonApiImageProxyUtils( open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar ) { - /** - * Returns a single Byte Buffer that is representative of the [planes] that are NV21 compatible. - */ + /** Returns a single buffer that is representative of three NV21-compatible [planes]. */ abstract fun getNv21Buffer( imageWidth: Long, imageHeight: Long, @@ -6423,6 +6421,15 @@ abstract class PigeonApiCaptureRequest( */ abstract fun controlAELock(): android.hardware.camera2.CaptureRequest.Key<*> + /** + * Whether video stabilization is active. + * + * Value is int. + * + * This key is available on all devices. + */ + abstract fun controlVideoStabilizationMode(): android.hardware.camera2.CaptureRequest.Key<*> + companion object { @Suppress("LocalVariableName") fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiCaptureRequest?) { @@ -6451,6 +6458,30 @@ abstract class PigeonApiCaptureRequest( channel.setMessageHandler(null) } } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.camera_android_camerax.CaptureRequest.controlVideoStabilizationMode", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val pigeon_identifierArg = args[0] as Long + val wrapped: List = + try { + api.pigeonRegistrar.instanceManager.addDartCreatedInstance( + api.controlVideoStabilizationMode(), pigeon_identifierArg) + listOf(null) + } catch (exception: Throwable) { + CameraXLibraryPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } @@ -6921,6 +6952,17 @@ abstract class PigeonApiCameraCharacteristics( */ abstract fun sensorOrientation(): android.hardware.camera2.CameraCharacteristics.Key<*> + /** + * List of video stabilization modes for android.control.videoStabilizationMode that are supported + * by this camera device. + * + * Value is `ControlAvailableVideoStabilizationMode`. + * + * This key is available on all devices. + */ + abstract fun controlAvailableVideoStabilizationModes(): + android.hardware.camera2.CameraCharacteristics.Key<*> + companion object { @Suppress("LocalVariableName") fun setUpMessageHandlers( @@ -6976,6 +7018,30 @@ abstract class PigeonApiCameraCharacteristics( channel.setMessageHandler(null) } } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.camera_android_camerax.CameraCharacteristics.controlAvailableVideoStabilizationModes", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val pigeon_identifierArg = args[0] as Long + val wrapped: List = + try { + api.pigeonRegistrar.instanceManager.addDartCreatedInstance( + api.controlAvailableVideoStabilizationModes(), pigeon_identifierArg) + listOf(null) + } catch (exception: Throwable) { + CameraXLibraryPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsProxyApi.java index d8b814142b8..424dea8e91a 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestOptionsProxyApi.java @@ -42,8 +42,15 @@ public CaptureRequestOptions pigeon_defaultConstructor( continue; } - builder.setCaptureRequestOption( - (CaptureRequest.Key) option.getKey(), option.getValue()); + // Because Pigeon isn't down-casting from Dart num to Java Int, + // it needs to be done below. + var key = option.getKey(); + var value = option.getValue(); + if (CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE == key) { + value = ((Long) value).intValue(); + } + + builder.setCaptureRequestOption((CaptureRequest.Key) key, value); } return builder.build(); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestProxyApi.java index 5887e63f6f2..cf737815d01 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CaptureRequestProxyApi.java @@ -22,4 +22,10 @@ class CaptureRequestProxyApi extends PigeonApiCaptureRequest { public CaptureRequest.Key controlAELock() { return CaptureRequest.CONTROL_AE_LOCK; } + + @NonNull + @Override + public CaptureRequest.Key controlVideoStabilizationMode() { + return CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE; + } } diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index f5238212492..2da55160b7c 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -29,3 +29,5 @@ dev_dependencies: flutter: uses-material-design: true +dependency_overrides: + {camera_android_camerax: {path: ../../../camera/camera_android_camerax}, camera_platform_interface: {path: ../../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 5ba79f7f710..267d1dfd0d5 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 @@ -914,6 +914,74 @@ class AndroidCameraCameraX extends CameraPlatform { } } + @override + Future> getSupportedVideoStabilizationModes( + int cameraId, + ) async { + return (await _getSupportedVideoStabilizationModeMap(cameraId)).keys; + } + + /// Throws a [ArgumentError] when an unsupported [mode] is + /// supplied. + @override + Future setVideoStabilizationMode( + int cameraId, + VideoStabilizationMode mode, + ) async { + final Map availableModes = + await _getSupportedVideoStabilizationModeMap(cameraId); + + final int? controlMode = availableModes[mode]; + if (controlMode == null) { + throw ArgumentError('Unavailable video stabilization mode.', 'mode'); + } + + final CaptureRequestOptions captureRequestOptions = proxy + .newCaptureRequestOptions( + options: { + proxy.controlVideoStabilizationModeRequest(): controlMode, + }, + ); + + final Camera2CameraControl camera2Control = proxy.fromCamera2CameraControl( + cameraControl: cameraControl, + ); + await camera2Control.addCaptureRequestOptions(captureRequestOptions); + } + + /// Gets a map of video stabilization control modes that are supported for the + /// selected camera, indexed by the respective [VideoStabilizationMode]. + Future> + _getSupportedVideoStabilizationModeMap(int cameraId) async { + final CameraInfo? camInfo = cameraInfo; + if (camInfo == null) { + return {}; + } + final Camera2CameraInfo camera2CameraInfo = proxy.fromCamera2CameraInfo( + cameraInfo: cameraInfo!, + ); + + final List controlModes = + (await camera2CameraInfo.getCameraCharacteristic( + proxy + .controlAvailableVideoStabilizationModesCameraCharacteristics(), + ))! + as List; + + final Map + modes = { + for (final int controlMode in controlModes) + // https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_OFF + if (controlMode == 0) + VideoStabilizationMode.off: 0 + // https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_ON + else if (controlMode == 1) + VideoStabilizationMode.level1: 1, + }; + + return modes; + } + /// The ui orientation changed. @override Stream onDeviceOrientationChanged() { 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 cff83c85079..92702129adc 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 @@ -6793,7 +6793,7 @@ class ImageProxy extends PigeonInternalProxyApiBaseClass { } } -/// Utils for working with [ImageProxy]s. +/// Utilities for working with [ImageProxy]s. class ImageProxyUtils extends PigeonInternalProxyApiBaseClass { /// Constructs [ImageProxyUtils] without creating the associated native object. /// @@ -6863,8 +6863,7 @@ class ImageProxyUtils extends PigeonInternalProxyApiBaseClass { } } - /// Returns a single Byte Buffer that is representative of the [planes] - /// that are NV21 compatible. + /// Returns a single buffer that is representative of three NV21-compatible [planes]. static Future getNv21Buffer( int imageWidth, int imageHeight, @@ -8258,6 +8257,14 @@ class CaptureRequest extends PigeonInternalProxyApiBaseClass { /// This key is available on all devices. static final CaptureRequestKey controlAELock = pigeonVar_controlAELock(); + /// Whether video stabilization is active. + /// + /// Value is int. + /// + /// This key is available on all devices. + static final CaptureRequestKey controlVideoStabilizationMode = + pigeonVar_controlVideoStabilizationMode(); + static void pigeon_setUpMessageHandlers({ bool pigeon_clearHandlers = false, BinaryMessenger? pigeon_binaryMessenger, @@ -8351,6 +8358,44 @@ class CaptureRequest extends PigeonInternalProxyApiBaseClass { return pigeonVar_instance; } + static CaptureRequestKey pigeonVar_controlVideoStabilizationMode() { + final CaptureRequestKey pigeonVar_instance = + CaptureRequestKey.pigeon_detached(); + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _PigeonInternalProxyApiBaseCodec(PigeonInstanceManager.instance); + final BinaryMessenger pigeonVar_binaryMessenger = + ServicesBinding.instance.defaultBinaryMessenger; + final int pigeonVar_instanceIdentifier = PigeonInstanceManager.instance + .addDartCreatedInstance(pigeonVar_instance); + () async { + const String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_android_camerax.CaptureRequest.controlVideoStabilizationMode'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [pigeonVar_instanceIdentifier], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + }(); + return pigeonVar_instance; + } + @override CaptureRequest pigeon_copy() { return CaptureRequest.pigeon_detached( @@ -8977,6 +9022,16 @@ class CameraCharacteristics extends PigeonInternalProxyApiBaseClass { static final CameraCharacteristicsKey sensorOrientation = pigeonVar_sensorOrientation(); + /// List of video stabilization modes for android.control.videoStabilizationMode + /// that are supported by this camera device. + /// + /// Value is `ControlAvailableVideoStabilizationMode`. + /// + /// This key is available on all devices. + static final CameraCharacteristicsKey + controlAvailableVideoStabilizationModes = + pigeonVar_controlAvailableVideoStabilizationModes(); + static void pigeon_setUpMessageHandlers({ bool pigeon_clearHandlers = false, BinaryMessenger? pigeon_binaryMessenger, @@ -9108,6 +9163,45 @@ class CameraCharacteristics extends PigeonInternalProxyApiBaseClass { return pigeonVar_instance; } + static CameraCharacteristicsKey + pigeonVar_controlAvailableVideoStabilizationModes() { + final CameraCharacteristicsKey pigeonVar_instance = + CameraCharacteristicsKey.pigeon_detached(); + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _PigeonInternalProxyApiBaseCodec(PigeonInstanceManager.instance); + final BinaryMessenger pigeonVar_binaryMessenger = + ServicesBinding.instance.defaultBinaryMessenger; + final int pigeonVar_instanceIdentifier = PigeonInstanceManager.instance + .addDartCreatedInstance(pigeonVar_instance); + () async { + const String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_android_camerax.CameraCharacteristics.controlAvailableVideoStabilizationModes'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [pigeonVar_instanceIdentifier], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + }(); + return pigeonVar_instance; + } + @override CameraCharacteristics pigeon_copy() { return CameraCharacteristics.pigeon_detached( diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart index 3885e720387..5c8c3bcf22a 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -66,11 +66,15 @@ class CameraXProxy { this.ratio_4_3FallbackAutoStrategyAspectRatioStrategy = _ratio_4_3FallbackAutoStrategyAspectRatioStrategy, this.controlAELockCaptureRequest = _controlAELockCaptureRequest, + this.controlVideoStabilizationModeRequest = + _controlVideoStabilizationModeRequest, this.infoSupportedHardwareLevelCameraCharacteristics = _infoSupportedHardwareLevelCameraCharacteristics, this.sensorOrientationCameraCharacteristics = _sensorOrientationCameraCharacteristics, this.getNv21BufferImageProxyUtils = ImageProxyUtils.getNv21Buffer, + this.controlAvailableVideoStabilizationModesCameraCharacteristics = + _controlAvailableVideoStabilizationModesCameraCharacteristics, }); /// Handles adding support for generic classes. @@ -369,6 +373,9 @@ class CameraXProxy { /// Calls to [CaptureRequest.controlAELock]. CaptureRequestKey Function() controlAELockCaptureRequest; + /// Calls to [CaptureRequest.controlVideoStabilizationMode]. + CaptureRequestKey Function() controlVideoStabilizationModeRequest; + /// Calls to [CameraCharacteristics.infoSupportedHardwareLevel]. final CameraCharacteristicsKey Function() infoSupportedHardwareLevelCameraCharacteristics; @@ -377,6 +384,10 @@ class CameraXProxy { final CameraCharacteristicsKey Function() sensorOrientationCameraCharacteristics; + /// Calls to [CameraCharacteristics.controlAvailableVideoStabilizationModes]. + final CameraCharacteristicsKey Function() + controlAvailableVideoStabilizationModesCameraCharacteristics; + /// Calls to [ImageProxyUtils.getNv21Buffer]. final Future Function( int imageWidth, @@ -407,10 +418,17 @@ class CameraXProxy { static CaptureRequestKey _controlAELockCaptureRequest() => CaptureRequest.controlAELock; + static CaptureRequestKey _controlVideoStabilizationModeRequest() => + CaptureRequest.controlVideoStabilizationMode; + static CameraCharacteristicsKey _infoSupportedHardwareLevelCameraCharacteristics() => CameraCharacteristics.infoSupportedHardwareLevel; static CameraCharacteristicsKey _sensorOrientationCameraCharacteristics() => CameraCharacteristics.sensorOrientation; + + static CameraCharacteristicsKey + _controlAvailableVideoStabilizationModesCameraCharacteristics() => + CameraCharacteristics.controlAvailableVideoStabilizationModes; } diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 8d0a05fbf07..43c251b99d0 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -1136,6 +1136,14 @@ abstract class CaptureRequest { /// This key is available on all devices. @static late CaptureRequestKey controlAELock; + + /// Whether video stabilization is active. + /// + /// Value is int. + /// + /// This key is available on all devices. + @static + late CaptureRequestKey controlVideoStabilizationMode; } /// A Key is used to do capture request field lookups with CaptureRequest.get or @@ -1233,6 +1241,15 @@ abstract class CameraCharacteristics { /// This key is available on all devices. @static late CameraCharacteristicsKey sensorOrientation; + + /// List of video stabilization modes for android.control.videoStabilizationMode + /// that are supported by this camera device. + /// + /// Value is `ControlAvailableVideoStabilizationMode`. + /// + /// This key is available on all devices. + @static + late CameraCharacteristicsKey controlAvailableVideoStabilizationModes; } /// An interface for retrieving Camera2-related camera information. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 311df6f2e43..0600724292b 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.23 +version: 0.7.0 environment: sdk: ^3.8.1 @@ -19,7 +19,7 @@ flutter: dependencies: async: ^2.5.0 - camera_platform_interface: ^2.11.0 + camera_platform_interface: ^2.12.0 flutter: sdk: flutter meta: ^1.7.0 @@ -35,3 +35,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} \ No newline at end of file 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 f49264de33a..ffffe96ea11 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 @@ -5215,6 +5215,424 @@ void main() { verify(mockCameraControl.setZoomRatio(zoomRatio)); }); + test('setVideoStabilizationMode sets mode expected', () async { + // Set up mocks and constants. + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraControl mockCamera2CameraControl = + MockCamera2CameraControl(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + final PigeonInstanceManager testInstanceManager = PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CaptureRequestKey controlVideoStabilizationModeKey = + CaptureRequestKey.pigeon_detached( + pigeon_instanceManager: testInstanceManager, + ); + + camera.proxy = CameraXProxy( + fromCamera2CameraControl: + ({ + required CameraControl cameraControl, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => cameraControl == mockCameraControl + ? mockCamera2CameraControl + : Camera2CameraControl.pigeon_detached( + pigeon_instanceManager: testInstanceManager, + ), + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => mockCamera2CameraInfo, + + newCaptureRequestOptions: + ({ + required Map options, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockCaptureRequestOptions mockCaptureRequestOptions = + MockCaptureRequestOptions(); + options.forEach((CaptureRequestKey key, Object? value) { + when( + mockCaptureRequestOptions.getCaptureRequestOption(key), + ).thenAnswer((_) async => value); + }); + return mockCaptureRequestOptions; + }, + + controlVideoStabilizationModeRequest: () => + controlVideoStabilizationModeKey, + controlAvailableVideoStabilizationModesCameraCharacteristics: () { + return MockCameraCharacteristicsKey(); + }, + ); + + const int cameraId = 98; + + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => [0, 1, 2]); + + // Test off. + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.off, + ); + + VerificationResult verificationResult = verify( + mockCamera2CameraControl.addCaptureRequestOptions(captureAny), + ); + CaptureRequestOptions capturedCaptureRequestOptions = + verificationResult.captured.single as CaptureRequestOptions; + expect( + await capturedCaptureRequestOptions.getCaptureRequestOption( + controlVideoStabilizationModeKey, + ), + equals(0), + ); + + clearInteractions(mockCamera2CameraControl); + + // Test level1. + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level1, + ); + + verificationResult = verify( + mockCamera2CameraControl.addCaptureRequestOptions(captureAny), + ); + capturedCaptureRequestOptions = + verificationResult.captured.single as CaptureRequestOptions; + expect( + await capturedCaptureRequestOptions.getCaptureRequestOption( + controlVideoStabilizationModeKey, + ), + equals(1), + ); + }); + + test( + 'setVideoStabilizationMode throws ArgumentError when mode not available', + () async { + // Set up mocks and constants. + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraControl mockCamera2CameraControl = + MockCamera2CameraControl(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = + MockCamera2CameraInfo(); + final PigeonInstanceManager testInstanceManager = PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CaptureRequestKey controlVideoStabilizationModeKey = + CaptureRequestKey.pigeon_detached( + pigeon_instanceManager: testInstanceManager, + ); + + camera.proxy = CameraXProxy( + fromCamera2CameraControl: + ({ + required CameraControl cameraControl, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => cameraControl == mockCameraControl + ? mockCamera2CameraControl + : Camera2CameraControl.pigeon_detached( + pigeon_instanceManager: testInstanceManager, + ), + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => mockCamera2CameraInfo, + + newCaptureRequestOptions: + ({ + required Map options, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockCaptureRequestOptions mockCaptureRequestOptions = + MockCaptureRequestOptions(); + options.forEach((CaptureRequestKey key, Object? value) { + when( + mockCaptureRequestOptions.getCaptureRequestOption(key), + ).thenAnswer((_) async => value); + }); + return mockCaptureRequestOptions; + }, + + controlVideoStabilizationModeRequest: () => + controlVideoStabilizationModeKey, + controlAvailableVideoStabilizationModesCameraCharacteristics: () { + return MockCameraCharacteristicsKey(); + }, + ); + + const int cameraId = 102; + + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => [0]); + + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level1, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + }, + ); + + test( + 'setVideoStabilizationMode throws ArgumentError when mode not mapped', + () async { + // Set up mocks and constants. + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockCameraControl mockCameraControl = MockCameraControl(); + final MockCamera2CameraControl mockCamera2CameraControl = + MockCamera2CameraControl(); + + // Set directly for test versus calling createCamera. + camera.cameraControl = mockCameraControl; + camera.cameraInfo = MockCameraInfo(); + final MockCamera2CameraInfo mockCamera2CameraInfo = + MockCamera2CameraInfo(); + final PigeonInstanceManager testInstanceManager = PigeonInstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CaptureRequestKey controlVideoStabilizationModeKey = + CaptureRequestKey.pigeon_detached( + pigeon_instanceManager: testInstanceManager, + ); + + camera.proxy = CameraXProxy( + fromCamera2CameraControl: + ({ + required CameraControl cameraControl, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => cameraControl == mockCameraControl + ? mockCamera2CameraControl + : Camera2CameraControl.pigeon_detached( + pigeon_instanceManager: testInstanceManager, + ), + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => mockCamera2CameraInfo, + + newCaptureRequestOptions: + ({ + required Map options, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockCaptureRequestOptions mockCaptureRequestOptions = + MockCaptureRequestOptions(); + options.forEach((CaptureRequestKey key, Object? value) { + when( + mockCaptureRequestOptions.getCaptureRequestOption(key), + ).thenAnswer((_) async => value); + }); + return mockCaptureRequestOptions; + }, + + controlVideoStabilizationModeRequest: () => + controlVideoStabilizationModeKey, + controlAvailableVideoStabilizationModesCameraCharacteristics: () { + return MockCameraCharacteristicsKey(); + }, + ); + + const int cameraId = 102; + + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => [0, 1, 2]); + + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level2, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level3, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + }, + ); + + test('getVideoStabilizationMode returns no available mode', () async { + // Set up mocks and constants. + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + // Set directly for test versus calling createCamera. + camera.cameraInfo = MockCameraInfo(); + camera.proxy = CameraXProxy( + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => mockCamera2CameraInfo, + + newCaptureRequestOptions: + ({ + required Map options, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockCaptureRequestOptions mockCaptureRequestOptions = + MockCaptureRequestOptions(); + options.forEach((CaptureRequestKey key, Object? value) { + when( + mockCaptureRequestOptions.getCaptureRequestOption(key), + ).thenAnswer((_) async => value); + }); + return mockCaptureRequestOptions; + }, + + controlAvailableVideoStabilizationModesCameraCharacteristics: () { + return MockCameraCharacteristicsKey(); + }, + ); + + const int cameraId = 103; + + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => []); + + final Iterable modes = await camera + .getSupportedVideoStabilizationModes(cameraId); + + expect(modes, orderedEquals([])); + }); + + test('getVideoStabilizationMode returns all available modes', () async { + // Set up mocks and constants. + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + final MockCamera2CameraInfo mockCamera2CameraInfo = MockCamera2CameraInfo(); + + // Set directly for test versus calling createCamera. + camera.cameraInfo = MockCameraInfo(); + camera.proxy = CameraXProxy( + fromCamera2CameraInfo: + ({ + required CameraInfo cameraInfo, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) => mockCamera2CameraInfo, + + newCaptureRequestOptions: + ({ + required Map options, + // ignore: non_constant_identifier_names + BinaryMessenger? pigeon_binaryMessenger, + // ignore: non_constant_identifier_names + PigeonInstanceManager? pigeon_instanceManager, + }) { + final MockCaptureRequestOptions mockCaptureRequestOptions = + MockCaptureRequestOptions(); + options.forEach((CaptureRequestKey key, Object? value) { + when( + mockCaptureRequestOptions.getCaptureRequestOption(key), + ).thenAnswer((_) async => value); + }); + return mockCaptureRequestOptions; + }, + + controlAvailableVideoStabilizationModesCameraCharacteristics: () { + return MockCameraCharacteristicsKey(); + }, + ); + + const int cameraId = 104; + + when( + mockCamera2CameraInfo.getCameraCharacteristic(any), + ).thenAnswer((_) async => [0, 1, 2]); + + final Iterable modes = await camera + .getSupportedVideoStabilizationModes(cameraId); + + expect( + modes, + orderedEquals([ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ]), + ); + }); + test('Should report support for image streaming', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); expect(camera.supportsImageStreaming(), true); 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 1af9e9207f7..154ac2ca702 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 @@ -2179,6 +2179,21 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { ) as _i2.CaptureRequestKey Function()); + @override + _i2.CaptureRequestKey Function() get controlVideoStabilizationModeRequest => + (super.noSuchMethod( + Invocation.getter(#controlVideoStabilizationModeRequest), + returnValue: () => _FakeCaptureRequestKey_34( + this, + Invocation.getter(#controlVideoStabilizationModeRequest), + ), + returnValueForMissingStub: () => _FakeCaptureRequestKey_34( + this, + Invocation.getter(#controlVideoStabilizationModeRequest), + ), + ) + as _i2.CaptureRequestKey Function()); + @override _i2.CameraCharacteristicsKey Function() get infoSupportedHardwareLevelCameraCharacteristics => @@ -2215,6 +2230,64 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { ) as _i2.CameraCharacteristicsKey Function()); + @override + _i2.CameraCharacteristicsKey Function() + get controlAvailableVideoStabilizationModesCameraCharacteristics => + (super.noSuchMethod( + Invocation.getter( + #controlAvailableVideoStabilizationModesCameraCharacteristics, + ), + returnValue: () => _FakeCameraCharacteristicsKey_9( + this, + Invocation.getter( + #controlAvailableVideoStabilizationModesCameraCharacteristics, + ), + ), + returnValueForMissingStub: () => _FakeCameraCharacteristicsKey_9( + this, + Invocation.getter( + #controlAvailableVideoStabilizationModesCameraCharacteristics, + ), + ), + ) + as _i2.CameraCharacteristicsKey Function()); + + @override + _i5.Future<_i9.Uint8List?> Function( + int, + int, + List<_i2.PlaneProxy>, { + _i8.BinaryMessenger? pigeon_binaryMessenger, + _i2.PigeonInstanceManager? pigeon_instanceManager, + }) + get getNv21BufferImageProxyUtils => + (super.noSuchMethod( + Invocation.getter(#getNv21BufferImageProxyUtils), + returnValue: + ( + int imageWidth, + int imageHeight, + List<_i2.PlaneProxy> planes, { + _i8.BinaryMessenger? pigeon_binaryMessenger, + _i2.PigeonInstanceManager? pigeon_instanceManager, + }) => _i5.Future<_i9.Uint8List?>.value(), + returnValueForMissingStub: + ( + int imageWidth, + int imageHeight, + List<_i2.PlaneProxy> planes, { + _i8.BinaryMessenger? pigeon_binaryMessenger, + _i2.PigeonInstanceManager? pigeon_instanceManager, + }) => _i5.Future<_i9.Uint8List?>.value(), + ) + as _i5.Future<_i9.Uint8List?> Function( + int, + int, + List<_i2.PlaneProxy>, { + _i8.BinaryMessenger? pigeon_binaryMessenger, + _i2.PigeonInstanceManager? pigeon_instanceManager, + })); + @override set withModeFocusMeteringActionBuilder( _i2.FocusMeteringActionBuilder Function({ @@ -2286,6 +2359,17 @@ class MockCameraXProxy extends _i1.Mock implements _i7.CameraXProxy { ), returnValueForMissingStub: null, ); + + @override + set controlVideoStabilizationModeRequest( + _i2.CaptureRequestKey Function()? _controlVideoStabilizationModeRequest, + ) => super.noSuchMethod( + Invocation.setter( + #controlVideoStabilizationModeRequest, + _controlVideoStabilizationModeRequest, + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [CaptureRequestOptions]. diff --git a/packages/camera/camera_avfoundation/AUTHORS b/packages/camera/camera_avfoundation/AUTHORS index 493a0b4ef9c..605414ab7dc 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 diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 797d053b5e2..a9bb0de8858 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.21+4 * Migrates `updateOrientation` and `setCaptureSessionPreset` methods to Swift. diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 8cf022f6ce0..d883223fe62 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -340,8 +340,6 @@ 03BB766E2665316900CE5A93 /* PBXTargetDependency */, ); name = RunnerTests; - packageProductDependencies = ( - ); productName = camera_exampleTests; productReference = 03BB76682665316900CE5A93 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -401,7 +399,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -879,7 +877,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ba0c5508103..82957b8b24a 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Void)?) -> Void)? var startImageStreamStub: ((FlutterBinaryMessenger, (FlutterError?) -> Void) -> Void)? var stopImageStreamStub: (() -> Void)? - + var setVideoStabilizationModeStub: ((FCPPlatformVideoStabilizationMode, (FlutterError?) -> Void) -> Void)? + var getIsVideoStabilizationModeSupportedStub: ((FCPPlatformVideoStabilizationMode) -> Bool)? + + var dartAPI: FCPCameraEventApi? { get { preconditionFailure("Attempted to access unimplemented property: dartAPI") @@ -185,6 +188,14 @@ final class MockCamera: NSObject, Camera { resumePreviewStub?() } + func setVideoStabilizationMode(_ mode: FCPPlatformVideoStabilizationMode, withCompletion: @escaping (FlutterError?) -> Void) { + setVideoStabilizationModeStub?(mode, withCompletion) + } + + func isVideoStabilizationModeSupported(_ mode: FCPPlatformVideoStabilizationMode) -> Bool { + return getIsVideoStabilizationModeSupportedStub?(mode) ?? false + } + func setDescriptionWhileRecording( _ cameraName: String, withCompletion completion: @escaping (FlutterError?) -> Void diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.swift index f65e0ce3c21..ba7c85d1377 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.swift @@ -26,4 +26,6 @@ final class MockCaptureConnection: NSObject, FLTCaptureConnection { var inputPorts: [AVCaptureInput.Port] = [] var isVideoMirroringSupported = false var isVideoOrientationSupported = false + var preferredVideoStabilizationMode = AVCaptureVideoStabilizationMode.off + } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift index b294b1e3e1c..04c98525031 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift @@ -112,6 +112,11 @@ class MockCaptureDevice: NSObject, FLTCaptureDevice { func iso() -> Float { return 0 } + + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) -> Bool { + return false + } + func lockForConfiguration() throws { try lockForConfigurationStub?() diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index 7aa5ff10832..30cc4ab18d1 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -29,3 +29,8 @@ dev_dependencies: flutter: uses-material-design: true + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_avfoundation: {path: ../../../camera/camera_avfoundation}, camera_platform_interface: {path: ../../../camera/camera_platform_interface}} \ No newline at end of file diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift index 72eb13b8102..c789b9a23a6 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/Camera.swift @@ -101,6 +101,10 @@ protocol Camera: FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate, ) func setZoomLevel(_ zoom: CGFloat, withCompletion: @escaping (_ error: FlutterError?) -> Void) + + func setVideoStabilizationMode(_ mode: FCPPlatformVideoStabilizationMode, withCompletion: @escaping (_ error: FlutterError?) -> Void) + + func isVideoStabilizationModeSupported(_ mode: FCPPlatformVideoStabilizationMode) -> Bool func setFlashMode( _ mode: FCPPlatformFlashMode, diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift index a6029e5436c..86c9ad2cfcd 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.swift @@ -497,6 +497,23 @@ extension CameraPlugin: FCPCameraApi { self?.camera?.setZoomLevel(zoom, withCompletion: completion) } } + + public func setVideoStabilizationMode(_ mode: FCPPlatformVideoStabilizationMode, completion: @escaping (FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + self?.camera?.setVideoStabilizationMode(mode, withCompletion: completion) + } + } + + public func isVideoStabilizationModeSupported(_ mode: FCPPlatformVideoStabilizationMode, completion: @escaping (NSNumber?, FlutterError?) -> Void) { + captureSessionQueue.async { [weak self] in + + if let isSupported = self?.camera?.isVideoStabilizationModeSupported(mode) { + completion(NSNumber(value: isSupported), nil) + } else { + completion(nil, nil) + } + } + } public func pausePreview(completion: @escaping (FlutterError?) -> Void) { captureSessionQueue.async { [weak self] in diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift index fb3b6a86d63..28746df083d 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/DefaultCamera.swift @@ -951,6 +951,30 @@ final class DefaultCamera: NSObject, Camera { completion(nil) } + func setVideoStabilizationMode(_ mode: FCPPlatformVideoStabilizationMode, withCompletion completion: @escaping (FlutterError?) -> Void) { + let stabilizationMode = getAvCaptureVideoStabilizationMode(mode) + + guard captureDevice.isVideoStabilizationModeSupported(stabilizationMode) else { + completion( + FlutterError( + code: "VIDEO_STABILIZATION_ERROR", + message: "Unavailable video stabilization mode.", + details: nil + ) + ) + return + } + if let connection = captureVideoOutput.connection(withMediaType: .video) { + connection.preferredVideoStabilizationMode = stabilizationMode + } + completion(nil) + } + + func isVideoStabilizationModeSupported(_ mode: FCPPlatformVideoStabilizationMode) -> Bool { + let stabilizationMode = getAvCaptureVideoStabilizationMode(mode) + return captureDevice.isVideoStabilizationModeSupported(stabilizationMode) + } + func setFlashMode( _ mode: FCPPlatformFlashMode, withCompletion completion: @escaping (FlutterError?) -> Void diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraProperties.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraProperties.m index e16f238829b..13d909e84f5 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraProperties.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/CameraProperties.m @@ -55,3 +55,24 @@ OSType FCPGetPixelFormatForPigeonFormat(FCPPlatformImageFormatGroup imageFormat) return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; } } + +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; + } +} diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureConnection.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureConnection.m index 3a3eef81caf..6f52dc94c70 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureConnection.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureConnection.m @@ -46,4 +46,13 @@ - (AVCaptureVideoOrientation)videoOrientation { return self.connection.inputPorts; } +- (void)setPreferredVideoStabilizationMode: + (AVCaptureVideoStabilizationMode)preferredVideoStabilizationMode { + self.connection.preferredVideoStabilizationMode = preferredVideoStabilizationMode; +} + +- (AVCaptureVideoStabilizationMode)preferredVideoStabilizationMode { + return self.connection.preferredVideoStabilizationMode; +} + @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureDevice.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureDevice.m index 68f486f91f4..7da69cd657b 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureDevice.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/FLTCaptureDevice.m @@ -139,6 +139,11 @@ - (void)setVideoZoomFactor:(CGFloat)factor { self.device.videoZoomFactor = factor; } +// Video Stabilization +- (BOOL)isVideoStabilizationModeSupported:(AVCaptureVideoStabilizationMode)videoStabilizationMode { + return [self.device.activeFormat isVideoStabilizationModeSupported:videoStabilizationMode]; +} + // Camera Properties - (float)lensAperture { return self.device.lensAperture; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraProperties.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraProperties.h index 6645cf7bb06..9f1ae13ce16 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraProperties.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/CameraProperties.h @@ -25,4 +25,7 @@ extern FCPPlatformDeviceOrientation FCPGetPigeonDeviceOrientationForOrientation( /// Gets VideoFormat from its Pigeon representation. extern OSType FCPGetPixelFormatForPigeonFormat(FCPPlatformImageFormatGroup imageFormat); +extern AVCaptureVideoStabilizationMode getAvCaptureVideoStabilizationMode( + FCPPlatformVideoStabilizationMode videoStabilizationMode); + NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureConnection.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureConnection.h index 3fd5d1c084e..98c60dfb2a3 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureConnection.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureConnection.h @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN /// Corresponds to the `supportsVideoOrientation` property of `AVCaptureConnection` @property(nonatomic, readonly, getter=isVideoOrientationSupported) BOOL supportsVideoOrientation; +/// Corresponds to the `preferredVideoStabilizationMode` property of `AVCaptureConnection` +@property(nonatomic) AVCaptureVideoStabilizationMode preferredVideoStabilizationMode; + @end /// A default implementation of the `FLTCaptureConnection` protocol. It wraps an instance diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureDevice.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureDevice.h index 3244e70fc73..a38929e056e 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureDevice.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/FLTCaptureDevice.h @@ -57,6 +57,9 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) CGFloat minAvailableVideoZoomFactor; @property(nonatomic) CGFloat videoZoomFactor; +// Video Stabilization +- (BOOL)isVideoStabilizationModeSupported:(AVCaptureVideoStabilizationMode)videoStabilizationMode; + // Camera Properties - (float)lensAperture; - (CMTime)exposureDuration; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/messages.g.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/messages.g.h index 23e4fb77246..89ecd3f839e 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/messages.g.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/include/camera_avfoundation/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), 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,6 +276,13 @@ NSObject *FCPGetMessagesCodec(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. diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/messages.g.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/messages.g.m index f0543adc6c4..f9f455217e4 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/messages.g.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation_objc/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), 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; @@ -356,15 +366,21 @@ - (nullable id)readValueOfType:(UInt8)type { : [[FCPPlatformResolutionPresetBox alloc] initWithValue:[enumAsNumber integerValue]]; } - case 137: - return [FCPPlatformCameraDescription fromList:[self readValue]]; + case 137: { + NSNumber *enumAsNumber = [self readValue]; + return enumAsNumber == nil ? nil + : [[FCPPlatformVideoStabilizationModeBox alloc] + initWithValue:[enumAsNumber integerValue]]; + } case 138: - return [FCPPlatformCameraState fromList:[self readValue]]; + return [FCPPlatformCameraDescription fromList:[self readValue]]; case 139: - return [FCPPlatformMediaSettings fromList:[self readValue]]; + return [FCPPlatformCameraState fromList:[self readValue]]; case 140: - return [FCPPlatformPoint fromList:[self readValue]]; + return [FCPPlatformMediaSettings fromList:[self readValue]]; case 141: + return [FCPPlatformPoint fromList:[self readValue]]; + case 142: return [FCPPlatformSize fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -408,20 +424,24 @@ - (void)writeValue:(id)value { FCPPlatformResolutionPresetBox *box = (FCPPlatformResolutionPresetBox *)value; [self writeByte:136]; [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; - } else if ([value isKindOfClass:[FCPPlatformCameraDescription class]]) { + } else if ([value isKindOfClass:[FCPPlatformVideoStabilizationModeBox class]]) { + FCPPlatformVideoStabilizationModeBox *box = (FCPPlatformVideoStabilizationModeBox *)value; [self writeByte:137]; + [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; + } else if ([value isKindOfClass:[FCPPlatformCameraDescription class]]) { + [self writeByte:138]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformCameraState class]]) { - [self writeByte:138]; + [self writeByte:139]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformMediaSettings class]]) { - [self writeByte:139]; + [self writeByte:140]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformPoint class]]) { - [self writeByte:140]; + [self writeByte:141]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformSize class]]) { - [self writeByte:141]; + [self writeByte:142]; [self writeValue:[value toList]]; } else { [super writeValue:value]; @@ -1116,6 +1136,63 @@ 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:FCPGetMessagesCodec()]; + 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; + FCPPlatformVideoStabilizationModeBox *boxedFCPPlatformVideoStabilizationMode = + GetNullableObjectAtIndex(args, 0); + FCPPlatformVideoStabilizationMode arg_mode = boxedFCPPlatformVideoStabilizationMode.value; + [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:FCPGetMessagesCodec()]; + 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; + FCPPlatformVideoStabilizationModeBox *boxedFCPPlatformVideoStabilizationMode = + GetNullableObjectAtIndex(args, 0); + FCPPlatformVideoStabilizationMode arg_mode = boxedFCPPlatformVideoStabilizationMode.value; + [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] diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index 188062f9970..e4665b0b31c 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -380,6 +380,52 @@ class AVFoundationCamera extends CameraPlatform { } } + @override + Future setVideoStabilizationMode( + int cameraId, + VideoStabilizationMode mode, + ) async { + try { + final Map + availableModes = await _getSupportedVideoStabilizationModeMap(cameraId); + + final PlatformVideoStabilizationMode? platformMode = availableModes[mode]; + if (platformMode == null) { + throw ArgumentError('Unavailable video stabilization mode.', 'mode'); + } + await _hostApi.setVideoStabilizationMode(platformMode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future> getSupportedVideoStabilizationModes( + int cameraId, + ) async { + return (await _getSupportedVideoStabilizationModeMap(cameraId)).keys; + } + + Future> + _getSupportedVideoStabilizationModeMap(int cameraId) async { + final Map ret = + {}; + + for (final VideoStabilizationMode mode in VideoStabilizationMode.values) { + final PlatformVideoStabilizationMode? platformMode = + _pigeonVideoStabilizationMode(mode); + if (platformMode != null) { + final bool isSupported = await _hostApi + .isVideoStabilizationModeSupported(platformMode); + if (isSupported) { + ret[mode] = platformMode; + } + } + } + + return ret; + } + @override Future pausePreview(int cameraId) async { await _hostApi.pausePreview(); @@ -494,6 +540,29 @@ 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.level1: + return PlatformVideoStabilizationMode.standard; + case VideoStabilizationMode.level2: + return PlatformVideoStabilizationMode.cinematic; + case VideoStabilizationMode.level3: + 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 f771611beab..77519e0654f 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 // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), 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 @@ -63,6 +63,13 @@ enum PlatformImageFormatGroup { bgra8888, yuv420 } enum PlatformResolutionPreset { low, medium, high, veryHigh, ultraHigh, max } +enum PlatformVideoStabilizationMode { + off, + standard, + cinematic, + cinematicExtended, +} + class PlatformCameraDescription { PlatformCameraDescription({required this.name, required this.lensDirection}); @@ -240,20 +247,23 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformResolutionPreset) { buffer.putUint8(136); writeValue(buffer, value.index); - } else if (value is PlatformCameraDescription) { + } else if (value is PlatformVideoStabilizationMode) { buffer.putUint8(137); + writeValue(buffer, value.index); + } else if (value is PlatformCameraDescription) { + buffer.putUint8(138); writeValue(buffer, value.encode()); } else if (value is PlatformCameraState) { - buffer.putUint8(138); + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is PlatformMediaSettings) { - buffer.putUint8(139); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is PlatformPoint) { - buffer.putUint8(140); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is PlatformSize) { - buffer.putUint8(141); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -288,14 +298,19 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : PlatformResolutionPreset.values[value]; case 137: - return PlatformCameraDescription.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null + ? null + : PlatformVideoStabilizationMode.values[value]; case 138: - return PlatformCameraState.decode(readValue(buffer)!); + return PlatformCameraDescription.decode(readValue(buffer)!); case 139: - return PlatformMediaSettings.decode(readValue(buffer)!); + return PlatformCameraState.decode(readValue(buffer)!); case 140: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMediaSettings.decode(readValue(buffer)!); case 141: + return PlatformPoint.decode(readValue(buffer)!); + case 142: return PlatformSize.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1029,6 +1044,65 @@ class CameraApi { } } + /// Sets the video stabilization mode. + Future setVideoStabilizationMode( + PlatformVideoStabilizationMode mode, + ) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.setVideoStabilizationMode$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([mode]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Sets the video stabilization mode. + Future isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode mode, + ) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.camera_avfoundation.CameraApi.isVideoStabilizationModeSupported$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([mode]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + /// Pauses streaming of preview frames. Future pausePreview() async { final String pigeonVar_channelName = diff --git a/packages/camera/camera_avfoundation/pigeons/messages.dart b/packages/camera/camera_avfoundation/pigeons/messages.dart index 4427fa28567..2f83feaeb98 100644 --- a/packages/camera/camera_avfoundation/pigeons/messages.dart +++ b/packages/camera/camera_avfoundation/pigeons/messages.dart @@ -56,6 +56,13 @@ enum PlatformImageFormatGroup { bgra8888, yuv420 } // Pigeon version of ResolutionPreset. enum PlatformResolutionPreset { low, medium, high, veryHigh, ultraHigh, max } +enum PlatformVideoStabilizationMode { + off, + standard, + cinematic, + cinematicExtended, +} + // Pigeon version of CameraDescription. class PlatformCameraDescription { PlatformCameraDescription({required this.name, required this.lensDirection}); @@ -263,6 +270,16 @@ 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(); diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 4603ead6659..c85002547a8 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.21+4 +version: 0.10.0 environment: sdk: ^3.9.0 @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.9.0 + camera_platform_interface: ^2.12.0 flutter: sdk: flutter stream_transform: ^2.0.0 @@ -33,3 +33,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 6a504422899..eb23348ad14 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -716,6 +716,224 @@ void main() { }, ); + test('Should set video stabilization mode to off', () async { + when( + mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.off, + ), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.off, + ); + + verify( + mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.off), + ); + }); + + test('Should set video stabilization mode to level1', () async { + when( + mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.standard, + ), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level1, + ); + + verify( + mockApi.setVideoStabilizationMode( + PlatformVideoStabilizationMode.standard, + ), + ); + }); + + test('Should set video stabilization mode to cinematic', () async { + when( + mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.cinematic, + ), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level2, + ); + + verify( + mockApi.setVideoStabilizationMode( + PlatformVideoStabilizationMode.cinematic, + ), + ); + }); + + test('Should set video stabilization mode to cinematicExtended', () async { + when( + mockApi.isVideoStabilizationModeSupported( + PlatformVideoStabilizationMode.cinematicExtended, + ), + ).thenAnswer((_) async => true); + + await camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level3, + ); + + 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 + .getSupportedVideoStabilizationModes(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.getSupportedVideoStabilizationModes(cameraId)).toList(); + + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + ]); + }); + + test('Should get all video stabilization modes', () async { + when( + mockApi.isVideoStabilizationModeSupported(any), + ).thenAnswer((_) async => true); + + final List modes = + (await camera.getSupportedVideoStabilizationModes(cameraId)).toList(); + + expect(modes, [ + VideoStabilizationMode.off, + VideoStabilizationMode.level1, + VideoStabilizationMode.level2, + VideoStabilizationMode.level3, + ]); + }); + + test( + 'Should throw ArgumentError when unavailable video stabilization mode is set', + () async { + when( + mockApi.isVideoStabilizationModeSupported(any), + ).thenAnswer((_) async => false); + + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.off, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level1, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level2, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + expect( + () => camera.setVideoStabilizationMode( + cameraId, + VideoStabilizationMode.level3, + ), + throwsA( + isA().having( + (ArgumentError e) => e.name, + 'name', + 'mode', + ), + ), + ); + }, + ); + + test( + 'Should throw CameraException when illegal zoom level is supplied', + () async { + const String code = 'ZOOM_ERROR'; + const String message = 'Illegal zoom error'; + when(mockApi.setZoomLevel(any)).thenAnswer( + (_) async => throw PlatformException(code: code, message: message), + ); + + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA( + isA() + .having((CameraException e) => e.code, 'code', code) + .having( + (CameraException e) => e.description, + 'description', + message, + ), + ), + ); + }, + ); + test('Should lock the capture orientation', () async { await camera.lockCaptureOrientation( cameraId, 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 0d09546d2ae..225eac9931c 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in camera_avfoundation/test/avfoundation_camera_test.dart. // Do not manually edit this file. @@ -17,6 +17,7 @@ import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types @@ -302,6 +303,28 @@ class MockCameraApi extends _i1.Mock implements _i2.CameraApi { ) as _i4.Future); + @override + _i4.Future setVideoStabilizationMode( + _i2.PlatformVideoStabilizationMode? mode, + ) => + (super.noSuchMethod( + Invocation.method(#setVideoStabilizationMode, [mode]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + + @override + _i4.Future isVideoStabilizationModeSupported( + _i2.PlatformVideoStabilizationMode? mode, + ) => + (super.noSuchMethod( + Invocation.method(#isVideoStabilizationModeSupported, [mode]), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) + as _i4.Future); + @override _i4.Future pausePreview() => (super.noSuchMethod( diff --git a/packages/camera/camera_platform_interface/AUTHORS b/packages/camera/camera_platform_interface/AUTHORS index 0d1bfa6a90c..d21929d105f 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 6c90a8a5c3e..38927ae325c 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.12.0 + +* Adds support for video stabilization. + ## 2.11.0 * Adds a flag to configure a recording to be persistent across camera changes. See @@ -20,6 +24,7 @@ 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/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 6fc4f8e17b5..0a3b7709092 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 @@ -281,6 +281,34 @@ 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. + Future> getSupportedVideoStabilizationModes( + int cameraId, + ) async => []; + + /// Sets the video stabilization mode for the selected camera. + Future setVideoStabilizationMode( + int cameraId, + VideoStabilizationMode mode, + ) async { + throw UnimplementedError('setVideoStabilizationMode() is not implemented.'); + } + + /// Gets the fallback mode of video stabilization [mode]. + /// + /// This method returns the video stabilization mode that [setVideoStabilizationMode] + /// should set when the device does not support the given [mode]. + static VideoStabilizationMode? getFallbackVideoStabilizationMode( + VideoStabilizationMode mode, + ) { + return switch (mode) { + VideoStabilizationMode.off => null, + VideoStabilizationMode.level1 => VideoStabilizationMode.off, + VideoStabilizationMode.level2 => VideoStabilizationMode.level1, + VideoStabilizationMode.level3 => VideoStabilizationMode.level2, + }; + } + /// 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 2bdc8407e6d..5f51756490e 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 00000000000..5077b588c09 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/video_stabilization_mode.dart @@ -0,0 +1,18 @@ +// 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, + + /// Least stabilized video stabilization mode with the least latency. + level1, + + /// More stabilized video with more latency. + level2, + + /// Most stabilized video with the most latency. + level3, +} diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index ec310739676..f3e45edb835 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.11.0 +version: 2.12.0 environment: sdk: ^3.7.0 diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index d664c7d7283..f4114427c45 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -531,6 +531,42 @@ void main() { expect(cameraPlatform.supportsImageStreaming(), false); }, ); + + test('getFallbackViewStabilizationMode returns level2 for mode level3', () { + final VideoStabilizationMode? fallbackMode = + CameraPlatform.getFallbackVideoStabilizationMode( + VideoStabilizationMode.level3, + ); + + expect(fallbackMode, VideoStabilizationMode.level2); + }); + + test('getFallbackViewStabilizationMode returns level1 for mode level2', () { + final VideoStabilizationMode? fallbackMode = + CameraPlatform.getFallbackVideoStabilizationMode( + VideoStabilizationMode.level2, + ); + + expect(fallbackMode, VideoStabilizationMode.level1); + }); + + test('getFallbackViewStabilizationMode returns off for mode level1', () { + final VideoStabilizationMode? fallbackMode = + CameraPlatform.getFallbackVideoStabilizationMode( + VideoStabilizationMode.level1, + ); + + expect(fallbackMode, VideoStabilizationMode.off); + }); + + test('getFallbackViewStabilizationMode returns null for mode off', () { + final VideoStabilizationMode? fallbackMode = + CameraPlatform.getFallbackVideoStabilizationMode( + VideoStabilizationMode.off, + ); + + expect(fallbackMode, null); + }); }); group('exports', () { 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 00000000000..49bc323d637 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/video_stabilization_mode_test.dart @@ -0,0 +1,23 @@ +// 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 4 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.level1); + expect(values[2], VideoStabilizationMode.level2); + expect(values[3], VideoStabilizationMode.level3); + }); +} diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index c32cfb084b0..b17be83dee6 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -31,3 +31,8 @@ dev_dependencies: topics: - camera + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} \ No newline at end of file diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index d064fa093a0..2e61369b5e1 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -34,3 +34,9 @@ dev_dependencies: topics: - camera + + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + {camera_platform_interface: {path: ../../camera/camera_platform_interface}} \ No newline at end of file