From c01d1d7cc13a97b0baf63c08666f944be0e43d0b Mon Sep 17 00:00:00 2001 From: Rui Craveiro Date: Wed, 5 Nov 2025 13:35:10 +0000 Subject: [PATCH] [camera_avfoundation] Adds support for video stabilization - Implements getSupportedVideoStabilizationModes() and setVideoStabilizationMode() methods in AVFoundationCamera. --- packages/camera/camera_avfoundation/AUTHORS | 1 + .../camera/camera_avfoundation/CHANGELOG.md | 6 + .../ios/RunnerTests/Mocks/MockCamera.swift | 13 ++ .../Mocks/MockCaptureConnection.swift | 1 + .../RunnerTests/Mocks/MockCaptureDevice.swift | 6 + .../Sources/camera_avfoundation/Camera.swift | 6 + .../camera_avfoundation/CameraPlugin.swift | 23 ++ .../CameraProperties.swift | 24 ++ .../CaptureConnection.swift | 4 + .../camera_avfoundation/CaptureDevice.swift | 11 + .../camera_avfoundation/DefaultCamera.swift | 29 +++ .../include/camera_avfoundation/messages.g.h | 20 ++ .../camera_avfoundation_objc/messages.g.m | 97 +++++++- .../lib/src/avfoundation_camera.dart | 68 ++++++ .../lib/src/messages.g.dart | 98 +++++++- .../camera_avfoundation/pigeons/messages.dart | 17 ++ .../camera/camera_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_camera_test.dart | 218 ++++++++++++++++++ .../test/avfoundation_camera_test.mocks.dart | 25 +- 19 files changed, 649 insertions(+), 22 deletions(-) diff --git a/packages/camera/camera_avfoundation/AUTHORS b/packages/camera/camera_avfoundation/AUTHORS index 493a0b4ef9c2..605414ab7dcf 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 f66df6476e7d..e29c14c0d286 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.10.0 + +* Adds video stabilization. + - Adds getSupportedVideoStabilizationModes method. + - Adds setVideoStabilizationMode method. + ## 0.9.23+2 * Code refactor related to Swift pigeon's generated struct MediaSettings being immutable. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift index b3803b119ea8..851760f84b7a 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCamera.swift @@ -44,6 +44,9 @@ final class MockCamera: NSObject, Camera { var setDescriptionWhileRecordingStub: ((String, ((FlutterError?) -> 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 { @@ -188,6 +191,16 @@ 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 1df85354da2d..aff13580ea75 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureConnection.swift @@ -28,4 +28,5 @@ final class MockCaptureConnection: NSObject, CaptureConnection { 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 6b690492f242..11e85d72f042 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift @@ -118,6 +118,12 @@ class MockCaptureDevice: NSObject, CaptureDevice { var iso: Float { 0 } + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) + -> Bool + { + return false + } + func lockForConfiguration() throws { try lockForConfigurationStub?() } 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 72eb13b81023..5a0b93f4e2a4 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 @@ -102,6 +102,12 @@ 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, withCompletion: @escaping (_ error: FlutterError?) -> Void 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 a998e08d9047..446f894042e8 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 @@ -514,6 +514,29 @@ extension CameraPlugin: FCPCameraApi { } } + 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 camera = self?.camera { + let isSupported = camera.isVideoStabilizationModeSupported(mode) + completion(NSNumber(value: isSupported), nil) + } else { + completion(nil, nil) + } + } + } + public func pausePreview(completion: @escaping (FlutterError?) -> Void) { captureSessionQueue.async { [weak self] in self?.camera?.pausePreview() diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift index fca5c0fef139..13fe6f30b819 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraProperties.swift @@ -81,3 +81,27 @@ func getPixelFormat(for imageFormat: FCPPlatformImageFormatGroup) -> OSType { return kCVPixelFormatType_32BGRA } } + +/// Gets video stabilization mode from its Pigeon representation. +/// videoStabilizationMode - the Pigeon video stabilization mode. +func getAvCaptureVideoStabilizationMode( + _ videoStabilizationMode: FCPPlatformVideoStabilizationMode +) -> AVCaptureVideoStabilizationMode { + + switch videoStabilizationMode { + case .off: + return .off + case .standard: + return .standard + case .cinematic: + return .cinematic + case .cinematicExtended: + if #available(iOS 13.0, *) { + return .cinematicExtended + } else { + return .cinematic + } + @unknown default: + return .off + } +} diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift index d63f71edf289..d119afb9474a 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureConnection.swift @@ -21,6 +21,10 @@ protocol CaptureConnection: NSObjectProtocol { /// Corresponds to the `supportsVideoOrientation` property of `AVCaptureConnection` var isVideoOrientationSupported: Bool { get } + + /// Corresponds to the preferredVideoStabilizationMode property of `AVCaptureConnection` + var preferredVideoStabilizationMode: AVCaptureVideoStabilizationMode { get set } + } extension AVCaptureConnection: CaptureConnection {} diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift index f24b53581a92..926242dbd317 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CaptureDevice.swift @@ -58,6 +58,10 @@ protocol CaptureDevice: NSObjectProtocol { var minAvailableVideoZoomFactor: CGFloat { get } var videoZoomFactor: CGFloat { get set } + // Video Stabilization + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) + -> Bool + // Camera Properties var lensAperture: Float { get } var exposureDuration: CMTime { get } @@ -97,6 +101,13 @@ extension AVCaptureDevice: CaptureDevice { } var flutterFormats: [CaptureDeviceFormat] { formats } + + func isVideoStabilizationModeSupported(_ videoStabilizationMode: AVCaptureVideoStabilizationMode) + -> Bool + { + return self.activeFormat.isVideoStabilizationModeSupported(videoStabilizationMode) + } + } extension AVCaptureInput: CaptureInput { 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 36b5e3cbfca3..15e732885b34 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 @@ -988,6 +988,35 @@ 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: [ + "requested_mode": stabilizationMode.rawValue + ] + ) + ) + return + } + if let connection = captureVideoOutput.connection(with: .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/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 7064a15caac7..ba60a1bedd4b 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 @@ -131,6 +131,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; @@ -283,6 +296,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; +/// Gets if the given video stabilization mode is supported. +- (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 64524e708c7d..630ef1e107ff 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 @@ -130,6 +130,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; @@ -377,15 +387,21 @@ - (nullable id)readValueOfType:(UInt8)type { : [[FCPPlatformResolutionPresetBox alloc] initWithValue:[enumAsNumber integerValue]]; } - case 138: - return [FCPPlatformCameraDescription fromList:[self readValue]]; + case 138: { + NSNumber *enumAsNumber = [self readValue]; + return enumAsNumber == nil ? nil + : [[FCPPlatformVideoStabilizationModeBox alloc] + initWithValue:[enumAsNumber integerValue]]; + } case 139: - return [FCPPlatformCameraState fromList:[self readValue]]; + return [FCPPlatformCameraDescription fromList:[self readValue]]; case 140: - return [FCPPlatformMediaSettings fromList:[self readValue]]; + return [FCPPlatformCameraState fromList:[self readValue]]; case 141: - return [FCPPlatformPoint fromList:[self readValue]]; + return [FCPPlatformMediaSettings fromList:[self readValue]]; case 142: + return [FCPPlatformPoint fromList:[self readValue]]; + case 143: return [FCPPlatformSize fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -433,20 +449,24 @@ - (void)writeValue:(id)value { FCPPlatformResolutionPresetBox *box = (FCPPlatformResolutionPresetBox *)value; [self writeByte:137]; [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:138]; + [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; + } else if ([value isKindOfClass:[FCPPlatformCameraDescription class]]) { + [self writeByte:139]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformCameraState class]]) { - [self writeByte:139]; + [self writeByte:140]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformMediaSettings class]]) { - [self writeByte:140]; + [self writeByte:141]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformPoint class]]) { - [self writeByte:141]; + [self writeByte:142]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FCPPlatformSize class]]) { - [self writeByte:142]; + [self writeByte:143]; [self writeValue:[value toList]]; } else { [super writeValue:value]; @@ -1141,6 +1161,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]; + } + } + /// Gets if the given video stabilization mode is supported. + { + 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 58afb3f18cc0..f87709caef69 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -380,6 +380,51 @@ 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 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 +539,29 @@ class AVFoundationCamera extends CameraPlatform { return PlatformResolutionPreset.max; } + /// Returns a [VideoStabilizationMode]'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 null; + } + /// 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 fd25e3be8bad..0f6694641112 100644 --- a/packages/camera/camera_avfoundation/lib/src/messages.g.dart +++ b/packages/camera/camera_avfoundation/lib/src/messages.g.dart @@ -95,6 +95,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, @@ -384,20 +391,23 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is PlatformResolutionPreset) { buffer.putUint8(137); writeValue(buffer, value.index); - } else if (value is PlatformCameraDescription) { + } else if (value is PlatformVideoStabilizationMode) { buffer.putUint8(138); + writeValue(buffer, value.index); + } else if (value is PlatformCameraDescription) { + buffer.putUint8(139); writeValue(buffer, value.encode()); } else if (value is PlatformCameraState) { - buffer.putUint8(139); + buffer.putUint8(140); writeValue(buffer, value.encode()); } else if (value is PlatformMediaSettings) { - buffer.putUint8(140); + buffer.putUint8(141); writeValue(buffer, value.encode()); } else if (value is PlatformPoint) { - buffer.putUint8(141); + buffer.putUint8(142); writeValue(buffer, value.encode()); } else if (value is PlatformSize) { - buffer.putUint8(142); + buffer.putUint8(143); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -435,14 +445,19 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : PlatformResolutionPreset.values[value]; case 138: - return PlatformCameraDescription.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null + ? null + : PlatformVideoStabilizationMode.values[value]; case 139: - return PlatformCameraState.decode(readValue(buffer)!); + return PlatformCameraDescription.decode(readValue(buffer)!); case 140: - return PlatformMediaSettings.decode(readValue(buffer)!); + return PlatformCameraState.decode(readValue(buffer)!); case 141: - return PlatformPoint.decode(readValue(buffer)!); + return PlatformMediaSettings.decode(readValue(buffer)!); case 142: + return PlatformPoint.decode(readValue(buffer)!); + case 143: return PlatformSize.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1224,6 +1239,71 @@ 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 Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mode], + ); + 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; + } + } + + /// Gets if the given video stabilization mode is supported. + 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 Future pigeonVar_sendFuture = pigeonVar_channel.send( + [mode], + ); + 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 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 f4bdd8d998da..bba2a51bd418 100644 --- a/packages/camera/camera_avfoundation/pigeons/messages.dart +++ b/packages/camera/camera_avfoundation/pigeons/messages.dart @@ -71,6 +71,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({ @@ -285,6 +292,16 @@ abstract class CameraApi { @ObjCSelector('setZoomLevel:') void setZoomLevel(double zoom); + /// Sets the video stabilization mode. + @async + @ObjCSelector('setVideoStabilizationMode:') + void setVideoStabilizationMode(PlatformVideoStabilizationMode mode); + + /// Gets if the given video stabilization mode is supported. + @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 1e87023aa7b0..0a27bc20a4d4 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.23+2 +version: 0.10.0 environment: sdk: ^3.9.0 @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.10.0 + camera_platform_interface: ^2.12.0 flutter: sdk: flutter stream_transform: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index fa47e376bc7e..53d7965c6bac 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -710,6 +710,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 code = 'ZOOM_ERROR'; + const 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 0d09546d2aed..225eac9931c6 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(