diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 413aa6b6809..0890c7f7314 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.18+9 + +* Backfills unit tests for `CameraPlugin` class. +* Adds `minimumExposureOffset` and `maximumExposureOffset` methods to `FLTCam` class. + ## 0.9.18+8 * Migrates unit tests to Swift. diff --git a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 575ae7828f2..4fd11717f7d 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -49,6 +49,10 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */; }; E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */; }; + E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */; }; + E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */; }; + E1FFEAAF2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */; }; + E1FFEAB12D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -141,6 +145,10 @@ B61D98BBC8FB276D1C4A7BB2 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CameraTestUtils.h; sourceTree = ""; }; E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraTestUtils.m; sourceTree = ""; }; + E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCameraPermissionManager.swift; sourceTree = ""; }; + E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCam.swift; sourceTree = ""; }; + E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginCreateCameraTests.swift; sourceTree = ""; }; + E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginInitializeCameraTests.swift; sourceTree = ""; }; E67C6DBF6478BE708993169F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; ECAF63F924EFA2D68883BA85 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -193,6 +201,8 @@ 978D90B32D5F630300CD817E /* StreamingTests.swift */, 97922B0C2D6380C300A9B4CF /* SampleBufferTests.swift */, 978296CE2D5F744B0009BDD3 /* PhotoCaptureTests.swift */, + E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */, + E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -215,8 +225,8 @@ 7F8FD22E2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m */, 7F8FD22A2D4D07A6001AF2C1 /* MockFlutterTextureRegistry.h */, 7F8FD22B2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m */, - 7F8FD2282D4BFABF001AF2C1 /* MockGlobalEventApi.m */, 7F8FD2272D4BFA8D001AF2C1 /* MockGlobalEventApi.h */, + 7F8FD2282D4BFABF001AF2C1 /* MockGlobalEventApi.m */, 7FD83D292D5BA49100F4DB7C /* MockCaptureConnection.h */, 7FD83D2A2D5BA65B00F4DB7C /* MockCaptureConnection.m */, 7FCEDD312D43C2B900EA1CA8 /* MockCaptureDevice.h */, @@ -227,12 +237,14 @@ 7FD5821F2D579ECC003B1200 /* MockCapturePhotoOutput.m */, 7FCEDD332D43C2B900EA1CA8 /* MockDeviceOrientationProvider.h */, 7FCEDD342D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m */, - 7F29EB282D26A59000740257 /* MockCameraDeviceDiscoverer.m */, 7F29EB272D26A55300740257 /* MockCameraDeviceDiscoverer.h */, + 7F29EB282D26A59000740257 /* MockCameraDeviceDiscoverer.m */, 7F29EB3E2D281C5800740257 /* MockCaptureSession.h */, 7F29EB402D281C7E00740257 /* MockCaptureSession.m */, - 970ADABD2D6740A900EFDCD9 /* MockWritableData.swift */, + E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */, 970ADABF2D6764CC00EFDCD9 /* MockEventChannel.swift */, + E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */, + 970ADABD2D6740A900EFDCD9 /* MockWritableData.swift */, ); path = Mocks; sourceTree = ""; @@ -523,6 +535,7 @@ files = ( 97BD4A0E2D5CC5AE00F857D5 /* CameraSettingsTests.swift in Sources */, 972CA92D2D5A28C4004B846F /* QueueUtilsTests.swift in Sources */, + E1FFEAB12D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift in Sources */, 979B3DFB2D5B6BC7009BDE1A /* ExceptionCatcher.m in Sources */, 7FD83D2B2D5BA65B00F4DB7C /* MockCaptureConnection.m in Sources */, 977A25242D5A511600931E34 /* CameraPermissionTests.swift in Sources */, @@ -531,16 +544,19 @@ 7FD582352D57D97C003B1200 /* MockCaptureDeviceFormat.m in Sources */, 979B3DFE2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift in Sources */, 7F8FD22F2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m in Sources */, + E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */, 97922B0D2D6380C300A9B4CF /* SampleBufferTests.swift in Sources */, 972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */, E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, 978296CF2D5F744B0009BDD3 /* PhotoCaptureTests.swift in Sources */, 7FD582202D579ECC003B1200 /* MockCapturePhotoOutput.m in Sources */, 979B3E002D5B9E6C009BDE1A /* CameraMethodChannelTests.swift in Sources */, + E1FFEAAF2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift in Sources */, 97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */, 970ADAC02D6764CC00EFDCD9 /* MockEventChannel.swift in Sources */, 977A25202D5A439300931E34 /* AvailableCamerasTests.swift in Sources */, 972CA9312D5A366C004B846F /* CameraExposureTests.swift in Sources */, + E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */, 7F29EB292D26A59000740257 /* MockCameraDeviceDiscoverer.m in Sources */, 97BD4A102D5CE13500F857D5 /* CameraSessionPresetsTests.swift in Sources */, 7FD582272D57C020003B1200 /* MockAssetWriter.m in Sources */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift index 436ab08bf6d..e86c51dfd87 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/AvailableCamerasTests.swift @@ -15,6 +15,7 @@ final class AvailableCamerasTest: XCTestCase { messenger: MockFlutterBinaryMessenger(), globalAPI: MockGlobalEventApi(), deviceDiscoverer: deviceDiscoverer, + permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { MockCaptureSession() }, captureDeviceInputFactory: MockCaptureDeviceInputFactory() @@ -26,35 +27,35 @@ final class AvailableCamerasTest: XCTestCase { let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer) let expectation = self.expectation(description: "Result finished") - // iPhone 13 Cameras: - let wideAngleCamera = MockCaptureDevice() - wideAngleCamera.uniqueID = "0" - wideAngleCamera.position = .back - - let frontFacingCamera = MockCaptureDevice() - frontFacingCamera.uniqueID = "1" - frontFacingCamera.position = .front - - let ultraWideCamera = MockCaptureDevice() - ultraWideCamera.uniqueID = "2" - ultraWideCamera.position = .back - - let telephotoCamera = MockCaptureDevice() - telephotoCamera.uniqueID = "3" - telephotoCamera.position = .back - - var requiredTypes: [AVCaptureDevice.DeviceType] = [ - .builtInWideAngleCamera, .builtInTelephotoCamera, - ] - if #available(iOS 13.0, *) { - requiredTypes.append(.builtInUltraWideCamera) - } - var cameras: [MockCaptureDevice] = [wideAngleCamera, frontFacingCamera, telephotoCamera] - if #available(iOS 13.0, *) { - cameras.append(ultraWideCamera) - } - mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in + // iPhone 13 Cameras: + let wideAngleCamera = MockCaptureDevice() + wideAngleCamera.uniqueID = "0" + wideAngleCamera.position = .back + + let frontFacingCamera = MockCaptureDevice() + frontFacingCamera.uniqueID = "1" + frontFacingCamera.position = .front + + let ultraWideCamera = MockCaptureDevice() + ultraWideCamera.uniqueID = "2" + ultraWideCamera.position = .back + + let telephotoCamera = MockCaptureDevice() + telephotoCamera.uniqueID = "3" + telephotoCamera.position = .back + + var requiredTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, .builtInTelephotoCamera, + ] + if #available(iOS 13.0, *) { + requiredTypes.append(.builtInUltraWideCamera) + } + var cameras = [wideAngleCamera, frontFacingCamera, telephotoCamera] + if #available(iOS 13.0, *) { + cameras.append(ultraWideCamera) + } + XCTAssertEqual(deviceTypes, requiredTypes) XCTAssertEqual(mediaType, .video) XCTAssertEqual(position, .unspecified) @@ -77,29 +78,65 @@ final class AvailableCamerasTest: XCTestCase { } } - func testAvailableCamerasShouldReturnOneCameraOnSingleCameraIPhone() { + func testAvailableCamerasShouldReturnTwoCamerasOnDualCameraIPhone() { let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer) let expectation = self.expectation(description: "Result finished") - // iPhone 8 Cameras: - let wideAngleCamera = MockCaptureDevice() - wideAngleCamera.uniqueID = "0" - wideAngleCamera.position = .back + mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in + // iPhone 8 Cameras: + let wideAngleCamera = MockCaptureDevice() + wideAngleCamera.uniqueID = "0" + wideAngleCamera.position = .back + + let frontFacingCamera = MockCaptureDevice() + frontFacingCamera.uniqueID = "1" + frontFacingCamera.position = .front + + var requiredTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, .builtInTelephotoCamera, + ] + if #available(iOS 13.0, *) { + requiredTypes.append(.builtInUltraWideCamera) + } + let cameras = [wideAngleCamera, frontFacingCamera] - let frontFacingCamera = MockCaptureDevice() - frontFacingCamera.uniqueID = "1" - frontFacingCamera.position = .front + XCTAssertEqual(deviceTypes, requiredTypes) + XCTAssertEqual(mediaType, .video) + XCTAssertEqual(position, .unspecified) + return cameras + } - var requiredTypes: [AVCaptureDevice.DeviceType] = [ - .builtInWideAngleCamera, .builtInTelephotoCamera, - ] - if #available(iOS 13.0, *) { - requiredTypes.append(.builtInUltraWideCamera) + var resultValue: [FCPPlatformCameraDescription]? + cameraPlugin.availableCameras { result, error in + XCTAssertNil(error) + resultValue = result + expectation.fulfill() } - let cameras: [MockCaptureDevice] = [wideAngleCamera, frontFacingCamera] + waitForExpectations(timeout: 30, handler: nil) + + // Verify the result. + XCTAssertEqual(resultValue?.count, 2) + } + + func testAvailableCamerasShouldReturnExternalLensDirectionForUnspecifiedCameraPosition() { + let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + let cameraPlugin = createCameraPlugin(with: mockDeviceDiscoverer) + let expectation = self.expectation(description: "Result finished") mockDeviceDiscoverer.discoverySessionStub = { deviceTypes, mediaType, position in + let unspecifiedCamera = MockCaptureDevice() + unspecifiedCamera.uniqueID = "0" + unspecifiedCamera.position = .unspecified + + var requiredTypes: [AVCaptureDevice.DeviceType] = [ + .builtInWideAngleCamera, .builtInTelephotoCamera, + ] + if #available(iOS 13.0, *) { + requiredTypes.append(.builtInUltraWideCamera) + } + let cameras = [unspecifiedCamera] + XCTAssertEqual(deviceTypes, requiredTypes) XCTAssertEqual(mediaType, .video) XCTAssertEqual(position, .unspecified) @@ -114,7 +151,6 @@ final class AvailableCamerasTest: XCTestCase { } waitForExpectations(timeout: 30, handler: nil) - // Verify the result. - XCTAssertEqual(resultValue?.count, 2) + XCTAssertEqual(resultValue?.first?.lensDirection, .external) } } diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift index bf0f7d300c7..e2698456838 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift @@ -13,6 +13,7 @@ final class CameraCaptureSessionQueueRaceConditionTests: XCTestCase { messenger: MockFlutterBinaryMessenger(), globalAPI: MockGlobalEventApi(), deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { MockCaptureSession() }, captureDeviceInputFactory: MockCaptureDeviceInputFactory() diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift index 5ad12652875..9befe9865de 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift @@ -14,6 +14,7 @@ final class CameraMethodChannelTests: XCTestCase { messenger: MockFlutterBinaryMessenger(), globalAPI: MockGlobalEventApi(), deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in MockCaptureDevice() }, captureSessionFactory: { session }, captureDeviceInputFactory: MockCaptureDeviceInputFactory() diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift index d6198f35d88..62cecea756e 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift @@ -8,14 +8,6 @@ import XCTest @testable import camera_avfoundation -private final class MockCamera: FLTCam { - var setDeviceOrientationStub: ((UIDeviceOrientation) -> Void)? - - override func setDeviceOrientation(_ orientation: UIDeviceOrientation) { - setDeviceOrientationStub?(orientation) - } -} - private final class MockUIDevice: UIDevice { var mockOrientation: UIDeviceOrientation = .unknown @@ -26,10 +18,10 @@ private final class MockUIDevice: UIDevice { final class CameraOrientationTests: XCTestCase { private func createCameraPlugin() -> ( - CameraPlugin, MockCamera, MockGlobalEventApi, MockCaptureDevice, MockCameraDeviceDiscoverer + CameraPlugin, MockFLTCam, MockGlobalEventApi, MockCaptureDevice, MockCameraDeviceDiscoverer ) { let mockDevice = MockCaptureDevice() - let mockCamera = MockCamera() + let mockCamera = MockFLTCam() let mockEventAPI = MockGlobalEventApi() let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() @@ -38,6 +30,7 @@ final class CameraOrientationTests: XCTestCase { messenger: MockFlutterBinaryMessenger(), globalAPI: mockEventAPI, deviceDiscoverer: mockDeviceDiscoverer, + permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in mockDevice }, captureSessionFactory: { MockCaptureSession() }, captureDeviceInputFactory: MockCaptureDeviceInputFactory() @@ -124,6 +117,7 @@ final class CameraOrientationTests: XCTestCase { messenger: MockFlutterBinaryMessenger(), globalAPI: mockEventAPI, deviceDiscoverer: mockDeviceDiscoverer, + permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in weakDevice! }, captureSessionFactory: { MockCaptureSession() }, captureDeviceInputFactory: MockCaptureDeviceInputFactory() diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift new file mode 100644 index 00000000000..de38b7507d2 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginCreateCameraTests.swift @@ -0,0 +1,130 @@ +// 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 XCTest + +@testable import camera_avfoundation + +final class CameraPluginCreateCameraTests: XCTestCase { + private func createCameraPlugin() -> ( + CameraPlugin, MockFLTCameraPermissionManager, MockCaptureSession + ) { + let mockPermissionManager = MockFLTCameraPermissionManager() + let mockCaptureSession = MockCaptureSession() + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: mockPermissionManager, + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { mockCaptureSession }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + + return (cameraPlugin, mockPermissionManager, mockCaptureSession) + } + + func testCreateCamera_requestsOnlyCameraPermissionWithAudioDisabled() { + let (cameraPlugin, mockPermissionManager, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var requestCameraPermissionCalled = false + mockPermissionManager.requestCameraPermissionStub = { completion in + requestCameraPermissionCalled = true + // Permission is granted + completion?(nil) + } + var requestAudioPermissionCalled = false + mockPermissionManager.requestAudioPermissionStub = { completion in + requestAudioPermissionCalled = true + // Permission is granted + completion?(nil) + } + + cameraPlugin.createCamera( + withName: "camera_name", + settings: FCPPlatformMediaSettings.make( + with: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: false) + ) { result, error in + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(requestCameraPermissionCalled) + XCTAssertFalse(requestAudioPermissionCalled) + } + + func testCreateCamera_requestsCameraAndAudioPermissionWithAudioEnabled() { + let (cameraPlugin, mockPermissionManager, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var requestCameraPermissionCalled = false + mockPermissionManager.requestCameraPermissionStub = { completion in + requestCameraPermissionCalled = true + // Permission is granted + completion?(nil) + } + var requestAudioPermissionCalled = false + mockPermissionManager.requestAudioPermissionStub = { completion in + requestAudioPermissionCalled = true + // Permission is granted + completion?(nil) + } + + cameraPlugin.createCamera( + withName: "camera_name", + settings: FCPPlatformMediaSettings.make( + with: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true) + ) { result, error in + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(requestCameraPermissionCalled) + XCTAssertTrue(requestAudioPermissionCalled) + } + + func testCreateCamera_createsFLTCamSuccessfully() { + let (cameraPlugin, mockPermissionManager, mockCaptureSession) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + mockPermissionManager.requestCameraPermissionStub = { completion in + // Permission is granted + completion?(nil) + } + mockPermissionManager.requestAudioPermissionStub = { completion in + // Permission is granted + completion?(nil) + } + mockCaptureSession.canSetSessionPreset = true + + cameraPlugin.createCamera( + withName: "camera_name", + settings: FCPPlatformMediaSettings.make( + with: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true) + ) { result, error in + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertNotNil(cameraPlugin.camera) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift new file mode 100644 index 00000000000..ef6b07010be --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPluginInitializeCameraTests.swift @@ -0,0 +1,113 @@ +// 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 XCTest + +@testable import camera_avfoundation + +final class CameraPluginInitializeCameraTests: XCTestCase { + private func createCameraPlugin() -> ( + CameraPlugin, MockFLTCam, MockGlobalEventApi + ) { + let mockCamera = MockFLTCam() + let mockGlobalEventApi = MockGlobalEventApi() + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: mockGlobalEventApi, + deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockFLTCameraPermissionManager(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + cameraPlugin.camera = mockCamera + + return (cameraPlugin, mockCamera, mockGlobalEventApi) + } + + private func waitForRoundTrip(with queue: DispatchQueue) { + let expectation = self.expectation(description: "Queue flush") + queue.async { + DispatchQueue.main.async { + expectation.fulfill() + } + } + waitForExpectations(timeout: 30, handler: nil) + } + + func testInitializeCamera_setsCameraOnFrameAvailableCallback() { + let (cameraPlugin, mockCamera, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var onFrameAvailableSet = false + mockCamera.setOnFrameAvailableStub = { callback in + onFrameAvailableSet = true + } + + cameraPlugin.initializeCamera(0, withImageFormat: FCPPlatformImageFormatGroup.bgra8888) { + error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(onFrameAvailableSet) + } + + func testInitializeCamera_setsCameraDartAPI() { + let (cameraPlugin, mockCamera, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var dartAPISet = false + mockCamera.setDartApiStub = { api in + dartAPISet = true + } + + cameraPlugin.initializeCamera(0, withImageFormat: FCPPlatformImageFormatGroup.bgra8888) { + error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(dartAPISet) + } + + func testInitializeCamera_sendsDeviceOrientation() { + let (cameraPlugin, _, mockGlobalEventApi) = createCameraPlugin() + + cameraPlugin.initializeCamera(0, withImageFormat: FCPPlatformImageFormatGroup.bgra8888) { + error in + XCTAssertNil(error) + } + + waitForRoundTrip(with: cameraPlugin.captureSessionQueue) + + XCTAssertTrue(mockGlobalEventApi.deviceOrientationChangedCalled) + } + + func testInitializeCamera_startsCamera() { + let (cameraPlugin, mockCamera, _) = createCameraPlugin() + let expectation = expectation(description: "Initialization completed") + + var startCalled = false + mockCamera.startStub = { + startCalled = true + } + + cameraPlugin.initializeCamera(0, withImageFormat: FCPPlatformImageFormatGroup.bgra8888) { + error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + + XCTAssertTrue(startCalled) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift index eb3d06dff49..b73769e5d02 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -156,6 +156,7 @@ final class CameraSettingsTests: XCTestCase { messenger: MockFlutterBinaryMessenger(), globalAPI: MockGlobalEventApi(), deviceDiscoverer: MockCameraDeviceDiscoverer(), + permissionManager: MockFLTCameraPermissionManager(), deviceFactory: { _ in mockDevice }, captureSessionFactory: { mockSession }, captureDeviceInputFactory: MockCaptureDeviceInputFactory() diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift new file mode 100644 index 00000000000..ad1122f7ae3 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift @@ -0,0 +1,37 @@ +// 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. + +final class MockFLTCam: FLTCam { + var setOnFrameAvailableStub: ((() -> Void) -> Void)? + var setDartApiStub: ((FCPCameraEventApi) -> Void)? + + var startStub: (() -> Void)? + var setDeviceOrientationStub: ((UIDeviceOrientation) -> Void)? + + override var onFrameAvailable: (() -> Void) { + get { + return super.onFrameAvailable + } + set { + setOnFrameAvailableStub?(newValue) + } + } + + override var dartAPI: FCPCameraEventApi { + get { + return super.dartAPI + } + set { + setDartApiStub?(newValue) + } + } + + override func start() { + startStub?() + } + + override func setDeviceOrientation(_ orientation: UIDeviceOrientation) { + setDeviceOrientationStub?(orientation) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCameraPermissionManager.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCameraPermissionManager.swift new file mode 100644 index 00000000000..61a38735d3c --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCameraPermissionManager.swift @@ -0,0 +1,16 @@ +// 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. + +final class MockFLTCameraPermissionManager: FLTCameraPermissionManager { + var requestCameraPermissionStub: ((((FlutterError?) -> Void)?) -> Void)? + var requestAudioPermissionStub: ((((FlutterError?) -> Void)?) -> Void)? + + override func requestCameraPermission(completionHandler: ((FlutterError?) -> Void)?) { + requestCameraPermissionStub?(completionHandler) + } + + override func requestAudioPermission(completionHandler: ((FlutterError?) -> Void)?) { + requestAudioPermissionStub?(completionHandler) + } +} 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 71fb384971d..b9e1ef1850c 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 @@ -43,10 +43,14 @@ + (void)registerWithRegistrar:(NSObject *)registrar { - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger { + id permissionService = [[FLTDefaultPermissionService alloc] init]; + return [self initWithRegistry:registry messenger:messenger globalAPI:[[FCPCameraGlobalEventApi alloc] initWithBinaryMessenger:messenger] deviceDiscoverer:[[FLTDefaultCameraDeviceDiscoverer alloc] init] + permissionManager:[[FLTCameraPermissionManager alloc] + initWithPermissionService:permissionService] deviceFactory:^NSObject *(NSString *name) { return [[FLTDefaultCaptureDevice alloc] initWithDevice:[AVCaptureDevice deviceWithUniqueID:name]]; @@ -62,6 +66,7 @@ - (instancetype)initWithRegistry:(NSObject *)registry messenger:(NSObject *)messenger globalAPI:(FCPCameraGlobalEventApi *)globalAPI deviceDiscoverer:(NSObject *)deviceDiscoverer + permissionManager:(FLTCameraPermissionManager *)permissionManager deviceFactory:(CaptureNamedDeviceFactory)deviceFactory captureSessionFactory:(CaptureSessionFactory)captureSessionFactory captureDeviceInputFactory: @@ -73,14 +78,11 @@ - (instancetype)initWithRegistry:(NSObject *)registry _globalEventAPI = globalAPI; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); _deviceDiscoverer = deviceDiscoverer; + _permissionManager = permissionManager; _captureDeviceFactory = deviceFactory; _captureSessionFactory = captureSessionFactory; _captureDeviceInputFactory = captureDeviceInputFactory; - id permissionService = [[FLTDefaultPermissionService alloc] init]; - _permissionManager = - [[FLTCameraPermissionManager alloc] initWithPermissionService:permissionService]; - dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); @@ -349,7 +351,7 @@ - (void)getMinimumExposureOffset:(nonnull void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion { __weak typeof(self) weakSelf = self; dispatch_async(self.captureSessionQueue, ^{ - completion(@(weakSelf.camera.captureDevice.minExposureTargetBias), nil); + completion(@(weakSelf.camera.minimumExposureOffset), nil); }); } @@ -357,7 +359,7 @@ - (void)getMaximumExposureOffset:(nonnull void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion { __weak typeof(self) weakSelf = self; dispatch_async(self.captureSessionQueue, ^{ - completion(@(weakSelf.camera.captureDevice.maxExposureTargetBias), nil); + completion(@(weakSelf.camera.maximumExposureOffset), nil); }); } 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 76819e75f16..dc9b45836d8 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 @@ -1248,6 +1248,14 @@ - (CGFloat)maximumAvailableZoomFactor { return _captureDevice.maxAvailableVideoZoomFactor; } +- (CGFloat)minimumExposureOffset { + return _captureDevice.minExposureTargetBias; +} + +- (CGFloat)maximumExposureOffset { + return _captureDevice.maxExposureTargetBias; +} + - (BOOL)setupWriterForPath:(NSString *)path { NSError *error = nil; NSURL *outputURL; 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 0e119c3bb14..dd15e3f3e6d 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 @@ -8,6 +8,7 @@ #import "FLTCam.h" #import "FLTCamConfiguration.h" #import "FLTCameraDeviceDiscovering.h" +#import "FLTCameraPermissionManager.h" #import "FLTCaptureDevice.h" #import "messages.g.h" @@ -34,6 +35,7 @@ typedef NSObject *_Nonnull (^CaptureNamedDeviceFactory)(NSStri messenger:(NSObject *)messenger globalAPI:(FCPCameraGlobalEventApi *)globalAPI deviceDiscoverer:(id)deviceDiscoverer + permissionManager:(FLTCameraPermissionManager *)permissionManager deviceFactory:(CaptureNamedDeviceFactory)deviceFactory captureSessionFactory:(CaptureSessionFactory)captureSessionFactory captureDeviceInputFactory:(id)captureDeviceInputFactory 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 e7dd92a7b83..be3af446e7c 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 @@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN @property(assign, nonatomic) FCPPlatformImageFileFormat fileFormat; @property(assign, nonatomic) CGFloat minimumAvailableZoomFactor; @property(assign, nonatomic) CGFloat maximumAvailableZoomFactor; +@property(assign, nonatomic) CGFloat minimumExposureOffset; +@property(assign, nonatomic) CGFloat maximumExposureOffset; /// Initializes an `FLTCam` instance with the given configuration. /// @param error report to the caller if any error happened creating the camera. diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 0ec67eb487f..e8d3a9d3fa3 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+8 +version: 0.9.18+9 environment: sdk: ^3.4.0