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 cdb2bb789e4..6f3223af2ac 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.22+4 * Migrates `FLTCameraDeviceDiscovering` and `FLTDeviceOrientationProviding` classes 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 { @@ -185,6 +188,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 f65e0ce3c21..4a1d7f1ca76 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,5 @@ 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 64ab36e43e0..e28f76c54b2 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.swift @@ -114,6 +114,12 @@ class MockCaptureDevice: NSObject, FLTCaptureDevice { return 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 72eb13b8102..5a0b93f4e2a 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 577c886ed57..929d1fc4c78 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 @@ -517,6 +517,28 @@ 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 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 self?.camera?.pausePreview() 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 276fd1897d8..7630a150c2d 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 @@ -950,6 +950,33 @@ 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(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/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 35ccd9790bb..ca8c34abd8b 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 @@ -143,6 +143,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 0d18df9ed9c..886559aa829 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 @@ -60,6 +60,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 782d3df866f..1cf9a0d2b14 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; +/// 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 0a86f9e9f96..8edcabda2d6 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]; + } + } + /// 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..02f8708e95b 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 [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 50fe099314e..99e2f61299d 100644 --- a/packages/camera/camera_avfoundation/lib/src/messages.g.dart +++ b/packages/camera/camera_avfoundation/lib/src/messages.g.dart @@ -77,6 +77,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, @@ -265,20 +272,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); @@ -316,14 +326,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); @@ -1057,6 +1072,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 f4bdd8d998d..c41dbea6cf1 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); + + /// 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 24f11c1bace..5b3ee3d5fda 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.22+4 +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 ec31faf9c27..8958aac192f 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -722,6 +722,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(