diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index d40aeca0e555..86f432a29337 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.18+2 + +* Refactors implementations to reduce usage of OCMock in internal testing. + ## 0.9.18+1 * Refactors implementations to reduce usage of OCMock in internal testing. 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 8dd444160cb9..0b28b4557b8a 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -19,10 +19,16 @@ 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 788A065927B0E02900533D74 /* StreamingTest.m */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 7D5FCCD42AEF9D0200FB7108 /* CameraSettingsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D5FCCD32AEF9D0200FB7108 /* CameraSettingsTests.m */; }; + 7F29EB222D269ED500740257 /* MockEventChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F29EB212D269ED500740257 /* MockEventChannel.m */; }; + 7F29EB292D26A59000740257 /* MockCameraDeviceDiscoverer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F29EB282D26A59000740257 /* MockCameraDeviceDiscoverer.m */; }; + 7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F29EB402D281C7E00740257 /* MockCaptureSession.m */; }; 7F56D0382D1EDDCE005676A5 /* CameraPermissionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */; }; - 7F87E8022D01FD6F00A3549C /* MockCaptureDeviceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F87E8012D01FD5600A3549C /* MockCaptureDeviceController.m */; }; - 7F87E80C2D0325D900A3549C /* MockDeviceOrientationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F87E80B2D0325D700A3549C /* MockDeviceOrientationProvider.m */; }; + 7F8FD2292D4BFABF001AF2C1 /* MockGlobalEventApi.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F8FD2282D4BFABF001AF2C1 /* MockGlobalEventApi.m */; }; + 7F8FD22C2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F8FD22B2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m */; }; + 7F8FD22F2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F8FD22E2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m */; }; 7FA99E592D22C75300582559 /* CameraExposureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FA99E582D22C75300582559 /* CameraExposureTests.m */; }; + 7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */; }; + 7FCEDD362D43C2B900EA1CA8 /* MockCaptureDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FCEDD322D43C2B900EA1CA8 /* MockCaptureDevice.m */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -79,11 +85,23 @@ 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 7D5FCCD32AEF9D0200FB7108 /* CameraSettingsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraSettingsTests.m; sourceTree = ""; }; - 7F87E8012D01FD5600A3549C /* MockCaptureDeviceController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockCaptureDeviceController.m; sourceTree = ""; }; - 7F87E8032D02FF8C00A3549C /* MockCaptureDeviceController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockCaptureDeviceController.h; sourceTree = ""; }; - 7F87E80A2D0325B200A3549C /* MockDeviceOrientationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockDeviceOrientationProvider.h; sourceTree = ""; }; - 7F87E80B2D0325D700A3549C /* MockDeviceOrientationProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockDeviceOrientationProvider.m; sourceTree = ""; }; + 7F29EB202D269E4300740257 /* MockEventChannel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockEventChannel.h; sourceTree = ""; }; + 7F29EB212D269ED500740257 /* MockEventChannel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockEventChannel.m; sourceTree = ""; }; + 7F29EB272D26A55300740257 /* MockCameraDeviceDiscoverer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockCameraDeviceDiscoverer.h; sourceTree = ""; }; + 7F29EB282D26A59000740257 /* MockCameraDeviceDiscoverer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockCameraDeviceDiscoverer.m; sourceTree = ""; }; + 7F29EB3E2D281C5800740257 /* MockCaptureSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockCaptureSession.h; sourceTree = ""; }; + 7F29EB402D281C7E00740257 /* MockCaptureSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockCaptureSession.m; sourceTree = ""; }; + 7F8FD2272D4BFA8D001AF2C1 /* MockGlobalEventApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockGlobalEventApi.h; sourceTree = ""; }; + 7F8FD2282D4BFABF001AF2C1 /* MockGlobalEventApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockGlobalEventApi.m; sourceTree = ""; }; + 7F8FD22A2D4D07A6001AF2C1 /* MockFlutterTextureRegistry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFlutterTextureRegistry.h; sourceTree = ""; }; + 7F8FD22B2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFlutterTextureRegistry.m; sourceTree = ""; }; + 7F8FD22D2D4D0B73001AF2C1 /* MockFlutterBinaryMessenger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFlutterBinaryMessenger.h; sourceTree = ""; }; + 7F8FD22E2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFlutterBinaryMessenger.m; sourceTree = ""; }; 7FA99E582D22C75300582559 /* CameraExposureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraExposureTests.m; sourceTree = ""; }; + 7FCEDD312D43C2B900EA1CA8 /* MockCaptureDevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockCaptureDevice.h; sourceTree = ""; }; + 7FCEDD322D43C2B900EA1CA8 /* MockCaptureDevice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockCaptureDevice.m; sourceTree = ""; }; + 7FCEDD332D43C2B900EA1CA8 /* MockDeviceOrientationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockDeviceOrientationProvider.h; sourceTree = ""; }; + 7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockDeviceOrientationProvider.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -136,10 +154,7 @@ 03BB76692665316900CE5A93 /* RunnerTests */ = { isa = PBXGroup; children = ( - 7F87E80B2D0325D700A3549C /* MockDeviceOrientationProvider.m */, - 7F87E80A2D0325B200A3549C /* MockDeviceOrientationProvider.h */, - 7F87E8032D02FF8C00A3549C /* MockCaptureDeviceController.h */, - 7F87E8012D01FD5600A3549C /* MockCaptureDeviceController.m */, + 7F29EB3F2D281C6D00740257 /* Mocks */, 7D5FCCD32AEF9D0200FB7108 /* CameraSettingsTests.m */, 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, 7FA99E582D22C75300582559 /* CameraExposureTests.m */, @@ -173,6 +188,29 @@ name = Frameworks; sourceTree = ""; }; + 7F29EB3F2D281C6D00740257 /* Mocks */ = { + isa = PBXGroup; + children = ( + 7F8FD22D2D4D0B73001AF2C1 /* MockFlutterBinaryMessenger.h */, + 7F8FD22E2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m */, + 7F8FD22A2D4D07A6001AF2C1 /* MockFlutterTextureRegistry.h */, + 7F8FD22B2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m */, + 7F8FD2282D4BFABF001AF2C1 /* MockGlobalEventApi.m */, + 7F8FD2272D4BFA8D001AF2C1 /* MockGlobalEventApi.h */, + 7FCEDD312D43C2B900EA1CA8 /* MockCaptureDevice.h */, + 7FCEDD322D43C2B900EA1CA8 /* MockCaptureDevice.m */, + 7FCEDD332D43C2B900EA1CA8 /* MockDeviceOrientationProvider.h */, + 7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */, + 7F29EB282D26A59000740257 /* MockCameraDeviceDiscoverer.m */, + 7F29EB272D26A55300740257 /* MockCameraDeviceDiscoverer.h */, + 7F29EB202D269E4300740257 /* MockEventChannel.h */, + 7F29EB212D269ED500740257 /* MockEventChannel.m */, + 7F29EB3E2D281C5800740257 /* MockCaptureSession.h */, + 7F29EB402D281C7E00740257 /* MockCaptureSession.m */, + ); + path = Mocks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -463,21 +501,27 @@ 7F56D0382D1EDDCE005676A5 /* CameraPermissionTests.m in Sources */, E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, - 7F87E8022D01FD6F00A3549C /* MockCaptureDeviceController.m in Sources */, 7D5FCCD42AEF9D0200FB7108 /* CameraSettingsTests.m in Sources */, + 7F8FD2292D4BFABF001AF2C1 /* MockGlobalEventApi.m in Sources */, 7FA99E592D22C75300582559 /* CameraExposureTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, - 7F87E80C2D0325D900A3549C /* MockDeviceOrientationProvider.m in Sources */, E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, + 7F29EB222D269ED500740257 /* MockEventChannel.m in Sources */, + 7F8FD22F2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m in Sources */, E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, 43ED1537282570DE00EB00DE /* AvailableCamerasTest.m in Sources */, E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, CEF6611A2B5E36A500D33FD4 /* CameraSessionPresetsTests.m in Sources */, E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */, + 7F29EB292D26A59000740257 /* MockCameraDeviceDiscoverer.m in Sources */, 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */, E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, + 7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */, E01EE4A82799F3A5008C1950 /* QueueUtilsTests.m in Sources */, + 7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */, + 7FCEDD362D43C2B900EA1CA8 /* MockCaptureDevice.m in Sources */, + 7F8FD22C2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m index f26a8dc48f16..516cd3c49585 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTest.m @@ -8,33 +8,57 @@ #endif @import XCTest; @import AVFoundation; -#import + +#import "MockCameraDeviceDiscoverer.h" +#import "MockCaptureDevice.h" +#import "MockCaptureSession.h" +#import "MockFlutterBinaryMessenger.h" +#import "MockFlutterTextureRegistry.h" +#import "MockGlobalEventApi.h" @interface AvailableCamerasTest : XCTestCase + @end @implementation AvailableCamerasTest +- (CameraPlugin *)createCameraPluginWithDeviceDiscoverer: + (MockCameraDeviceDiscoverer *)deviceDiscoverer { + return [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init] + globalAPI:[[MockGlobalEventApi alloc] init] + deviceDiscoverer:deviceDiscoverer + deviceFactory:^NSObject *(NSString *name) { + return [[MockCaptureDevice alloc] init]; + } + captureSessionFactory:^NSObject * { + return [[MockCaptureSession alloc] init]; + } + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; +} + - (void)testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + MockCameraDeviceDiscoverer *mockDeviceDiscoverer = [[MockCameraDeviceDiscoverer alloc] init]; + CameraPlugin *cameraPlugin = [self createCameraPluginWithDeviceDiscoverer:mockDeviceDiscoverer]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; // iPhone 13 Cameras: - AVCaptureDevice *wideAngleCamera = OCMClassMock([AVCaptureDevice class]); - OCMStub([wideAngleCamera uniqueID]).andReturn(@"0"); - OCMStub([wideAngleCamera position]).andReturn(AVCaptureDevicePositionBack); + MockCaptureDevice *wideAngleCamera = [[MockCaptureDevice alloc] init]; + wideAngleCamera.uniqueID = @"0"; + wideAngleCamera.position = AVCaptureDevicePositionBack; - AVCaptureDevice *frontFacingCamera = OCMClassMock([AVCaptureDevice class]); - OCMStub([frontFacingCamera uniqueID]).andReturn(@"1"); - OCMStub([frontFacingCamera position]).andReturn(AVCaptureDevicePositionFront); + MockCaptureDevice *frontFacingCamera = [[MockCaptureDevice alloc] init]; + frontFacingCamera.uniqueID = @"1"; + frontFacingCamera.position = AVCaptureDevicePositionFront; - AVCaptureDevice *ultraWideCamera = OCMClassMock([AVCaptureDevice class]); - OCMStub([ultraWideCamera uniqueID]).andReturn(@"2"); - OCMStub([ultraWideCamera position]).andReturn(AVCaptureDevicePositionBack); + MockCaptureDevice *ultraWideCamera = [[MockCaptureDevice alloc] init]; + ultraWideCamera.uniqueID = @"2"; + ultraWideCamera.position = AVCaptureDevicePositionBack; - AVCaptureDevice *telephotoCamera = OCMClassMock([AVCaptureDevice class]); - OCMStub([telephotoCamera uniqueID]).andReturn(@"3"); - OCMStub([telephotoCamera position]).andReturn(AVCaptureDevicePositionBack); + MockCaptureDevice *telephotoCamera = [[MockCaptureDevice alloc] init]; + telephotoCamera.uniqueID = @"3"; + telephotoCamera.position = AVCaptureDevicePositionBack; NSMutableArray *requiredTypes = [@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ] @@ -43,21 +67,23 @@ - (void)testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone { [requiredTypes addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; } - id discoverySessionMock = OCMClassMock([AVCaptureDeviceDiscoverySession class]); - OCMStub([discoverySessionMock discoverySessionWithDeviceTypes:requiredTypes - mediaType:AVMediaTypeVideo - position:AVCaptureDevicePositionUnspecified]) - .andReturn(discoverySessionMock); - NSMutableArray *cameras = [NSMutableArray array]; [cameras addObjectsFromArray:@[ wideAngleCamera, frontFacingCamera, telephotoCamera ]]; if (@available(iOS 13.0, *)) { [cameras addObject:ultraWideCamera]; } - OCMStub([discoverySessionMock devices]).andReturn([NSArray arrayWithArray:cameras]); + + mockDeviceDiscoverer.discoverySessionStub = ^NSArray *> *_Nullable( + NSArray *_Nonnull deviceTypes, AVMediaType _Nonnull mediaType, + AVCaptureDevicePosition position) { + XCTAssertEqualObjects(deviceTypes, requiredTypes); + XCTAssertEqual(mediaType, AVMediaTypeVideo); + XCTAssertEqual(position, AVCaptureDevicePositionUnspecified); + return cameras; + }; __block NSArray *resultValue; - [camera + [cameraPlugin availableCamerasWithCompletion:^(NSArray *_Nullable result, FlutterError *_Nullable error) { XCTAssertNil(error); @@ -74,17 +100,19 @@ - (void)testAvailableCamerasShouldReturnAllCamerasOnMultiCameraIPhone { } } - (void)testAvailableCamerasShouldReturnOneCameraOnSingleCameraIPhone { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + MockCameraDeviceDiscoverer *mockDeviceDiscoverer = [[MockCameraDeviceDiscoverer alloc] init]; + CameraPlugin *cameraPlugin = [self createCameraPluginWithDeviceDiscoverer:mockDeviceDiscoverer]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; // iPhone 8 Cameras: - AVCaptureDevice *wideAngleCamera = OCMClassMock([AVCaptureDevice class]); - OCMStub([wideAngleCamera uniqueID]).andReturn(@"0"); - OCMStub([wideAngleCamera position]).andReturn(AVCaptureDevicePositionBack); + MockCaptureDevice *wideAngleCamera = [[MockCaptureDevice alloc] init]; + wideAngleCamera.uniqueID = @"0"; + wideAngleCamera.position = AVCaptureDevicePositionBack; - AVCaptureDevice *frontFacingCamera = OCMClassMock([AVCaptureDevice class]); - OCMStub([frontFacingCamera uniqueID]).andReturn(@"1"); - OCMStub([frontFacingCamera position]).andReturn(AVCaptureDevicePositionFront); + MockCaptureDevice *frontFacingCamera = [[MockCaptureDevice alloc] init]; + frontFacingCamera.uniqueID = @"1"; + frontFacingCamera.position = AVCaptureDevicePositionFront; NSMutableArray *requiredTypes = [@[ AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInTelephotoCamera ] @@ -93,18 +121,20 @@ - (void)testAvailableCamerasShouldReturnOneCameraOnSingleCameraIPhone { [requiredTypes addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; } - id discoverySessionMock = OCMClassMock([AVCaptureDeviceDiscoverySession class]); - OCMStub([discoverySessionMock discoverySessionWithDeviceTypes:requiredTypes - mediaType:AVMediaTypeVideo - position:AVCaptureDevicePositionUnspecified]) - .andReturn(discoverySessionMock); - NSMutableArray *cameras = [NSMutableArray array]; [cameras addObjectsFromArray:@[ wideAngleCamera, frontFacingCamera ]]; - OCMStub([discoverySessionMock devices]).andReturn([NSArray arrayWithArray:cameras]); + + mockDeviceDiscoverer.discoverySessionStub = ^NSArray *> *_Nullable( + NSArray *_Nonnull deviceTypes, AVMediaType _Nonnull mediaType, + AVCaptureDevicePosition position) { + XCTAssertEqualObjects(deviceTypes, requiredTypes); + XCTAssertEqual(mediaType, AVMediaTypeVideo); + XCTAssertEqual(position, AVCaptureDevicePositionUnspecified); + return cameras; + }; __block NSArray *resultValue; - [camera + [cameraPlugin availableCamerasWithCompletion:^(NSArray *_Nullable result, FlutterError *_Nullable error) { XCTAssertNil(error); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m index 5892e1d4b797..b651fec78649 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m @@ -8,13 +8,36 @@ #endif @import XCTest; +#import "MockCameraDeviceDiscoverer.h" +#import "MockCaptureDevice.h" +#import "MockCaptureSession.h" +#import "MockFlutterBinaryMessenger.h" +#import "MockFlutterTextureRegistry.h" +#import "MockGlobalEventApi.h" + @interface CameraCaptureSessionQueueRaceConditionTests : XCTestCase @end @implementation CameraCaptureSessionQueueRaceConditionTests +- (CameraPlugin *)createCameraPlugin { + MockCaptureDevice *captureDevice = [[MockCaptureDevice alloc] init]; + + return [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init] + globalAPI:[[MockGlobalEventApi alloc] init] + deviceDiscoverer:[[MockCameraDeviceDiscoverer alloc] init] + deviceFactory:^NSObject *(NSString *name) { + return captureDevice; + } + captureSessionFactory:^NSObject * { + return [[MockCaptureSession alloc] init]; + } + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; +} + - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + CameraPlugin *cameraPlugin = [self createCameraPlugin]; XCTestExpectation *disposeExpectation = [self expectationWithDescription:@"dispose's result block must be called"]; @@ -22,27 +45,27 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { [self expectationWithDescription:@"create's result block must be called"]; // Mimic a dispose call followed by a create call, which can be triggered by slightly dragging the // home bar, causing the app to be inactive, and immediately regain active. - [camera disposeCamera:0 - completion:^(FlutterError *_Nullable error) { - [disposeExpectation fulfill]; - }]; - [camera createCameraOnSessionQueueWithName:@"acamera" - settings:[FCPPlatformMediaSettings - makeWithResolutionPreset: - FCPPlatformResolutionPresetMedium - framesPerSecond:nil - videoBitrate:nil - audioBitrate:nil - enableAudio:YES] - completion:^(NSNumber *_Nullable result, - FlutterError *_Nullable error) { - [createExpectation fulfill]; - }]; + [cameraPlugin disposeCamera:0 + completion:^(FlutterError *_Nullable error) { + [disposeExpectation fulfill]; + }]; + [cameraPlugin createCameraOnSessionQueueWithName:@"acamera" + settings:[FCPPlatformMediaSettings + makeWithResolutionPreset: + FCPPlatformResolutionPresetMedium + framesPerSecond:nil + videoBitrate:nil + audioBitrate:nil + enableAudio:YES] + completion:^(NSNumber *_Nullable result, + FlutterError *_Nullable error) { + [createExpectation fulfill]; + }]; [self waitForExpectationsWithTimeout:30 handler:nil]; // `captureSessionQueue` must not be nil after `create` call. Otherwise a nil // `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:` // API will cause a crash. - XCTAssertNotNil(camera.captureSessionQueue, + XCTAssertNotNil(cameraPlugin.captureSessionQueue, @"captureSessionQueue must not be nil after create method. "); } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m index 30de3182c33e..e0969ad703de 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m @@ -7,25 +7,25 @@ @import AVFoundation; #import "CameraTestUtils.h" -#import "MockCaptureDeviceController.h" +#import "MockCaptureDevice.h" #import "MockDeviceOrientationProvider.h" @interface CameraExposureTests : XCTestCase @property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) MockCaptureDeviceController *mockDevice; +@property(readonly, nonatomic) MockCaptureDevice *mockDevice; @property(readonly, nonatomic) MockDeviceOrientationProvider *mockDeviceOrientationProvider; @end @implementation CameraExposureTests - (void)setUp { - MockCaptureDeviceController *mockDevice = [[MockCaptureDeviceController alloc] init]; + MockCaptureDevice *mockDevice = [[MockCaptureDevice alloc] init]; _mockDeviceOrientationProvider = [[MockDeviceOrientationProvider alloc] init]; _mockDevice = mockDevice; _camera = FLTCreateCamWithCaptureSessionQueueAndMediaSettings( nil, nil, nil, - ^id(void) { + ^NSObject *(void) { return mockDevice; }, _mockDeviceOrientationProvider); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m index 63be762f5296..5f52c9bc5ee1 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.m @@ -10,25 +10,25 @@ @import AVFoundation; #import "CameraTestUtils.h" -#import "MockCaptureDeviceController.h" +#import "MockCaptureDevice.h" #import "MockDeviceOrientationProvider.h" @interface CameraFocusTests : XCTestCase @property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) MockCaptureDeviceController *mockDevice; +@property(readonly, nonatomic) MockCaptureDevice *mockDevice; @property(readonly, nonatomic) MockDeviceOrientationProvider *mockDeviceOrientationProvider; @end @implementation CameraFocusTests - (void)setUp { - MockCaptureDeviceController *mockDevice = [[MockCaptureDeviceController alloc] init]; + MockCaptureDevice *mockDevice = [[MockCaptureDevice alloc] init]; _mockDevice = mockDevice; _mockDeviceOrientationProvider = [[MockDeviceOrientationProvider alloc] init]; _camera = FLTCreateCamWithCaptureSessionQueueAndMediaSettings( nil, nil, nil, - ^id(void) { + ^NSObject *(void) { return mockDevice; }, _mockDeviceOrientationProvider); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m index 4df1994699df..0acc392545fe 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -10,13 +10,21 @@ @import AVFoundation; #import +#import "MockFlutterBinaryMessenger.h" +#import "MockFlutterTextureRegistry.h" + @interface CameraMethodChannelTests : XCTestCase @end @implementation CameraMethodChannelTests +- (CameraPlugin *)createCameraPlugin { + return [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init]]; +} + - (void)testCreate_ShouldCallResultOnMainThread { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + CameraPlugin *camera = [self createCameraPlugin]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; @@ -51,7 +59,7 @@ - (void)testCreate_ShouldCallResultOnMainThread { } - (void)testDisposeShouldDeallocCamera { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + CameraPlugin *camera = [self createCameraPlugin]; id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); OCMStub([avCaptureDeviceInputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg anyObjectRef]]) diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m index b1547c53212c..09f57551f0f3 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m @@ -9,27 +9,38 @@ @import XCTest; @import Flutter; -#import +#import "MockCameraDeviceDiscoverer.h" +#import "MockCaptureDevice.h" +#import "MockCaptureSession.h" +#import "MockDeviceOrientationProvider.h" +#import "MockFlutterBinaryMessenger.h" +#import "MockFlutterTextureRegistry.h" +#import "MockGlobalEventApi.h" + +@interface MockCamera : FLTCam +@property(nonatomic, copy) void (^setDeviceOrientationStub)(UIDeviceOrientation orientation); +@end -#import "MockCaptureDeviceController.h" +@implementation MockCamera +- (void)setDeviceOrientation:(UIDeviceOrientation)orientation { + if (self.setDeviceOrientationStub) { + self.setDeviceOrientationStub(orientation); + } +} + +- (void)setCaptureDevice:(NSObject *)device { + self.captureDevice = device; +} -@interface StubGlobalEventApi : FCPCameraGlobalEventApi -@property(nonatomic) BOOL called; -@property(nonatomic) FCPPlatformDeviceOrientation lastOrientation; @end -@implementation StubGlobalEventApi -- (void)deviceOrientationChangedOrientation:(FCPPlatformDeviceOrientation)orientation - completion:(void (^)(FlutterError *_Nullable))completion { - self.called = YES; - self.lastOrientation = orientation; - completion(nil); -} +@interface MockUIDevice : UIDevice +@property(nonatomic, assign) UIDeviceOrientation mockOrientation; +@end -- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString *)channel - binaryMessageHandler: - (nullable FlutterBinaryMessageHandler)handler { - return 0; +@implementation MockUIDevice +- (UIDeviceOrientation)orientation { + return self.mockOrientation; } @end @@ -37,19 +48,35 @@ - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString #pragma mark - @interface CameraOrientationTests : XCTestCase -@property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) MockCaptureDeviceController *mockDevice; -@property(readonly, nonatomic) StubGlobalEventApi *eventAPI; +@property(readonly, nonatomic) MockCamera *camera; +@property(readonly, nonatomic) MockCaptureDevice *mockDevice; +@property(readonly, nonatomic) MockGlobalEventApi *eventAPI; +@property(readonly, nonatomic) CameraPlugin *cameraPlugin; +@property(readonly, nonatomic) MockCameraDeviceDiscoverer *deviceDiscoverer; @end @implementation CameraOrientationTests - (void)setUp { [super setUp]; - _mockDevice = [[MockCaptureDeviceController alloc] init]; - _camera = [[FLTCam alloc] init]; - - [_camera setValue:_mockDevice forKey:@"captureDevice"]; + MockCaptureDevice *mockDevice = [[MockCaptureDevice alloc] init]; + _camera = [[MockCamera alloc] init]; + _eventAPI = [[MockGlobalEventApi alloc] init]; + _mockDevice = mockDevice; + _deviceDiscoverer = [[MockCameraDeviceDiscoverer alloc] init]; + + _cameraPlugin = [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init] + globalAPI:_eventAPI + deviceDiscoverer:_deviceDiscoverer + deviceFactory:^NSObject *(NSString *name) { + return mockDevice; + } + captureSessionFactory:^NSObject *_Nonnull { + return [[MockCaptureSession alloc] init]; + } + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; + _cameraPlugin.camera = _camera; } // Ensure that the given queue and then the main queue have both cycled, to wait for any pending @@ -70,93 +97,96 @@ - (void)sendOrientation:(UIDeviceOrientation)orientation toCamera:(CameraPlugin } - (void)testOrientationNotifications { - StubGlobalEventApi *eventAPI = [[StubGlobalEventApi alloc] init]; - CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil - messenger:nil - globalAPI:eventAPI]; - - [self sendOrientation:UIDeviceOrientationPortraitUpsideDown toCamera:cameraPlugin]; - XCTAssertEqual(eventAPI.lastOrientation, FCPPlatformDeviceOrientationPortraitDown); - [self sendOrientation:UIDeviceOrientationPortrait toCamera:cameraPlugin]; - XCTAssertEqual(eventAPI.lastOrientation, FCPPlatformDeviceOrientationPortraitUp); - [self sendOrientation:UIDeviceOrientationLandscapeLeft toCamera:cameraPlugin]; - XCTAssertEqual(eventAPI.lastOrientation, FCPPlatformDeviceOrientationLandscapeLeft); - [self sendOrientation:UIDeviceOrientationLandscapeRight toCamera:cameraPlugin]; - XCTAssertEqual(eventAPI.lastOrientation, FCPPlatformDeviceOrientationLandscapeRight); + [self sendOrientation:UIDeviceOrientationPortraitUpsideDown toCamera:_cameraPlugin]; + XCTAssertEqual(_eventAPI.lastOrientation, FCPPlatformDeviceOrientationPortraitDown); + [self sendOrientation:UIDeviceOrientationPortrait toCamera:_cameraPlugin]; + XCTAssertEqual(_eventAPI.lastOrientation, FCPPlatformDeviceOrientationPortraitUp); + [self sendOrientation:UIDeviceOrientationLandscapeLeft toCamera:_cameraPlugin]; + XCTAssertEqual(_eventAPI.lastOrientation, FCPPlatformDeviceOrientationLandscapeLeft); + [self sendOrientation:UIDeviceOrientationLandscapeRight toCamera:_cameraPlugin]; + XCTAssertEqual(_eventAPI.lastOrientation, FCPPlatformDeviceOrientationLandscapeRight); } - (void)testOrientationNotificationsNotCalledForFaceUp { - StubGlobalEventApi *eventAPI = [[StubGlobalEventApi alloc] init]; - CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil - messenger:nil - globalAPI:eventAPI]; - - [self sendOrientation:UIDeviceOrientationFaceUp toCamera:cameraPlugin]; + [self sendOrientation:UIDeviceOrientationFaceUp toCamera:_cameraPlugin]; - XCTAssertFalse(eventAPI.called); + XCTAssertFalse(_eventAPI.deviceOrientationChangedCalled); } - (void)testOrientationNotificationsNotCalledForFaceDown { - StubGlobalEventApi *eventAPI = [[StubGlobalEventApi alloc] init]; - CameraPlugin *cameraPlugin = [[CameraPlugin alloc] initWithRegistry:nil - messenger:nil - globalAPI:eventAPI]; - - [self sendOrientation:UIDeviceOrientationFaceDown toCamera:cameraPlugin]; + [self sendOrientation:UIDeviceOrientationFaceDown toCamera:_cameraPlugin]; - XCTAssertFalse(eventAPI.called); + XCTAssertFalse(_eventAPI.deviceOrientationChangedCalled); } - (void)testOrientationUpdateMustBeOnCaptureSessionQueue { XCTestExpectation *queueExpectation = [self expectationWithDescription:@"Orientation update must happen on the capture session queue"]; - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + + CameraPlugin *plugin = + [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init]]; const char *captureSessionQueueSpecific = "capture_session_queue"; - dispatch_queue_set_specific(camera.captureSessionQueue, captureSessionQueueSpecific, + dispatch_queue_set_specific(plugin.captureSessionQueue, captureSessionQueueSpecific, (void *)captureSessionQueueSpecific, NULL); - FLTCam *mockCam = OCMClassMock([FLTCam class]); - camera.camera = mockCam; + plugin.camera = _camera; - OCMStub([mockCam setDeviceOrientation:UIDeviceOrientationLandscapeLeft]) - .andDo(^(NSInvocation *invocation) { - if (dispatch_get_specific(captureSessionQueueSpecific)) { - [queueExpectation fulfill]; - } - }); + _camera.setDeviceOrientationStub = ^(UIDeviceOrientation orientation) { + if (dispatch_get_specific(captureSessionQueueSpecific)) { + [queueExpectation fulfill]; + } + }; - [camera orientationChanged: + [plugin orientationChanged: [self createMockNotificationForOrientation:UIDeviceOrientationLandscapeLeft]]; [self waitForExpectationsWithTimeout:1 handler:nil]; } - (void)testOrientationChanged_noRetainCycle { dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); - FLTCam *mockCam = OCMClassMock([FLTCam class]); - StubGlobalEventApi *stubAPI = [[StubGlobalEventApi alloc] init]; - __weak CameraPlugin *weakCamera; + __weak CameraPlugin *weakPlugin; + __weak MockCaptureDevice *weakDevice = _mockDevice; @autoreleasepool { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil - messenger:nil - globalAPI:stubAPI]; - weakCamera = camera; - camera.captureSessionQueue = captureSessionQueue; - camera.camera = mockCam; - - [camera orientationChanged: + CameraPlugin *plugin = + [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init] + globalAPI:_eventAPI + deviceDiscoverer:_deviceDiscoverer + deviceFactory:^NSObject *(NSString *name) { + return weakDevice; + } + captureSessionFactory:^NSObject *_Nonnull { + return [[MockCaptureSession alloc] init]; + } + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; + weakPlugin = plugin; + plugin.captureSessionQueue = captureSessionQueue; + plugin.camera = _camera; + + [plugin orientationChanged: [self createMockNotificationForOrientation:UIDeviceOrientationLandscapeLeft]]; } // Sanity check - XCTAssertNil(weakCamera, @"Camera must have been deallocated."); + XCTAssertNil(weakPlugin, @"Camera must have been deallocated."); + + __block BOOL setDeviceOrientationCalled = NO; + _camera.setDeviceOrientationStub = ^(UIDeviceOrientation orientation) { + if (orientation == UIDeviceOrientationLandscapeLeft) { + setDeviceOrientationCalled = YES; + } + }; + + __weak MockGlobalEventApi *weakEventAPI = _eventAPI; // Must check in captureSessionQueue since orientationChanged dispatches to this queue. XCTestExpectation *expectation = [self expectationWithDescription:@"Dispatched to capture session queue"]; dispatch_async(captureSessionQueue, ^{ - OCMVerify(never(), [mockCam setDeviceOrientation:UIDeviceOrientationLandscapeLeft]); - XCTAssertFalse(stubAPI.called); + XCTAssertFalse(setDeviceOrientationCalled); + XCTAssertFalse(weakEventAPI.deviceOrientationChangedCalled); [expectation fulfill]; }); @@ -164,8 +194,8 @@ - (void)testOrientationChanged_noRetainCycle { } - (NSNotification *)createMockNotificationForOrientation:(UIDeviceOrientation)deviceOrientation { - UIDevice *mockDevice = OCMClassMock([UIDevice class]); - OCMStub([mockDevice orientation]).andReturn(deviceOrientation); + MockUIDevice *mockDevice = [[MockUIDevice alloc] init]; + mockDevice.mockOrientation = deviceOrientation; return [NSNotification notificationWithName:@"orientation_test" object:mockDevice]; } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m index 08cba70bf3a2..9f7092f6e52f 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m @@ -21,11 +21,11 @@ @implementation FLTCamSessionPresetsTest - (void)testResolutionPresetWithBestFormat_mustUpdateCaptureSessionPreset { NSString *expectedPreset = AVCaptureSessionPresetInputPriority; - id videoSessionMock = OCMClassMock([AVCaptureSession class]); + id videoSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([videoSessionMock addInputWithNoConnections:[OCMArg any]]); id captureFormatMock = OCMClassMock([AVCaptureDeviceFormat class]); - id captureDeviceMock = OCMClassMock([AVCaptureDevice class]); + id captureDeviceMock = OCMProtocolMock(@protocol(FLTCaptureDevice)); OCMStub([captureDeviceMock formats]).andReturn(@[ captureFormatMock ]); OCMExpect([captureDeviceMock activeFormat]).andReturn(captureFormatMock); @@ -48,7 +48,7 @@ - (void)testResolutionPresetWithBestFormat_mustUpdateCaptureSessionPreset { - (void)testResolutionPresetWithCanSetSessionPresetMax_mustUpdateCaptureSessionPreset { NSString *expectedPreset = AVCaptureSessionPreset3840x2160; - id videoSessionMock = OCMClassMock([AVCaptureSession class]); + id videoSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([videoSessionMock addInputWithNoConnections:[OCMArg any]]); // Make sure that setting resolution preset for session always succeeds. @@ -64,7 +64,7 @@ - (void)testResolutionPresetWithCanSetSessionPresetMax_mustUpdateCaptureSessionP - (void)testResolutionPresetWithCanSetSessionPresetUltraHigh_mustUpdateCaptureSessionPreset { NSString *expectedPreset = AVCaptureSessionPreset3840x2160; - id videoSessionMock = OCMClassMock([AVCaptureSession class]); + id videoSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([videoSessionMock addInputWithNoConnections:[OCMArg any]]); // Make sure that setting resolution preset for session always succeeds. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m index 98e12020b59e..873cc212a8a1 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m @@ -9,7 +9,10 @@ @import XCTest; @import AVFoundation; #import + #import "CameraTestUtils.h" +#import "MockFlutterBinaryMessenger.h" +#import "MockFlutterTextureRegistry.h" static const FCPPlatformResolutionPreset gTestResolutionPreset = FCPPlatformResolutionPresetMedium; static const int gTestFramesPerSecond = 15; @@ -167,7 +170,9 @@ - (void)testSettings_shouldPassConfigurationToCameraDeviceAndWriter { } - (void)testSettings_ShouldBeSupportedByMethodCall { - CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; + CameraPlugin *camera = + [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] + messenger:[[MockFlutterBinaryMessenger alloc] init]]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h index 008ba9df48f5..c8a5b230453a 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.h @@ -29,7 +29,7 @@ extern FLTCam *FLTCreateCamWithCaptureSessionQueue(dispatch_queue_t captureSessi /// @param captureSession AVCaptureSession for video /// @param resolutionPreset preset for camera's captureSession resolution /// @return an FLTCam object. -extern FLTCam *FLTCreateCamWithVideoCaptureSession(AVCaptureSession *captureSession, +extern FLTCam *FLTCreateCamWithVideoCaptureSession(NSObject *captureSession, FCPPlatformResolutionPreset resolutionPreset); /// Creates an `FLTCam` with a given captureSession and resolutionPreset. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m index f10a3b122d7b..67d4432d2f48 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraTestUtils.m @@ -8,6 +8,8 @@ @import AVFoundation; @import camera_avfoundation; +#import "MockCaptureDevice.h" +#import "MockCaptureSession.h" #import "MockDeviceOrientationProvider.h" static FCPPlatformMediaSettings *FCPGetDefaultMediaSettings( @@ -27,7 +29,7 @@ FLTCam *FLTCreateCamWithCaptureSessionQueueAndMediaSettings( dispatch_queue_t captureSessionQueue, FCPPlatformMediaSettings *mediaSettings, FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper, CaptureDeviceFactory captureDeviceFactory, - id deviceOrientationProvider) { + NSObject *deviceOrientationProvider) { if (!mediaSettings) { mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPresetMedium); } @@ -40,11 +42,11 @@ deviceOrientationProvider = [[MockDeviceOrientationProvider alloc] init]; } - id inputMock = OCMClassMock([AVCaptureDeviceInput class]); - OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) - .andReturn(inputMock); + if (!captureSessionQueue) { + captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + } - id videoSessionMock = OCMClassMock([AVCaptureSession class]); + id videoSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([videoSessionMock beginConfiguration]) .andDo(^(NSInvocation *invocation){ }); @@ -55,7 +57,7 @@ OCMStub([videoSessionMock addInputWithNoConnections:[OCMArg any]]); OCMStub([videoSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - id audioSessionMock = OCMClassMock([AVCaptureSession class]); + id audioSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([audioSessionMock addInputWithNoConnections:[OCMArg any]]); OCMStub([audioSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); @@ -75,7 +77,7 @@ frameRateRangeMock2 ]); - id captureDeviceMock = OCMClassMock([AVCaptureDevice class]); + id captureDeviceMock = OCMProtocolMock(@protocol(FLTCaptureDevice)); OCMStub([captureDeviceMock lockForConfiguration:[OCMArg setTo:nil]]).andReturn(YES); OCMStub([captureDeviceMock formats]).andReturn((@[ captureDeviceFormatMock1, captureDeviceFormatMock2 @@ -89,20 +91,25 @@ [invocation setReturnValue:&format]; }); - id fltCam = [[FLTCam alloc] initWithMediaSettings:mediaSettings - mediaSettingsAVWrapper:mediaSettingsAVWrapper - orientation:UIDeviceOrientationPortrait - videoCaptureSession:videoSessionMock - audioCaptureSession:audioSessionMock - captureSessionQueue:captureSessionQueue - captureDeviceFactory:captureDeviceFactory ?: ^id(void) { - return [[FLTDefaultCaptureDeviceController alloc] initWithDevice:captureDeviceMock]; - } - videoDimensionsForFormat:^CMVideoDimensions(AVCaptureDeviceFormat *format) { - return CMVideoFormatDescriptionGetDimensions(format.formatDescription); - } - deviceOrientationProvider:deviceOrientationProvider - error:nil]; + FLTCamConfiguration *configuration = [[FLTCamConfiguration alloc] initWithMediaSettings:mediaSettings + mediaSettingsWrapper:mediaSettingsAVWrapper + captureDeviceFactory:captureDeviceFactory ?: ^NSObject *(void) { + return captureDeviceMock; + } + captureSessionFactory:^NSObject *_Nonnull{ + return videoSessionMock; + } + captureSessionQueue:captureSessionQueue + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; + + configuration.videoDimensionsForFormat = ^CMVideoDimensions(AVCaptureDeviceFormat *format) { + return CMVideoFormatDescriptionGetDimensions(format.formatDescription); + }; + configuration.deviceOrientationProvider = deviceOrientationProvider; + configuration.videoCaptureSession = videoSessionMock; + configuration.audioCaptureSession = audioSessionMock; + + id fltCam = [[FLTCam alloc] initWithConfiguration:configuration error:nil]; id captureVideoDataOutputMock = [OCMockObject niceMockForClass:[AVCaptureVideoDataOutput class]]; @@ -132,24 +139,35 @@ return fltCam; } -FLTCam *FLTCreateCamWithVideoCaptureSession(AVCaptureSession *captureSession, +FLTCam *FLTCreateCamWithVideoCaptureSession(NSObject *captureSession, FCPPlatformResolutionPreset resolutionPreset) { id inputMock = OCMClassMock([AVCaptureDeviceInput class]); OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) .andReturn(inputMock); - id audioSessionMock = OCMClassMock([AVCaptureSession class]); + id audioSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([audioSessionMock addInputWithNoConnections:[OCMArg any]]); OCMStub([audioSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - return [[FLTCam alloc] initWithCameraName:@"camera" - mediaSettings:FCPGetDefaultMediaSettings(resolutionPreset) - mediaSettingsAVWrapper:[[FLTCamMediaSettingsAVWrapper alloc] init] - orientation:UIDeviceOrientationPortrait - videoCaptureSession:captureSession - audioCaptureSession:audioSessionMock - captureSessionQueue:dispatch_queue_create("capture_session_queue", NULL) - error:nil]; + id captureDeviceMock = OCMProtocolMock(@protocol(FLTCaptureDevice)); + + FLTCamConfiguration *configuration = [[FLTCamConfiguration alloc] + initWithMediaSettings:FCPGetDefaultMediaSettings(resolutionPreset) + mediaSettingsWrapper:[[FLTCamMediaSettingsAVWrapper alloc] init] + captureDeviceFactory:^NSObject *(void) { + return captureDeviceMock; + } + captureSessionFactory:^NSObject *_Nonnull { + return captureSession; + } + captureSessionQueue:dispatch_queue_create("capture_session_queue", NULL) + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; + + configuration.orientation = UIDeviceOrientationPortrait; + configuration.videoCaptureSession = captureSession; + configuration.audioCaptureSession = audioSessionMock; + + return [[FLTCam alloc] initWithConfiguration:configuration error:nil]; } FLTCam *FLTCreateCamWithVideoDimensionsForFormat( @@ -159,23 +177,31 @@ OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) .andReturn(inputMock); - id audioSessionMock = OCMClassMock([AVCaptureSession class]); + id audioSessionMock = OCMProtocolMock(@protocol(FLTCaptureSession)); OCMStub([audioSessionMock addInputWithNoConnections:[OCMArg any]]); OCMStub([audioSessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); - return [[FLTCam alloc] - initWithMediaSettings:FCPGetDefaultMediaSettings(resolutionPreset) - mediaSettingsAVWrapper:[[FLTCamMediaSettingsAVWrapper alloc] init] - orientation:UIDeviceOrientationPortrait - videoCaptureSession:captureSession - audioCaptureSession:audioSessionMock - captureSessionQueue:dispatch_queue_create("capture_session_queue", NULL) - captureDeviceFactory:^id(void) { - return [[FLTDefaultCaptureDeviceController alloc] initWithDevice:captureDevice]; - } - videoDimensionsForFormat:videoDimensionsForFormat - deviceOrientationProvider:[[MockDeviceOrientationProvider alloc] init] - error:nil]; + FLTCamConfiguration *configuration = [[FLTCamConfiguration alloc] + initWithMediaSettings:FCPGetDefaultMediaSettings(resolutionPreset) + mediaSettingsWrapper:[[FLTCamMediaSettingsAVWrapper alloc] init] + captureDeviceFactory:^NSObject *(void) { + return [[FLTDefaultCaptureDevice alloc] initWithDevice:captureDevice]; + } + captureSessionFactory:^NSObject *_Nonnull { + return [[FLTDefaultCaptureSession alloc] + initWithCaptureSession:[[AVCaptureSession alloc] init]]; + } + captureSessionQueue:dispatch_queue_create("capture_session_queue", NULL) + captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; + + configuration.videoDimensionsForFormat = videoDimensionsForFormat; + configuration.deviceOrientationProvider = [[MockDeviceOrientationProvider alloc] init]; + configuration.videoCaptureSession = + [[FLTDefaultCaptureSession alloc] initWithCaptureSession:captureSession]; + configuration.audioCaptureSession = audioSessionMock; + configuration.orientation = UIDeviceOrientationPortrait; + + return [[FLTCam alloc] initWithConfiguration:configuration error:nil]; } CMSampleBufferRef FLTCreateTestSampleBuffer(void) { diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m index c1bfb6ad376e..dc6fd86a8b69 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -177,7 +177,7 @@ - (void)testCaptureToFile_handlesTorchMode { [self expectationWithDescription: @"Must send file path to result if save photo delegate completes with file path."]; - id captureDeviceMock = OCMClassMock([AVCaptureDevice class]); + id captureDeviceMock = OCMProtocolMock(@protocol(FLTCaptureDevice)); OCMStub([captureDeviceMock hasTorch]).andReturn(YES); OCMStub([captureDeviceMock isTorchAvailable]).andReturn(YES); OCMStub([captureDeviceMock torchMode]).andReturn(AVCaptureTorchModeAuto); @@ -189,8 +189,8 @@ - (void)testCaptureToFile_handlesTorchMode { FLTCam *cam = FLTCreateCamWithCaptureSessionQueueAndMediaSettings( captureSessionQueue, nil, nil, - ^id(void) { - return [[FLTDefaultCaptureDeviceController alloc] initWithDevice:captureDeviceMock]; + ^NSObject *(void) { + return captureDeviceMock; }, nil); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCameraDeviceDiscoverer.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCameraDeviceDiscoverer.h new file mode 100644 index 000000000000..27f72fdb4098 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCameraDeviceDiscoverer.h @@ -0,0 +1,22 @@ +// 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 camera_avfoundation; +@import AVFoundation; + +NS_ASSUME_NONNULL_BEGIN + +/// Mock implementation of `FLTCameraDeviceDiscovering` protocol which allows injecting a custom +/// implementation for session discovery. +@interface MockCameraDeviceDiscoverer : NSObject + +/// A stub that replaces the default implementation of +/// `discoverySessionWithDeviceTypes:mediaType:position`. +@property(nonatomic, copy) NSArray *> *_Nullable (^discoverySessionStub) + (NSArray *deviceTypes, AVMediaType mediaType, + AVCaptureDevicePosition position); + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCameraDeviceDiscoverer.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCameraDeviceDiscoverer.m new file mode 100644 index 000000000000..f132945338cf --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCameraDeviceDiscoverer.m @@ -0,0 +1,19 @@ +// 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 "MockCameraDeviceDiscoverer.h" + +@implementation MockCameraDeviceDiscoverer + +- (NSArray *> *) + discoverySessionWithDeviceTypes:(NSArray *)deviceTypes + mediaType:(AVMediaType)mediaType + position:(AVCaptureDevicePosition)position { + if (self.discoverySessionStub) { + return self.discoverySessionStub(deviceTypes, mediaType, position); + } + return @[]; +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockCaptureDeviceController.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h similarity index 81% rename from packages/camera/camera_avfoundation/example/ios/RunnerTests/MockCaptureDeviceController.h rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h index bd7b2ee9a58f..89dd1648827e 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockCaptureDeviceController.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h @@ -10,7 +10,10 @@ NS_ASSUME_NONNULL_BEGIN -@interface MockCaptureDeviceController : NSObject +@interface MockCaptureDevice : NSObject + +@property(nonatomic, assign) NSString *uniqueID; + // Position/Orientation @property(nonatomic, assign) AVCaptureDevicePosition position; @@ -95,10 +98,27 @@ NS_ASSUME_NONNULL_BEGIN /// @param duration The maximum frame duration being set @property(nonatomic, copy) void (^setActiveVideoMaxFrameDurationStub)(CMTime duration); -// Input Creation -/// Overrides the default implementation of creating capture input. -/// @param error Error pointer to be set if creation fails -@property(nonatomic, copy) AVCaptureInput * (^createInputStub)(NSError **error); +@end + +/// A mocked implementation of FLTCaptureDeviceInputFactory which allows injecting a custom +/// implementation. +@interface MockCaptureInput : NSObject + +/// This property is re-declared to be read/write to allow setting a mocked value for testing. +@property(nonatomic, strong) NSArray *ports; + +@end + +/// A mocked implementation of FLTCaptureDeviceInputFactory which allows injecting a custom +/// implementation. +@interface MockCaptureDeviceInputFactory : NSObject + +/// Initializes a new instance with the given mock device input. Whenever `deviceInputWithDevice` is +/// called on this instance, it will return the mock device input. +- (nonnull instancetype)initWithMockDeviceInput:(NSObject *)mockDeviceInput; + +/// The mock device input to be returned by `deviceInputWithDevice`. +@property(nonatomic, strong) NSObject *mockDeviceInput; @end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockCaptureDeviceController.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m similarity index 76% rename from packages/camera/camera_avfoundation/example/ios/RunnerTests/MockCaptureDeviceController.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m index 38403656fffa..8114be864b7b 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockCaptureDeviceController.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m @@ -1,9 +1,8 @@ - // 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 "MockCaptureDeviceController.h" +#import "MockCaptureDevice.h" @import camera_avfoundation; #if __has_include() @@ -11,7 +10,7 @@ #endif @import AVFoundation; -@implementation MockCaptureDeviceController +@implementation MockCaptureDevice - (void)setActiveFormat:(AVCaptureDeviceFormat *)format { if (self.setActiveFormatStub) { @@ -101,11 +100,37 @@ - (BOOL)isExposureModeSupported:(AVCaptureExposureMode)mode { return self.exposureModeSupported; } -- (AVCaptureInput *)createInput:(NSError *_Nullable *_Nullable)error { - if (self.createInputStub) { - return self.createInputStub(error); +@synthesize device; + +@end + +@implementation MockCaptureInput +@synthesize ports; +@synthesize input; +@end + +@implementation MockCaptureDeviceInputFactory + +- (nonnull instancetype)init { + self = [super init]; + if (self) { + _mockDeviceInput = [[MockCaptureInput alloc] init]; + } + return self; +} + +- (nonnull instancetype)initWithMockDeviceInput: + (nonnull NSObject *)mockDeviceInput { + self = [super init]; + if (self) { + _mockDeviceInput = mockDeviceInput; } - return NULL; + return self; +} + +- (NSObject *)deviceInputWithDevice:(NSObject *)device + error:(NSError **)error { + return _mockDeviceInput; } @end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.h new file mode 100644 index 000000000000..beb3054b776b --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.h @@ -0,0 +1,33 @@ +// 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 camera_avfoundation; +#if __has_include() +@import camera_avfoundation.Test; +#endif +@import AVFoundation; + +NS_ASSUME_NONNULL_BEGIN + +/// Mock implementation of `FLTCaptureSession` protocol which allows injecting a custom +/// implementation. +@interface MockCaptureSession : NSObject + +// Stubs that are called when the corresponding public method is called. +@property(nonatomic, copy) void (^beginConfigurationStub)(void); +@property(nonatomic, copy) void (^commitConfigurationStub)(void); +@property(nonatomic, copy) void (^startRunningStub)(void); +@property(nonatomic, copy) void (^stopRunningStub)(void); +@property(nonatomic, copy) void (^setSessionPresetStub)(AVCaptureSessionPreset preset); + +// Properties re-declared as read/write so a mocked value can be set during testing. +@property(nonatomic, strong) NSMutableArray *inputs; +@property(nonatomic, strong) NSMutableArray *outputs; +@property(nonatomic, assign) BOOL canSetSessionPreset; +@property(nonatomic, copy) AVCaptureSessionPreset sessionPreset; +@property(nonatomic, assign) BOOL automaticallyConfiguresApplicationAudioSession; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.m new file mode 100644 index 000000000000..eb1dee864958 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureSession.m @@ -0,0 +1,85 @@ +// 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 "MockCaptureSession.h" + +@implementation MockCaptureSession + +- (instancetype)init { + self = [super init]; + if (self) { + _inputs = [NSMutableArray array]; + _outputs = [NSMutableArray array]; + } + return self; +} + +- (void)beginConfiguration { + if (self.beginConfigurationStub) { + self.beginConfigurationStub(); + } +} + +- (void)commitConfiguration { + if (self.commitConfigurationStub) { + self.commitConfigurationStub(); + } +} + +- (void)startRunning { + if (self.startRunningStub) { + self.startRunningStub(); + } +} + +- (void)stopRunning { + if (self.stopRunningStub) { + self.stopRunningStub(); + } +} + +- (BOOL)canSetSessionPreset:(AVCaptureSessionPreset)preset { + return self.canSetSessionPreset; +} + +- (void)addConnection:(nonnull AVCaptureConnection *)connection { +} + +- (void)addInput:(nonnull NSObject *)input { +} + +- (void)addInputWithNoConnections:(nonnull NSObject *)input { +} + +- (void)addOutput:(nonnull AVCaptureOutput *)output { +} + +- (void)addOutputWithNoConnections:(nonnull AVCaptureOutput *)output { +} + +- (BOOL)canAddConnection:(nonnull AVCaptureConnection *)connection { + return YES; +} + +- (BOOL)canAddInput:(nonnull NSObject *)input { + return YES; +} + +- (BOOL)canAddOutput:(nonnull AVCaptureOutput *)output { + return YES; +} + +- (void)removeInput:(nonnull NSObject *)input { +} + +- (void)removeOutput:(nonnull AVCaptureOutput *)output { +} + +- (void)setSessionPreset:(AVCaptureSessionPreset)sessionPreset { + if (self.setSessionPresetStub) { + self.setSessionPresetStub(sessionPreset); + } +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockDeviceOrientationProvider.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.h similarity index 100% rename from packages/camera/camera_avfoundation/example/ios/RunnerTests/MockDeviceOrientationProvider.h rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.h diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/MockDeviceOrientationProvider.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.m similarity index 100% rename from packages/camera/camera_avfoundation/example/ios/RunnerTests/MockDeviceOrientationProvider.m rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockDeviceOrientationProvider.m diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockEventChannel.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockEventChannel.h new file mode 100644 index 000000000000..d92adf35c058 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockEventChannel.h @@ -0,0 +1,21 @@ +// 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 camera_avfoundation; +#if __has_include() +@import camera_avfoundation.Test; +#endif +@import Flutter; + +NS_ASSUME_NONNULL_BEGIN + +/// A mock implementation of `FLTEventChannel` that allows injecting a custom stream handler. +@interface MockEventChannel : NSObject + +/// Overrides the default implementation of setting the stream handler. +@property(nonatomic, copy) void (^setStreamHandlerStub)(NSObject *); + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockEventChannel.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockEventChannel.m new file mode 100644 index 000000000000..7190cfb17714 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockEventChannel.m @@ -0,0 +1,15 @@ +// 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 "MockEventChannel.h" + +@implementation MockEventChannel + +- (void)setStreamHandler:(NSObject *)handler { + if (self.setStreamHandlerStub) { + self.setStreamHandlerStub(handler); + } +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterBinaryMessenger.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterBinaryMessenger.h new file mode 100644 index 000000000000..7150c1516116 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterBinaryMessenger.h @@ -0,0 +1,11 @@ +// 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 Flutter; + +/// Mocked implementation of `FlutterBinaryMessenger` protocol that exists to allow constructing +/// a `CameraPlugin` instance for testing. It contains an empty implementation for all protocol +/// methods. +@interface MockFlutterBinaryMessenger : NSObject +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterBinaryMessenger.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterBinaryMessenger.m new file mode 100644 index 000000000000..a23511dfba5d --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterBinaryMessenger.m @@ -0,0 +1,26 @@ +// 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 "MockFlutterBinaryMessenger.h" + +@implementation MockFlutterBinaryMessenger + +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { +} + +- (void)sendOnChannel:(nonnull NSString *)channel message:(NSData *_Nullable)message { +} + +- (void)sendOnChannel:(nonnull NSString *)channel + message:(NSData *_Nullable)message + binaryReply:(FlutterBinaryReply _Nullable)callback { +} + +- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString *)channel + binaryMessageHandler: + (FlutterBinaryMessageHandler _Nullable)handler { + return 0; +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterTextureRegistry.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterTextureRegistry.h new file mode 100644 index 000000000000..37deb1e08c38 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterTextureRegistry.h @@ -0,0 +1,11 @@ +// 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 Flutter; + +/// Mocked implementation of `FlutterTextureRegistry` protocol that exists to allow constructing +/// a `CameraPlugin` instance for testing. It contains an empty implementation for all protocol +/// methods. +@interface MockFlutterTextureRegistry : NSObject +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterTextureRegistry.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterTextureRegistry.m new file mode 100644 index 000000000000..5a1db894b942 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFlutterTextureRegistry.m @@ -0,0 +1,19 @@ +// 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 "MockFlutterTextureRegistry.h" + +@implementation MockFlutterTextureRegistry + +- (int64_t)registerTexture:(nonnull NSObject *)texture { + return 0; +} + +- (void)textureFrameAvailable:(int64_t)textureId { +} + +- (void)unregisterTexture:(int64_t)textureId { +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockGlobalEventApi.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockGlobalEventApi.h new file mode 100644 index 000000000000..802c3e2d0172 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockGlobalEventApi.h @@ -0,0 +1,21 @@ +// 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 camera_avfoundation; +#if __has_include() +@import camera_avfoundation.Test; +#endif + +/// A mock implementation of `FCPCameraGlobalEventApi` that captures received +/// `deviceOrientationChanged` events and exposes the information whether they were received to the +/// testing code. +@interface MockGlobalEventApi : FCPCameraGlobalEventApi + +/// Whether the `deviceOrientationChanged` callback was called. +@property(nonatomic) BOOL deviceOrientationChangedCalled; + +/// The last orientation received by the `deviceOrientationChanged` callback. +@property(nonatomic) FCPPlatformDeviceOrientation lastOrientation; + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockGlobalEventApi.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockGlobalEventApi.m new file mode 100644 index 000000000000..d6920b89a3b8 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockGlobalEventApi.m @@ -0,0 +1,21 @@ +// 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 "MockGlobalEventApi.h" + +@implementation MockGlobalEventApi +- (void)deviceOrientationChangedOrientation:(FCPPlatformDeviceOrientation)orientation + completion:(void (^)(FlutterError *_Nullable))completion { + self.deviceOrientationChangedCalled = YES; + self.lastOrientation = orientation; + completion(nil); +} + +- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString *)channel + binaryMessageHandler: + (nullable FlutterBinaryMessageHandler)handler { + return 0; +} + +@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m index 169b75ddfbb1..644502072664 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/ThreadSafeEventChannelTests.m @@ -7,15 +7,17 @@ @import camera_avfoundation.Test; #endif @import XCTest; -#import + +#import "MockEventChannel.h" @interface ThreadSafeEventChannelTests : XCTestCase + @end @implementation ThreadSafeEventChannelTests - (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { - FlutterEventChannel *mockEventChannel = OCMClassMock([FlutterEventChannel class]); + MockEventChannel *mockEventChannel = [[MockEventChannel alloc] init]; FLTThreadSafeEventChannel *threadSafeEventChannel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; @@ -24,11 +26,12 @@ - (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { XCTestExpectation *mainThreadCompletionExpectation = [self expectationWithDescription: @"setStreamHandler's completion block must be called on the main thread"]; - OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + + [mockEventChannel setSetStreamHandlerStub:^(NSObject *handler) { if (NSThread.isMainThread) { [mainThreadExpectation fulfill]; } - }); + }]; [threadSafeEventChannel setStreamHandler:nil completion:^{ @@ -40,7 +43,7 @@ - (void)testSetStreamHandler_shouldStayOnMainThreadIfCalledFromMainThread { } - (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThread { - FlutterEventChannel *mockEventChannel = OCMClassMock([FlutterEventChannel class]); + MockEventChannel *mockEventChannel = [[MockEventChannel alloc] init]; FLTThreadSafeEventChannel *threadSafeEventChannel = [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; @@ -49,11 +52,12 @@ - (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThr XCTestExpectation *mainThreadCompletionExpectation = [self expectationWithDescription: @"setStreamHandler's completion block must be called on the main thread"]; - OCMStub([mockEventChannel setStreamHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + + [mockEventChannel setSetStreamHandlerStub:^(NSObject *handler) { if (NSThread.isMainThread) { [mainThreadExpectation fulfill]; } - }); + }]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [threadSafeEventChannel setStreamHandler:nil @@ -67,13 +71,16 @@ - (void)testSetStreamHandler_shouldDispatchToMainThreadIfCalledFromBackgroundThr } - (void)testEventChannel_shouldBeKeptAliveWhenDispatchingBackToMainThread { + MockEventChannel *mockEventChannel = [[MockEventChannel alloc] init]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion should be called."]; + dispatch_async(dispatch_queue_create("test", NULL), ^{ - FLTThreadSafeEventChannel *channel = [[FLTThreadSafeEventChannel alloc] - initWithEventChannel:OCMClassMock([FlutterEventChannel class])]; + FLTThreadSafeEventChannel *channel = + [[FLTThreadSafeEventChannel alloc] initWithEventChannel:mockEventChannel]; - [channel setStreamHandler:OCMOCK_ANY + [channel setStreamHandler:nil completion:^{ [expectation fulfill]; }]; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m index 1bd44969c4cd..71fb384971d7 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m @@ -10,6 +10,7 @@ #import "./include/camera_avfoundation/CameraProperties.h" #import "./include/camera_avfoundation/FLTCam.h" +#import "./include/camera_avfoundation/FLTCameraDeviceDiscovering.h" #import "./include/camera_avfoundation/FLTCameraPermissionManager.h" #import "./include/camera_avfoundation/FLTThreadSafeEventChannel.h" #import "./include/camera_avfoundation/QueueUtils.h" @@ -22,10 +23,14 @@ } @interface CameraPlugin () -@property(readonly, nonatomic) id registry; +@property(readonly, nonatomic) NSObject *registry; @property(readonly, nonatomic) NSObject *messenger; @property(nonatomic) FCPCameraGlobalEventApi *globalEventAPI; @property(readonly, nonatomic) FLTCameraPermissionManager *permissionManager; +@property(readonly, nonatomic) NSObject *deviceDiscoverer; +@property(readonly, nonatomic) CaptureNamedDeviceFactory captureDeviceFactory; +@property(readonly, nonatomic) CaptureSessionFactory captureSessionFactory; +@property(readonly, nonatomic) NSObject *captureDeviceInputFactory; @end @implementation CameraPlugin @@ -38,21 +43,39 @@ + (void)registerWithRegistrar:(NSObject *)registrar { - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger { - return - [self initWithRegistry:registry - messenger:messenger - globalAPI:[[FCPCameraGlobalEventApi alloc] initWithBinaryMessenger:messenger]]; + return [self initWithRegistry:registry + messenger:messenger + globalAPI:[[FCPCameraGlobalEventApi alloc] initWithBinaryMessenger:messenger] + deviceDiscoverer:[[FLTDefaultCameraDeviceDiscoverer alloc] init] + deviceFactory:^NSObject *(NSString *name) { + return [[FLTDefaultCaptureDevice alloc] + initWithDevice:[AVCaptureDevice deviceWithUniqueID:name]]; + } + captureSessionFactory:^NSObject *(void) { + return [[FLTDefaultCaptureSession alloc] + initWithCaptureSession:[[AVCaptureSession alloc] init]]; + } + captureDeviceInputFactory:[[FLTDefaultCaptureDeviceInputFactory alloc] init]]; } - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger - globalAPI:(FCPCameraGlobalEventApi *)globalAPI { + globalAPI:(FCPCameraGlobalEventApi *)globalAPI + deviceDiscoverer:(NSObject *)deviceDiscoverer + deviceFactory:(CaptureNamedDeviceFactory)deviceFactory + captureSessionFactory:(CaptureSessionFactory)captureSessionFactory + captureDeviceInputFactory: + (NSObject *)captureDeviceInputFactory { self = [super init]; NSAssert(self, @"super init cannot be nil"); _registry = registry; _messenger = messenger; _globalEventAPI = globalAPI; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + _deviceDiscoverer = deviceDiscoverer; + _captureDeviceFactory = deviceFactory; + _captureSessionFactory = captureSessionFactory; + _captureDeviceInputFactory = captureDeviceInputFactory; id permissionService = [[FLTDefaultPermissionService alloc] init]; _permissionManager = @@ -117,14 +140,13 @@ - (void)availableCamerasWithCompletion: if (@available(iOS 13.0, *)) { [discoveryDevices addObject:AVCaptureDeviceTypeBuiltInUltraWideCamera]; } - AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession - discoverySessionWithDeviceTypes:discoveryDevices - mediaType:AVMediaTypeVideo - position:AVCaptureDevicePositionUnspecified]; - NSArray *devices = discoverySession.devices; + NSArray *> *devices = + [self.deviceDiscoverer discoverySessionWithDeviceTypes:discoveryDevices + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; NSMutableArray *reply = [[NSMutableArray alloc] initWithCapacity:devices.count]; - for (AVCaptureDevice *device in devices) { + for (NSObject *device in devices) { FCPPlatformCameraLensDirection lensFacing; switch (device.position) { case AVCaptureDevicePositionBack: @@ -477,13 +499,18 @@ - (void)sessionQueueCreateCameraWithName:(NSString *)name FLTCamMediaSettingsAVWrapper *mediaSettingsAVWrapper = [[FLTCamMediaSettingsAVWrapper alloc] init]; + FLTCamConfiguration *camConfiguration = + [[FLTCamConfiguration alloc] initWithMediaSettings:settings + mediaSettingsWrapper:mediaSettingsAVWrapper + captureDeviceFactory:^NSObject *_Nonnull { + return self.captureDeviceFactory(name); + } + captureSessionFactory:_captureSessionFactory + captureSessionQueue:_captureSessionQueue + captureDeviceInputFactory:_captureDeviceInputFactory]; + NSError *error; - FLTCam *cam = [[FLTCam alloc] initWithCameraName:name - mediaSettings:settings - mediaSettingsAVWrapper:mediaSettingsAVWrapper - orientation:[[UIDevice currentDevice] orientation] - captureSessionQueue:self.captureSessionQueue - error:&error]; + FLTCam *cam = [[FLTCam alloc] initWithConfiguration:camConfiguration error:&error]; if (error) { completion(nil, FlutterErrorFromNSError(error)); diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m index 577460b1a868..9007e767b145 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCam.m @@ -9,8 +9,9 @@ @import Flutter; #import -#import "./include/camera_avfoundation/FLTCaptureDeviceControlling.h" +#import "./include/camera_avfoundation/FLTCaptureDevice.h" #import "./include/camera_avfoundation/FLTDeviceOrientationProviding.h" +#import "./include/camera_avfoundation/FLTEventChannel.h" #import "./include/camera_avfoundation/FLTSavePhotoDelegate.h" #import "./include/camera_avfoundation/FLTThreadSafeEventChannel.h" #import "./include/camera_avfoundation/QueueUtils.h" @@ -56,10 +57,10 @@ @interface FLTCam () *videoCaptureSession; +@property(readonly, nonatomic) NSObject *audioCaptureSession; -@property(readonly, nonatomic) AVCaptureInput *captureVideoInput; +@property(readonly, nonatomic) NSObject *captureVideoInput; /// Tracks the latest pixel buffer sent from AVFoundation's sample buffer delegate callback. /// Used to deliver the latest pixel buffer to the flutter engine via the `copyPixelBuffer` API. @property(readwrite, nonatomic) CVPixelBufferRef latestPixelBuffer; @@ -105,7 +106,8 @@ @interface FLTCam () deviceOrientationProvider; +@property(readonly, nonatomic) NSObject *captureDeviceInputFactory; +@property(readonly, nonatomic) NSObject *deviceOrientationProvider; /// Reports the given error message to the Dart side of the plugin. /// /// Can be called from any thread. @@ -116,47 +118,6 @@ @implementation FLTCam NSString *const errorMethod = @"error"; -- (instancetype)initWithCameraName:(NSString *)cameraName - mediaSettings:(FCPPlatformMediaSettings *)mediaSettings - mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper - orientation:(UIDeviceOrientation)orientation - captureSessionQueue:(dispatch_queue_t)captureSessionQueue - error:(NSError **)error { - return [self initWithCameraName:cameraName - mediaSettings:mediaSettings - mediaSettingsAVWrapper:mediaSettingsAVWrapper - orientation:orientation - videoCaptureSession:[[AVCaptureSession alloc] init] - audioCaptureSession:[[AVCaptureSession alloc] init] - captureSessionQueue:captureSessionQueue - error:error]; -} - -- (instancetype)initWithCameraName:(NSString *)cameraName - mediaSettings:(FCPPlatformMediaSettings *)mediaSettings - mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper - orientation:(UIDeviceOrientation)orientation - videoCaptureSession:(AVCaptureSession *)videoCaptureSession - audioCaptureSession:(AVCaptureSession *)audioCaptureSession - captureSessionQueue:(dispatch_queue_t)captureSessionQueue - error:(NSError **)error { - return [self initWithMediaSettings:mediaSettings - mediaSettingsAVWrapper:mediaSettingsAVWrapper - orientation:orientation - videoCaptureSession:videoCaptureSession - audioCaptureSession:videoCaptureSession - captureSessionQueue:captureSessionQueue - captureDeviceFactory:^id(void) { - AVCaptureDevice *device = [AVCaptureDevice deviceWithUniqueID:cameraName]; - return [[FLTDefaultCaptureDeviceController alloc] initWithDevice:device]; - } - videoDimensionsForFormat:^CMVideoDimensions(AVCaptureDeviceFormat *format) { - return CMVideoFormatDescriptionGetDimensions(format.formatDescription); - } - deviceOrientationProvider:[[FLTDefaultDeviceOrientationProvider alloc] init] - error:error]; -} - // Returns frame rate supported by format closest to targetFrameRate. static double bestFrameRateForFormat(AVCaptureDeviceFormat *format, double targetFrameRate) { double bestFrameRate = 0; @@ -178,7 +139,7 @@ static double bestFrameRateForFormat(AVCaptureDeviceFormat *format, double targe // as activeFormat and also updates mediaSettings.framesPerSecond to value which // bestFrameRateForFormat returned for that format. static void selectBestFormatForRequestedFrameRate( - AVCaptureDevice *captureDevice, FCPPlatformMediaSettings *mediaSettings, + NSObject *captureDevice, FCPPlatformMediaSettings *mediaSettings, VideoDimensionsForFormat videoDimensionsForFormat) { CMVideoDimensions targetResolution = videoDimensionsForFormat(captureDevice.activeFormat); double targetFrameRate = mediaSettings.framesPerSecond.doubleValue; @@ -210,35 +171,28 @@ static void selectBestFormatForRequestedFrameRate( mediaSettings.framesPerSecond = @(bestFrameRate); } -- (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings - mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper - orientation:(UIDeviceOrientation)orientation - videoCaptureSession:(AVCaptureSession *)videoCaptureSession - audioCaptureSession:(AVCaptureSession *)audioCaptureSession - captureSessionQueue:(dispatch_queue_t)captureSessionQueue - captureDeviceFactory:(CaptureDeviceFactory)captureDeviceFactory - videoDimensionsForFormat:(VideoDimensionsForFormat)videoDimensionsForFormat - deviceOrientationProvider:(id)deviceOrientationProvider +- (instancetype)initWithConfiguration:(nonnull FLTCamConfiguration *)configuration error:(NSError **)error { self = [super init]; NSAssert(self, @"super init cannot be nil"); - _mediaSettings = mediaSettings; - _mediaSettingsAVWrapper = mediaSettingsAVWrapper; + _mediaSettings = configuration.mediaSettings; + _mediaSettingsAVWrapper = configuration.mediaSettingsWrapper; - _captureSessionQueue = captureSessionQueue; + _captureSessionQueue = configuration.captureSessionQueue; _pixelBufferSynchronizationQueue = dispatch_queue_create("io.flutter.camera.pixelBufferSynchronizationQueue", NULL); _photoIOQueue = dispatch_queue_create("io.flutter.camera.photoIOQueue", NULL); - _videoCaptureSession = videoCaptureSession; - _audioCaptureSession = audioCaptureSession; - _captureDeviceFactory = captureDeviceFactory; - _captureDevice = captureDeviceFactory(); - _videoDimensionsForFormat = videoDimensionsForFormat; + _videoCaptureSession = configuration.videoCaptureSession; + _audioCaptureSession = configuration.audioCaptureSession; + _captureDeviceFactory = configuration.captureDeviceFactory; + _captureDevice = _captureDeviceFactory(); + _captureDeviceInputFactory = configuration.captureDeviceInputFactory; + _videoDimensionsForFormat = configuration.videoDimensionsForFormat; _flashMode = _captureDevice.hasFlash ? FCPPlatformFlashModeAuto : FCPPlatformFlashModeOff; _exposureMode = FCPPlatformExposureModeAuto; _focusMode = FCPPlatformFocusModeAuto; _lockedCaptureOrientation = UIDeviceOrientationUnknown; - _deviceOrientation = orientation; + _deviceOrientation = configuration.orientation; _videoFormat = kCVPixelFormatType_32BGRA; _inProgressSavePhotoDelegates = [NSMutableDictionary dictionary]; _fileFormat = FCPPlatformImageFileFormatJpeg; @@ -270,11 +224,11 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings _motionManager = [[CMMotionManager alloc] init]; [_motionManager startAccelerometerUpdates]; - _deviceOrientationProvider = deviceOrientationProvider; + _deviceOrientationProvider = configuration.deviceOrientationProvider; if (_mediaSettings.framesPerSecond) { // The frame rate can be changed only on a locked for configuration device. - if ([mediaSettingsAVWrapper lockDevice:_captureDevice error:error]) { + if ([_mediaSettingsAVWrapper lockDevice:_captureDevice error:error]) { [_mediaSettingsAVWrapper beginConfigurationForSession:_videoCaptureSession]; // Possible values for presets are hard-coded in FLT interface having @@ -295,8 +249,8 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings int fpsNominator = floor([_mediaSettings.framesPerSecond doubleValue] * 10.0); CMTime duration = CMTimeMake(10, fpsNominator); - [mediaSettingsAVWrapper setMinFrameDuration:duration onDevice:_captureDevice]; - [mediaSettingsAVWrapper setMaxFrameDuration:duration onDevice:_captureDevice]; + [_mediaSettingsAVWrapper setMinFrameDuration:duration onDevice:_captureDevice]; + [_mediaSettingsAVWrapper setMaxFrameDuration:duration onDevice:_captureDevice]; [_mediaSettingsAVWrapper commitConfigurationForSession:_videoCaptureSession]; [_mediaSettingsAVWrapper unlockDevice:_captureDevice]; @@ -318,7 +272,8 @@ - (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings - (AVCaptureConnection *)createConnection:(NSError **)error { // Setup video capture input. - _captureVideoInput = [_captureDevice createInput:error]; + _captureVideoInput = [_captureDeviceInputFactory deviceInputWithDevice:_captureDevice + error:error]; // Test the return value of the `deviceInputWithDevice` method to see whether an error occurred. // Don’t just test to see whether the error pointer was set to point to an error. @@ -597,7 +552,7 @@ - (BOOL)setCaptureSessionPreset:(FCPPlatformResolutionPreset)resolutionPreset /// Finds the highest available resolution in terms of pixel count for the given device. /// Preferred are formats with the same subtype as current activeFormat. - (AVCaptureDeviceFormat *)highestResolutionFormatForCaptureDevice: - (AVCaptureDevice *)captureDevice { + (NSObject *)captureDevice { FourCharCode preferredSubType = CMFormatDescriptionGetMediaSubType(_captureDevice.activeFormat.formatDescription); AVCaptureDeviceFormat *bestFormat = nil; @@ -844,13 +799,13 @@ - (void)newAudioSample:(CMSampleBufferRef)sampleBuffer { - (void)close { [self stop]; for (AVCaptureInput *input in [_videoCaptureSession inputs]) { - [_videoCaptureSession removeInput:input]; + [_videoCaptureSession removeInput:[[FLTDefaultCaptureInput alloc] initWithInput:input]]; } for (AVCaptureOutput *output in [_videoCaptureSession outputs]) { [_videoCaptureSession removeOutput:output]; } for (AVCaptureInput *input in [_audioCaptureSession inputs]) { - [_audioCaptureSession removeInput:input]; + [_audioCaptureSession removeInput:[[FLTDefaultCaptureInput alloc] initWithInput:input]]; } for (AVCaptureOutput *output in [_audioCaptureSession outputs]) { [_audioCaptureSession removeOutput:output]; @@ -1049,7 +1004,7 @@ - (void)applyFocusMode { } - (void)applyFocusMode:(FCPPlatformFocusMode)focusMode - onDevice:(id)captureDevice { + onDevice:(NSObject *)captureDevice { [captureDevice lockForConfiguration:nil]; switch (focusMode) { case FCPPlatformFocusModeLocked: @@ -1214,7 +1169,7 @@ - (void)startImageStreamWithMessenger:(NSObject *)messen - (void)startImageStreamWithMessenger:(NSObject *)messenger imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler { if (!_isStreamingImages) { - FlutterEventChannel *eventChannel = [FlutterEventChannel + id eventChannel = [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera_avfoundation/imageStream" binaryMessenger:messenger]; FLTThreadSafeEventChannel *threadSafeEventChannel = @@ -1416,9 +1371,10 @@ - (void)setUpCaptureSessionForAudioIfNeeded { NSError *error = nil; // Create a device input with the device and add it to the session. // Setup the audio input. - AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; - AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice - error:&error]; + NSObject *audioDevice = [[FLTDefaultCaptureDevice alloc] + initWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]]; + NSObject *audioInput = + [_captureDeviceInputFactory deviceInputWithDevice:audioDevice error:&error]; if (error) { [self reportErrorMessage:error.description]; } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamConfiguration.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamConfiguration.m new file mode 100644 index 000000000000..c9c2a7ca257d --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamConfiguration.m @@ -0,0 +1,34 @@ +// 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 "./include/camera_avfoundation/FLTCamConfiguration.h" + +@implementation FLTCamConfiguration + +- (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings + mediaSettingsWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsWrapper + captureDeviceFactory:(CaptureDeviceFactory)captureDeviceFactory + captureSessionFactory:(CaptureSessionFactory)captureSessionFactory + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + captureDeviceInputFactory: + (NSObject *)captureDeviceInputFactory { + self = [super init]; + if (self) { + _mediaSettings = mediaSettings; + _mediaSettingsWrapper = mediaSettingsWrapper; + _captureSessionQueue = captureSessionQueue; + _videoCaptureSession = captureSessionFactory(); + _audioCaptureSession = captureSessionFactory(); + _captureDeviceFactory = captureDeviceFactory; + _orientation = [[UIDevice currentDevice] orientation]; + _deviceOrientationProvider = [[FLTDefaultDeviceOrientationProvider alloc] init]; + _videoDimensionsForFormat = ^CMVideoDimensions(AVCaptureDeviceFormat *format) { + return CMVideoFormatDescriptionGetDimensions(format.formatDescription); + }; + _captureDeviceInputFactory = captureDeviceInputFactory; + } + return self; +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m index 21a0d9a68eeb..01f18b52ebe7 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCamMediaSettingsAVWrapper.m @@ -3,34 +3,33 @@ // found in the LICENSE file. #import "./include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h" -#import "./include/camera_avfoundation/FLTCaptureDeviceControlling.h" +#import "./include/camera_avfoundation/FLTCaptureDevice.h" +#import "./include/camera_avfoundation/FLTCaptureSession.h" @implementation FLTCamMediaSettingsAVWrapper -- (BOOL)lockDevice:(id)captureDevice +- (BOOL)lockDevice:(NSObject *)captureDevice error:(NSError *_Nullable *_Nullable)outError { return [captureDevice lockForConfiguration:outError]; } -- (void)unlockDevice:(id)captureDevice { +- (void)unlockDevice:(NSObject *)captureDevice { return [captureDevice unlockForConfiguration]; } -- (void)beginConfigurationForSession:(AVCaptureSession *)videoCaptureSession { +- (void)beginConfigurationForSession:(NSObject *)videoCaptureSession { [videoCaptureSession beginConfiguration]; } -- (void)commitConfigurationForSession:(AVCaptureSession *)videoCaptureSession { +- (void)commitConfigurationForSession:(NSObject *)videoCaptureSession { [videoCaptureSession commitConfiguration]; } -- (void)setMinFrameDuration:(CMTime)duration - onDevice:(id)captureDevice { +- (void)setMinFrameDuration:(CMTime)duration onDevice:(NSObject *)captureDevice { captureDevice.activeVideoMinFrameDuration = duration; } -- (void)setMaxFrameDuration:(CMTime)duration - onDevice:(id)captureDevice { +- (void)setMaxFrameDuration:(CMTime)duration onDevice:(NSObject *)captureDevice { captureDevice.activeVideoMaxFrameDuration = duration; } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraDeviceDiscovering.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraDeviceDiscovering.m new file mode 100644 index 000000000000..070908aab928 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraDeviceDiscovering.m @@ -0,0 +1,30 @@ +// 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 Flutter; + +#import "FLTCameraDeviceDiscovering.h" + +@implementation FLTDefaultCameraDeviceDiscoverer + +- (NSArray *> *) + discoverySessionWithDeviceTypes:(NSArray *)deviceTypes + mediaType:(AVMediaType)mediaType + position:(AVCaptureDevicePosition)position { + AVCaptureDeviceDiscoverySession *discoverySession = + [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes + mediaType:mediaType + position:position]; + + NSArray *devices = discoverySession.devices; + NSMutableArray *> *deviceControllers = + [NSMutableArray arrayWithCapacity:devices.count]; + for (AVCaptureDevice *device in devices) { + [deviceControllers addObject:[[FLTDefaultCaptureDevice alloc] initWithDevice:device]]; + } + + return deviceControllers; +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureDeviceControlling.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureDevice.m similarity index 79% rename from packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureDeviceControlling.m rename to packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureDevice.m index 95d3f80519fc..d9badbfe94a4 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureDeviceControlling.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureDevice.m @@ -2,13 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import "./include/camera_avfoundation/FLTCaptureDeviceControlling.h" +@import Flutter; -@interface FLTDefaultCaptureDeviceController () +#import "FLTCaptureDevice.h" + +@interface FLTDefaultCaptureDevice () @property(nonatomic, strong) AVCaptureDevice *device; @end -@implementation FLTDefaultCaptureDeviceController +@implementation FLTDefaultCaptureDevice - (instancetype)initWithDevice:(AVCaptureDevice *)device { self = [super init]; @@ -18,6 +20,11 @@ - (instancetype)initWithDevice:(AVCaptureDevice *)device { return self; } +// Device identifier +- (NSString *)uniqueID { + return self.device.uniqueID; +} + // Position/Orientation - (AVCaptureDevicePosition)position { return self.device.position; @@ -165,8 +172,38 @@ - (void)setActiveVideoMaxFrameDuration:(CMTime)duration { self.device.activeVideoMaxFrameDuration = duration; } -- (AVCaptureInput *)createInput:(NSError *_Nullable *_Nullable)error { - return [AVCaptureDeviceInput deviceInputWithDevice:_device error:error]; +@end + +@interface FLTDefaultCaptureInput () +@property(nonatomic, strong) AVCaptureInput *input; +@end + +@implementation FLTDefaultCaptureInput + +- (instancetype)initWithInput:(AVCaptureInput *)input { + self = [super init]; + if (self) { + _input = input; + } + return self; +} + +- (AVCaptureInput *)input { + return _input; +} + +- (NSArray *)ports { + return self.input.ports; +} + +@end + +@implementation FLTDefaultCaptureDeviceInputFactory + +- (NSObject *)deviceInputWithDevice:(NSObject *)device + error:(NSError **)error { + return [[FLTDefaultCaptureInput alloc] + initWithInput:[AVCaptureDeviceInput deviceInputWithDevice:device.device error:error]]; } @end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureSession.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureSession.m new file mode 100644 index 000000000000..4812d883476d --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCaptureSession.m @@ -0,0 +1,105 @@ +// 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 "FLTCaptureSession.h" + +@interface FLTDefaultCaptureSession () +@property(nonatomic, strong) AVCaptureSession *captureSession; +@end + +@implementation FLTDefaultCaptureSession + +- (instancetype)initWithCaptureSession:(AVCaptureSession *)session { + self = [super init]; + if (self) { + _captureSession = session; + } + return self; +} + +- (void)beginConfiguration { + [_captureSession beginConfiguration]; +} + +- (void)commitConfiguration { + [_captureSession commitConfiguration]; +} + +- (void)startRunning { + [_captureSession startRunning]; +} + +- (void)stopRunning { + [_captureSession stopRunning]; +} + +- (BOOL)automaticallyConfiguresApplicationAudioSession { + return _captureSession.automaticallyConfiguresApplicationAudioSession; +} + +- (void)setAutomaticallyConfiguresApplicationAudioSession:(BOOL)value { + _captureSession.automaticallyConfiguresApplicationAudioSession = value; +} + +- (BOOL)canSetSessionPreset:(AVCaptureSessionPreset)preset { + return [_captureSession canSetSessionPreset:preset]; +} + +- (void)addInputWithNoConnections:(NSObject *)input { + [_captureSession addInputWithNoConnections:input.input]; +} + +- (void)addOutputWithNoConnections:(AVCaptureOutput *)output { + [_captureSession addOutputWithNoConnections:output]; +} + +- (void)addConnection:(AVCaptureConnection *)connection { + [_captureSession addConnection:connection]; +} + +- (void)addOutput:(AVCaptureOutput *)output { + [_captureSession addOutput:output]; +} + +- (void)removeInput:(NSObject *)input { + [_captureSession removeInput:input.input]; +} + +- (void)removeOutput:(AVCaptureOutput *)output { + [_captureSession removeOutput:output]; +} + +- (void)setSessionPreset:(AVCaptureSessionPreset)sessionPreset { + _captureSession.sessionPreset = sessionPreset; +} + +- (AVCaptureSessionPreset)sessionPreset { + return _captureSession.sessionPreset; +} + +- (NSArray *)inputs { + return _captureSession.inputs; +} + +- (NSArray *)outputs { + return _captureSession.outputs; +} + +- (BOOL)canAddInput:(NSObject *)input { + return [_captureSession canAddInput:input.input]; +} + +- (BOOL)canAddOutput:(AVCaptureOutput *)output { + return [_captureSession canAddOutput:output]; +} + +- (BOOL)canAddConnection:(AVCaptureConnection *)connection { + return [_captureSession canAddConnection:connection]; +} + +- (void)addInput:(NSObject *)input { + [_captureSession addInput:input.input]; +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTThreadSafeEventChannel.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTThreadSafeEventChannel.m index 53c7273a5901..ceae1600e85e 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTThreadSafeEventChannel.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTThreadSafeEventChannel.m @@ -3,15 +3,16 @@ // found in the LICENSE file. #import "./include/camera_avfoundation/FLTThreadSafeEventChannel.h" +#import "./include/camera_avfoundation/FLTEventChannel.h" #import "./include/camera_avfoundation/QueueUtils.h" @interface FLTThreadSafeEventChannel () -@property(nonatomic, strong) FlutterEventChannel *channel; +@property(nonatomic, strong) id channel; @end @implementation FLTThreadSafeEventChannel -- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel { +- (instancetype)initWithEventChannel:(id)channel { self = [super init]; if (self) { _channel = channel; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap index 899a43a3cab1..394fba5d5707 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap @@ -5,13 +5,17 @@ framework module camera_avfoundation { module * { export * } explicit module Test { - header "FLTCaptureDeviceControlling.h" + header "FLTCameraDeviceDiscovering.h" + header "FLTCaptureDevice.h" + header "FLTCaptureSession.h" header "FLTDeviceOrientationProviding.h" + header "FLTEventChannel.h" header "FLTPermissionServicing.h" header "CameraPlugin_Test.h" header "CameraProperties.h" header "FLTCam.h" header "FLTCam_Test.h" + header "FLTCamConfiguration.h" header "FLTSavePhotoDelegate_Test.h" header "FLTThreadSafeEventChannel.h" header "FLTCameraPermissionManager.h" diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPlugin_Test.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPlugin_Test.h index c29c2f306db8..0e119c3bb144 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPlugin_Test.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPlugin_Test.h @@ -6,8 +6,15 @@ #import "CameraPlugin.h" #import "FLTCam.h" +#import "FLTCamConfiguration.h" +#import "FLTCameraDeviceDiscovering.h" +#import "FLTCaptureDevice.h" #import "messages.g.h" +NS_ASSUME_NONNULL_BEGIN + +typedef NSObject *_Nonnull (^CaptureNamedDeviceFactory)(NSString *name); + /// APIs exposed for unit testing. @interface CameraPlugin () @@ -15,7 +22,7 @@ @property(nonatomic, strong) dispatch_queue_t captureSessionQueue; /// An internal camera object that manages camera's state and performs camera operations. -@property(nonatomic, strong) FLTCam *camera; +@property(nonatomic, strong) FLTCam *_Nullable camera; /// Inject @p FlutterTextureRegistry and @p FlutterBinaryMessenger for unit testing. - (instancetype)initWithRegistry:(NSObject *)registry @@ -25,7 +32,12 @@ /// unit testing. - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger - globalAPI:(FCPCameraGlobalEventApi *)globalAPI NS_DESIGNATED_INITIALIZER; + globalAPI:(FCPCameraGlobalEventApi *)globalAPI + deviceDiscoverer:(id)deviceDiscoverer + deviceFactory:(CaptureNamedDeviceFactory)deviceFactory + captureSessionFactory:(CaptureSessionFactory)captureSessionFactory + captureDeviceInputFactory:(id)captureDeviceInputFactory + NS_DESIGNATED_INITIALIZER; /// Hide the default public constructor. - (instancetype)init NS_UNAVAILABLE; @@ -42,5 +54,8 @@ /// @param completion the callback to inform the Dart side of the plugin of creation. - (void)createCameraOnSessionQueueWithName:(NSString *)name settings:(FCPPlatformMediaSettings *)settings - completion:(void (^)(NSNumber *, FlutterError *))completion; + completion:(void (^)(NSNumber *_Nullable, + FlutterError *_Nullable))completion; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h index 68ab3a861581..6e22ad5b1ac1 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam.h @@ -7,8 +7,10 @@ @import Flutter; #import "CameraProperties.h" +#import "FLTCamConfiguration.h" #import "FLTCamMediaSettingsAVWrapper.h" -#import "FLTCaptureDeviceControlling.h" +#import "FLTCaptureDevice.h" +#import "FLTDeviceOrientationProviding.h" #import "messages.g.h" NS_ASSUME_NONNULL_BEGIN @@ -16,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN /// A class that manages camera's state and performs camera operations. @interface FLTCam : NSObject -@property(readonly, nonatomic) id captureDevice; +@property(readonly, nonatomic) NSObject *captureDevice; @property(readonly, nonatomic) CGSize previewSize; @property(assign, nonatomic) BOOL isPreviewPaused; @property(nonatomic, copy) void (^onFrameAvailable)(void); @@ -32,20 +34,9 @@ NS_ASSUME_NONNULL_BEGIN @property(assign, nonatomic) CGFloat minimumAvailableZoomFactor; @property(assign, nonatomic) CGFloat maximumAvailableZoomFactor; -/// Initializes an `FLTCam` instance. -/// @param cameraName a name used to uniquely identify the camera. -/// @param mediaSettings the media settings configuration parameters -/// @param mediaSettingsAVWrapper AVFoundation wrapper to perform media settings related operations -/// (for dependency injection in unit tests). -/// @param orientation the orientation of camera -/// @param captureSessionQueue the queue on which camera's capture session operations happen. +/// Initializes an `FLTCam` instance with the given configuration. /// @param error report to the caller if any error happened creating the camera. -- (instancetype)initWithCameraName:(NSString *)cameraName - mediaSettings:(FCPPlatformMediaSettings *)mediaSettings - mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper - orientation:(UIDeviceOrientation)orientation - captureSessionQueue:(dispatch_queue_t)captureSessionQueue - error:(NSError **)error; +- (instancetype)initWithConfiguration:(FLTCamConfiguration *)configuration error:(NSError **)error; /// Informs the Dart side of the plugin of the current camera state and capabilities. - (void)reportInitializationState; @@ -94,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN /// @param focusMode The focus mode that should be applied to the @captureDevice instance. /// @param captureDevice The AVCaptureDevice to which the @focusMode will be applied. - (void)applyFocusMode:(FCPPlatformFocusMode)focusMode - onDevice:(id)captureDevice; + onDevice:(NSObject *)captureDevice; - (void)pausePreview; - (void)resumePreview; - (void)setDescriptionWhileRecording:(NSString *)cameraName diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamConfiguration.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamConfiguration.h new file mode 100644 index 000000000000..36393009b7a3 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamConfiguration.h @@ -0,0 +1,53 @@ +// 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 AVFoundation; +@import Foundation; +@import Flutter; + +#import "CameraProperties.h" +#import "FLTCamMediaSettingsAVWrapper.h" +#import "FLTCaptureDevice.h" +#import "FLTCaptureSession.h" +#import "FLTDeviceOrientationProviding.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Factory block returning an AVCaptureDevice. +/// Used in tests to inject a device into FLTCam. +typedef NSObject *_Nonnull (^CaptureDeviceFactory)(void); + +typedef NSObject *_Nonnull (^CaptureSessionFactory)(void); + +/// Determines the video dimensions (width and height) for a given capture device format. +/// Used in tests to mock CMVideoFormatDescriptionGetDimensions. +typedef CMVideoDimensions (^VideoDimensionsForFormat)(AVCaptureDeviceFormat *); + +/// A configuration object that centralizes dependencies for `FLTCam`. +@interface FLTCamConfiguration : NSObject + +/// Initializes a new camera configuration with specified media settings and factories. +- (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings + mediaSettingsWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsWrapper + captureDeviceFactory:(CaptureDeviceFactory)captureDeviceFactory + captureSessionFactory:(CaptureSessionFactory)captureSessionFactory + captureSessionQueue:(dispatch_queue_t)captureSessionQueue + captureDeviceInputFactory: + (NSObject *)captureDeviceInputFactory; + +@property(nonatomic, strong) id deviceOrientationProvider; +@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; +@property(nonatomic, strong) FCPPlatformMediaSettings *mediaSettings; +@property(nonatomic, strong) FLTCamMediaSettingsAVWrapper *mediaSettingsWrapper; +@property(nonatomic, copy) CaptureDeviceFactory captureDeviceFactory; +@property(nonatomic, copy) CaptureDeviceFactory audioCaptureDeviceFactory; +@property(nonatomic, copy) VideoDimensionsForFormat videoDimensionsForFormat; +@property(nonatomic, assign) UIDeviceOrientation orientation; +@property(nonatomic, strong) NSObject *videoCaptureSession; +@property(nonatomic, strong) NSObject *audioCaptureSession; +@property(nonatomic, strong) NSObject *captureDeviceInputFactory; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h index 8d95cf02afe0..fb8d7040d679 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCamMediaSettingsAVWrapper.h @@ -5,7 +5,8 @@ @import AVFoundation; @import Foundation; -#import "FLTCaptureDeviceControlling.h" +#import "FLTCaptureDevice.h" +#import "FLTCaptureSession.h" NS_ASSUME_NONNULL_BEGIN @@ -27,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN * @param outError The optional error. * @result A BOOL indicating whether the device was successfully locked for configuration. */ -- (BOOL)lockDevice:(id)captureDevice +- (BOOL)lockDevice:(NSObject *)captureDevice error:(NSError *_Nullable *_Nullable)outError; /** @@ -35,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN * @abstract Release exclusive control over device hardware properties. * @param captureDevice The capture device. */ -- (void)unlockDevice:(id)captureDevice; +- (void)unlockDevice:(NSObject *)captureDevice; /** * @method beginConfigurationForSession: @@ -43,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN * operations on a running session into atomic updates. * @param videoCaptureSession The video capture session. */ -- (void)beginConfigurationForSession:(AVCaptureSession *)videoCaptureSession; +- (void)beginConfigurationForSession:(NSObject *)videoCaptureSession; /** * @method commitConfigurationForSession: @@ -51,7 +52,7 @@ NS_ASSUME_NONNULL_BEGIN * operations on a running session into atomic updates. * @param videoCaptureSession The video capture session. */ -- (void)commitConfigurationForSession:(AVCaptureSession *)videoCaptureSession; +- (void)commitConfigurationForSession:(NSObject *)videoCaptureSession; /** * @method setMinFrameDuration:onDevice: @@ -60,8 +61,7 @@ NS_ASSUME_NONNULL_BEGIN * @param duration The frame duration. * @param captureDevice The capture device */ -- (void)setMinFrameDuration:(CMTime)duration - onDevice:(id)captureDevice; +- (void)setMinFrameDuration:(CMTime)duration onDevice:(NSObject *)captureDevice; /** * @method setMaxFrameDuration:onDevice: @@ -70,8 +70,7 @@ NS_ASSUME_NONNULL_BEGIN * @param duration The frame duration. * @param captureDevice The capture device */ -- (void)setMaxFrameDuration:(CMTime)duration - onDevice:(id)captureDevice; +- (void)setMaxFrameDuration:(CMTime)duration onDevice:(NSObject *)captureDevice; /** * @method assetWriterAudioInputWithOutputSettings: diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h index 7e27e31a8880..5651755d8958 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCam_Test.h @@ -3,18 +3,10 @@ // found in the LICENSE file. #import "FLTCam.h" -#import "FLTCaptureDeviceControlling.h" +#import "FLTCaptureDevice.h" #import "FLTDeviceOrientationProviding.h" #import "FLTSavePhotoDelegate.h" -/// Determines the video dimensions (width and height) for a given capture device format. -/// Used in tests to mock CMVideoFormatDescriptionGetDimensions. -typedef CMVideoDimensions (^VideoDimensionsForFormat)(AVCaptureDeviceFormat *); - -/// Factory block returning an AVCaptureDevice. -/// Used in tests to inject a device into FLTCam. -typedef id (^CaptureDeviceFactory)(void); - @interface FLTImageStreamHandler : NSObject /// The queue on which `eventSink` property should be accessed. @@ -54,31 +46,6 @@ typedef id (^CaptureDeviceFactory)(void); didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection; -/// Initializes a camera instance. -/// Allows for injecting dependencies that are usually internal. -- (instancetype)initWithCameraName:(NSString *)cameraName - mediaSettings:(FCPPlatformMediaSettings *)mediaSettings - mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper - orientation:(UIDeviceOrientation)orientation - videoCaptureSession:(AVCaptureSession *)videoCaptureSession - audioCaptureSession:(AVCaptureSession *)audioCaptureSession - captureSessionQueue:(dispatch_queue_t)captureSessionQueue - error:(NSError **)error; - -/// Initializes a camera instance. -/// Allows for testing with specified resolution, audio preference, orientation, -/// and direct access to capture sessions and blocks. -- (instancetype)initWithMediaSettings:(FCPPlatformMediaSettings *)mediaSettings - mediaSettingsAVWrapper:(FLTCamMediaSettingsAVWrapper *)mediaSettingsAVWrapper - orientation:(UIDeviceOrientation)orientation - videoCaptureSession:(AVCaptureSession *)videoCaptureSession - audioCaptureSession:(AVCaptureSession *)audioCaptureSession - captureSessionQueue:(dispatch_queue_t)captureSessionQueue - captureDeviceFactory:(CaptureDeviceFactory)captureDeviceFactory - videoDimensionsForFormat:(VideoDimensionsForFormat)videoDimensionsForFormat - deviceOrientationProvider:(id)deviceOrientationProvider - error:(NSError **)error; - /// Start streaming images. - (void)startImageStreamWithMessenger:(NSObject *)messenger imageStreamHandler:(FLTImageStreamHandler *)imageStreamHandler; diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCameraDeviceDiscovering.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCameraDeviceDiscovering.h new file mode 100644 index 000000000000..e953585f4f55 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCameraDeviceDiscovering.h @@ -0,0 +1,25 @@ +// 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 AVFoundation; + +#import "FLTCaptureDevice.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol which abstracts the discovery of camera devices. +/// It is a thin wrapper around `AVCaptureDiscoverySession` and it exists to allow mocking in tests. +@protocol FLTCameraDeviceDiscovering +- (NSArray *> *) + discoverySessionWithDeviceTypes:(NSArray *)deviceTypes + mediaType:(AVMediaType)mediaType + position:(AVCaptureDevicePosition)position; +@end + +/// The default implementation of the `FLTCameraDeviceDiscovering` protocol. +/// It wraps a call to `AVCaptureDeviceDiscoverySession`. +@interface FLTDefaultCameraDeviceDiscoverer : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureDeviceControlling.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureDevice.h similarity index 51% rename from packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureDeviceControlling.h rename to packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureDevice.h index 38f79408959f..09344600a86d 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureDeviceControlling.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureDevice.h @@ -9,7 +9,15 @@ NS_ASSUME_NONNULL_BEGIN /// A protocol which is a direct passthrough to AVCaptureDevice. /// It exists to allow replacing AVCaptureDevice in tests. -@protocol FLTCaptureDeviceControlling +@protocol FLTCaptureDevice + +/// Underlying `AVCaptureDevice` instance. This is should not be used directly +/// in the plugin implementation code, but it exists so that other protocol default +/// implementation can pass the raw device to AVFoundation methods. +@property(nonatomic, readonly) AVCaptureDevice *device; + +// Device identifier +@property(nonatomic, readonly) NSString *uniqueID; // Position/Orientation - (AVCaptureDevicePosition)position; @@ -63,17 +71,41 @@ NS_ASSUME_NONNULL_BEGIN - (CMTime)activeVideoMaxFrameDuration; - (void)setActiveVideoMaxFrameDuration:(CMTime)duration; -- (AVCaptureInput *)createInput:(NSError *_Nullable *_Nullable)error; +@end + +/// A protocol which is a direct passthrough to AVCaptureInput. +/// It exists to allow replacing AVCaptureInput in tests. +@protocol FLTCaptureInput + +/// Underlying input instance. It is exposed as raw AVCaptureInput has to be passed to some +/// AVFoundation methods. The plugin implementation code shouldn't use it though. +@property(nonatomic, readonly) AVCaptureInput *input; +@property(nonatomic, readonly) NSArray *ports; @end -/// A default implementation of FLTCaptureDeviceControlling protocol which -/// wraps an instance of AVCaptureDevice. -@interface FLTDefaultCaptureDeviceController : NSObject +/// A protocol which wraps the creation of AVCaptureDeviceInput. +/// It exists to allow mocking instances of AVCaptureDeviceInput in tests. +@protocol FLTCaptureDeviceInputFactory +- (nullable NSObject *)deviceInputWithDevice:(NSObject *)device + error:(NSError **)error; +@end -/// Initializes the controller with the given device. +/// A default implementation of `FLTCaptureDevice` which is a direct passthrough to the underlying +/// `AVCaptureDevice`. +@interface FLTDefaultCaptureDevice : NSObject - (instancetype)initWithDevice:(AVCaptureDevice *)device; +@end + +/// A default implementation of `FLTCaptureInput` which is a direct passthrough to the underlying +/// `AVCaptureInput`. +@interface FLTDefaultCaptureInput : NSObject +- (instancetype)initWithInput:(AVCaptureInput *)input; +@end +/// A default implementation of FLTCaptureDeviceInputFactory protocol which +/// wraps a call to AVCaptureInput static method `deviceInputWithDevice`. +@interface FLTDefaultCaptureDeviceInputFactory : NSObject @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureSession.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureSession.h new file mode 100644 index 000000000000..8d8aa21816e4 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCaptureSession.h @@ -0,0 +1,44 @@ +// 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 AVFoundation; + +#import "FLTCaptureDevice.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol which is a direct passthrough to AVCaptureSession. +/// It exists to allow replacing AVCaptureSession in tests. +@protocol FLTCaptureSession + +@property(nonatomic, copy) AVCaptureSessionPreset sessionPreset; +@property(nonatomic, readonly) NSArray *inputs; +@property(nonatomic, readonly) NSArray *outputs; +@property(nonatomic, assign) BOOL automaticallyConfiguresApplicationAudioSession; + +- (void)beginConfiguration; +- (void)commitConfiguration; +- (void)startRunning; +- (void)stopRunning; +- (BOOL)canSetSessionPreset:(AVCaptureSessionPreset)preset; +- (void)addInputWithNoConnections:(NSObject *)input; +- (void)addOutputWithNoConnections:(AVCaptureOutput *)output; +- (void)addConnection:(AVCaptureConnection *)connection; +- (void)addOutput:(AVCaptureOutput *)output; +- (void)removeInput:(NSObject *)input; +- (void)removeOutput:(AVCaptureOutput *)output; +- (BOOL)canAddInput:(NSObject *)input; +- (BOOL)canAddOutput:(AVCaptureOutput *)output; +- (BOOL)canAddConnection:(AVCaptureConnection *)connection; +- (void)addInput:(NSObject *)input; + +@end + +/// A default implementation of `FLTCaptureSession` which is a direct passthrough +/// to the underlying `AVCaptureSession`. +@interface FLTDefaultCaptureSession : NSObject +- (instancetype)initWithCaptureSession:(AVCaptureSession *)session; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTEventChannel.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTEventChannel.h new file mode 100644 index 000000000000..6dc6b4e4e390 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTEventChannel.h @@ -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. + +@import Flutter; + +NS_ASSUME_NONNULL_BEGIN + +/// A protocol which is a direct passthrough to FlutterStreamHandler. +/// It exists to allow replacing FlutterStreamHandler in tests. +@protocol FLTEventChannel +- (void)setStreamHandler:(nullable NSObject *)handler; +@end + +@interface FlutterEventChannel (FLTEventChannel) +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTThreadSafeEventChannel.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTThreadSafeEventChannel.h index 20a1d4023a31..008e2279be01 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTThreadSafeEventChannel.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTThreadSafeEventChannel.h @@ -4,6 +4,8 @@ #import +#import "FLTEventChannel.h" + NS_ASSUME_NONNULL_BEGIN /// A thread safe wrapper for FlutterEventChannel that can be called from any thread, by dispatching @@ -12,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN /// Creates a FLTThreadSafeEventChannel by wrapping a FlutterEventChannel object. /// @param channel The FlutterEventChannel object to be wrapped. -- (instancetype)initWithEventChannel:(FlutterEventChannel *)channel; +- (instancetype)initWithEventChannel:(NSObject *)channel; /// Registers a handler on the main thread for stream setup requests from the Flutter side. /// The completion block runs on the main thread. diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index d5546a0fafb7..2c5c86e5b293 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.18+1 +version: 0.9.18+2 environment: sdk: ^3.4.0