From cbc5559d449ad60edabfe6d5df2bed90707a30b0 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 14 Mar 2025 15:52:03 +0100 Subject: [PATCH 1/4] Add tests for setting camera exposure mode --- .../camera/camera_avfoundation/CHANGELOG.md | 5 + .../ios/Runner.xcodeproj/project.pbxproj | 8 +- .../ios/RunnerTests/CameraExposureTests.swift | 66 -------- .../ios/RunnerTests/FLTCamExposureTests.swift | 141 ++++++++++++++++++ .../ios/RunnerTests/Mocks/MockCaptureDevice.h | 2 +- .../ios/RunnerTests/Mocks/MockCaptureDevice.m | 6 +- .../ios/RunnerTests/Mocks/MockFLTCam.swift | 16 +- .../Sources/camera_avfoundation/FLTCam.m | 5 +- .../include/camera_avfoundation/FLTCam.h | 1 - .../camera/camera_avfoundation/pubspec.yaml | 2 +- 10 files changed, 166 insertions(+), 86 deletions(-) delete mode 100644 packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.swift create mode 100644 packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 0890c7f7314..9997ab70594 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.9.18+10 + +* Backfills unit tests for the `FLTCam` class. +* Makes `exposureMode` property private. + ## 0.9.18+9 * Backfills unit tests for `CameraPlugin` class. 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 af91d6648c4..99f42db3520 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 970ADAC02D6764CC00EFDCD9 /* MockEventChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970ADABF2D6764CC00EFDCD9 /* MockEventChannel.swift */; }; 972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972CA92A2D5A1D8C004B846F /* CameraPropertiesTests.swift */; }; 972CA92D2D5A28C4004B846F /* QueueUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972CA92C2D5A28C4004B846F /* QueueUtilsTests.swift */; }; - 972CA9312D5A366C004B846F /* CameraExposureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972CA9302D5A366C004B846F /* CameraExposureTests.swift */; }; 977A25202D5A439300931E34 /* AvailableCamerasTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */; }; 977A25222D5A49EC00931E34 /* CameraFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A25212D5A49EC00931E34 /* CameraFocusTests.swift */; }; 977A25242D5A511600931E34 /* CameraPermissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A25232D5A511600931E34 /* CameraPermissionTests.swift */; }; @@ -49,6 +48,7 @@ 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 */; }; + E11D6A912D82C7740031E6C5 /* FLTCamExposureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */; }; E12C4FF62D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */; }; E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */; }; E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; }; @@ -116,7 +116,6 @@ 970ADABF2D6764CC00EFDCD9 /* MockEventChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEventChannel.swift; sourceTree = ""; }; 972CA92A2D5A1D8C004B846F /* CameraPropertiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPropertiesTests.swift; sourceTree = ""; }; 972CA92C2D5A28C4004B846F /* QueueUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueUtilsTests.swift; sourceTree = ""; }; - 972CA9302D5A366C004B846F /* CameraExposureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraExposureTests.swift; 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 = ""; }; 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableCamerasTests.swift; sourceTree = ""; }; @@ -147,6 +146,7 @@ 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 = ""; }; + E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamExposureTests.swift; sourceTree = ""; }; E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginDelegatingMethodTests.swift; sourceTree = ""; }; E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCameraPermissionManager.swift; sourceTree = ""; }; E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.swift; sourceTree = ""; }; @@ -196,7 +196,6 @@ 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */, 972CA92A2D5A1D8C004B846F /* CameraPropertiesTests.swift */, 972CA92C2D5A28C4004B846F /* QueueUtilsTests.swift */, - 972CA9302D5A366C004B846F /* CameraExposureTests.swift */, 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */, 977A25212D5A49EC00931E34 /* CameraFocusTests.swift */, 977A25232D5A511600931E34 /* CameraPermissionTests.swift */, @@ -208,6 +207,7 @@ E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */, E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */, E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */, + E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */, E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */, ); path = RunnerTests; @@ -539,6 +539,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E11D6A912D82C7740031E6C5 /* FLTCamExposureTests.swift in Sources */, 97BD4A0E2D5CC5AE00F857D5 /* CameraSettingsTests.swift in Sources */, 972CA92D2D5A28C4004B846F /* QueueUtilsTests.swift in Sources */, E1FFEAB12D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift in Sources */, @@ -562,7 +563,6 @@ 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 */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.swift deleted file mode 100644 index b3d62d6b0aa..00000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// 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 XCTest - -@testable import camera_avfoundation - -final class CameraExposureTests: XCTestCase { - private func createSutAndMocks() -> (FLTCam, MockCaptureDevice, MockDeviceOrientationProvider) { - let mockDevice = MockCaptureDevice() - let mockDeviceOrientationProvider = MockDeviceOrientationProvider() - - let configuration = FLTCreateTestCameraConfiguration() - configuration.captureDeviceFactory = { mockDevice } - configuration.deviceOrientationProvider = mockDeviceOrientationProvider - let camera = FLTCreateCamWithConfiguration(configuration) - - return (camera, mockDevice, mockDeviceOrientationProvider) - } - - func testSetExposurePointWithResult_SetsExposurePointOfInterest() { - let (camera, mockDevice, mockDeviceOrientationProvider) = createSutAndMocks() - // UI is currently in landscape left orientation. - mockDeviceOrientationProvider.orientation = .landscapeLeft - // Exposure point of interest is supported. - mockDevice.exposurePointOfInterestSupported = true - - // Verify the focus point of interest has been set. - var setPoint = CGPoint.zero - mockDevice.setExposurePointOfInterestStub = { point in - if point == CGPoint(x: 1, y: 1) { - setPoint = point - } - } - - let completionExpectation = expectation(description: "Completion called") - camera.setExposurePoint(FCPPlatformPoint.makeWith(x: 1, y: 1)) { error in - XCTAssertNil(error) - completionExpectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - XCTAssertEqual(setPoint, CGPoint(x: 1.0, y: 1.0)) - } - - func testSetExposurePoint_WhenNotSupported_ReturnsError() { - let (camera, mockDevice, mockDeviceOrientationProvider) = createSutAndMocks() - // UI is currently in landscape left orientation. - mockDeviceOrientationProvider.orientation = .landscapeLeft - // Exposure point of interest is not supported. - mockDevice.exposurePointOfInterestSupported = false - - let expectation = self.expectation(description: "Completion with error") - - camera.setExposurePoint(FCPPlatformPoint.makeWith(x: 1, y: 1)) { error in - XCTAssertNotNil(error) - XCTAssertEqual(error?.code, "setExposurePointFailed") - XCTAssertEqual(error?.message, "Device does not have exposure point capabilities") - expectation.fulfill() - } - - waitForExpectations(timeout: 30, handler: nil) - } -} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift new file mode 100644 index 00000000000..faa61949da0 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamExposureTests.swift @@ -0,0 +1,141 @@ +// 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 FLTCamExposureTests: XCTestCase { + private func createCamera() -> (FLTCam, MockCaptureDevice, MockDeviceOrientationProvider) { + let mockDevice = MockCaptureDevice() + let mockDeviceOrientationProvider = MockDeviceOrientationProvider() + + let configuration = FLTCreateTestCameraConfiguration() + configuration.captureDeviceFactory = { mockDevice } + configuration.deviceOrientationProvider = mockDeviceOrientationProvider + let camera = FLTCreateCamWithConfiguration(configuration) + + return (camera, mockDevice, mockDeviceOrientationProvider) + } + + func testSetExposureModeLocked_setsAuthExposeMode() { + let (camera, mockDevice, _) = createCamera() + + mockDevice.setExposureModeStub = { mode in + // AVCaptureExposureModeAutoExpose automatically adjusts the exposure one time, and then + // locks exposure for the device + XCTAssertEqual(mode, .autoExpose) + } + + camera.setExposureMode(.locked) + } + + func testSetExposureModeAuto_setsContinousAutoExposureMode_ifSupported() { + let (camera, mockDevice, _) = createCamera() + + // All exposure modes are supported + mockDevice.isExposureModeSupportedStub = { _ in true } + + mockDevice.setExposureModeStub = { mode in + XCTAssertEqual(mode, .continuousAutoExposure) + } + + camera.setExposureMode(.auto) + } + + func testSetExposureModeAuto_setsAutoExposeMode_ifContinousAutoIsNotSupported() { + let (camera, mockDevice, _) = createCamera() + + // Continous auto exposure is not supported + mockDevice.isExposureModeSupportedStub = { mode in + mode != .continuousAutoExposure + } + + mockDevice.setExposureModeStub = { mode in + XCTAssertEqual(mode, .autoExpose) + } + + camera.setExposureMode(.auto) + } + + func testSetExposurePoint_setsExposurePointOfInterest() { + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() + // UI is currently in landscape left orientation. + mockDeviceOrientationProvider.orientation = .landscapeLeft + // Exposure point of interest is supported. + mockDevice.exposurePointOfInterestSupported = true + + // Verify the focus point of interest has been set. + var setPoint = CGPoint.zero + mockDevice.setExposurePointOfInterestStub = { point in + if point == CGPoint(x: 1, y: 1) { + setPoint = point + } + } + + let expectation = expectation(description: "Completion called") + camera.setExposurePoint(FCPPlatformPoint.makeWith(x: 1, y: 1)) { error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertEqual(setPoint, CGPoint(x: 1.0, y: 1.0)) + } + + func testSetExposurePoint_returnsError_ifNotSupported() { + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() + // UI is currently in landscape left orientation. + mockDeviceOrientationProvider.orientation = .landscapeLeft + // Exposure point of interest is not supported. + mockDevice.exposurePointOfInterestSupported = false + + let expectation = expectation(description: "Completion with error") + + camera.setExposurePoint(FCPPlatformPoint.makeWith(x: 1, y: 1)) { error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.code, "setExposurePointFailed") + XCTAssertEqual(error?.message, "Device does not have exposure point capabilities") + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testSetExposureOffset_setsExposureTargetBias() { + let (camera, mockDevice, _) = createCamera() + + let targetOffset = CGFloat(1.0) + + var setExposureTargetBiasCalled = false + mockDevice.setExposureTargetBiasStub = { bias, handler in + XCTAssertEqual(bias, Float(targetOffset)) + setExposureTargetBiasCalled = true + } + + camera.setExposureOffset(targetOffset) + + XCTAssertTrue(setExposureTargetBiasCalled) + } + + func testMaximumExposureOffset_returnsDeviceMaxExposureTargetBias() { + let (camera, mockDevice, _) = createCamera() + + let targetOffset = CGFloat(1.0) + + mockDevice.maxExposureTargetBias = Float(targetOffset) + + XCTAssertEqual(camera.maximumExposureOffset, targetOffset) + } + + func testMinimumExposureOffset_returnsDeviceMinExposureTargetBias() { + let (camera, mockDevice, _) = createCamera() + + let targetOffset = CGFloat(1.0) + + mockDevice.minExposureTargetBias = Float(targetOffset) + + XCTAssertEqual(camera.minimumExposureOffset, targetOffset) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h index 4161e0fc1d4..71589e1338c 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.h @@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_BEGIN // Exposure @property(nonatomic, assign) BOOL exposurePointOfInterestSupported; @property(nonatomic, assign) AVCaptureExposureMode exposureMode; -@property(nonatomic, assign) BOOL exposureModeSupported; +@property(nonatomic, copy) BOOL (^isExposureModeSupportedStub)(AVCaptureExposureMode mode); /// Overrides the default implementation of setting exposure mode. /// @param mode The exposure mode being set @property(nonatomic, copy) void (^setExposureModeStub)(AVCaptureExposureMode mode); diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m index 13a44a8f1a4..b8ff1e326fe 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockCaptureDevice.m @@ -111,7 +111,11 @@ - (void)setActiveVideoMaxFrameDuration:(CMTime)duration { } - (BOOL)isExposureModeSupported:(AVCaptureExposureMode)mode { - return self.exposureModeSupported; + if (self.isExposureModeSupportedStub) { + return self.isExposureModeSupportedStub(mode); + } else { + return NO; + } } @synthesize device; diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift index a2740cebb32..4d6f00d9766 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift @@ -5,7 +5,6 @@ final class MockFLTCam: FLTCam { var setOnFrameAvailableStub: ((() -> Void) -> Void)? var setDartApiStub: ((FCPCameraEventApi) -> Void)? - var setExposureModeStub: ((FCPPlatformExposureMode) -> Void)? var setFocusModeStub: ((FCPPlatformFocusMode) -> Void)? var getMinimumAvailableZoomFactorStub: (() -> CGFloat)? var getMaximumAvailableZoomFactorStub: (() -> CGFloat)? @@ -24,6 +23,7 @@ final class MockFLTCam: FLTCam { var lockCaptureStub: ((FCPPlatformDeviceOrientation) -> Void)? var unlockCaptureOrientationStub: (() -> Void)? var setFlashModeStub: ((FCPPlatformFlashMode, ((FlutterError?) -> Void)?) -> Void)? + var setExposureModeStub: ((FCPPlatformExposureMode) -> Void)? var receivedImageStreamDataStub: (() -> Void)? var pausePreviewStub: (() -> Void)? var resumePreviewStub: (() -> Void)? @@ -54,16 +54,6 @@ final class MockFLTCam: FLTCam { } } - /// The `setExposureMode` ObjC method is converted to property accessor in Swift translation - override var exposureMode: FCPPlatformExposureMode { - get { - return super.exposureMode - } - set { - setExposureModeStub?(newValue) - } - } - /// The `setFocusMode` ObjC method is converted to property accessor in Swift translation override var focusMode: FCPPlatformFocusMode { get { @@ -159,6 +149,10 @@ final class MockFLTCam: FLTCam { setFlashModeStub?(mode, completion) } + override func setExposureMode(_ mode: FCPPlatformExposureMode) { + setExposureModeStub?(mode) + } + override func receivedImageStreamData() { receivedImageStreamDataStub?() } 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 dc9b45836d8..8ba93c09aae 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 @@ -109,6 +109,7 @@ @interface FLTCam () *captureDeviceInputFactory; +@property(assign, nonatomic) FCPPlatformExposureMode exposureMode; @property(readonly, nonatomic) NSObject *deviceOrientationProvider; @property(nonatomic, copy) AssetWriterFactory assetWriterFactory; @property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory; @@ -987,8 +988,10 @@ - (void)setExposureMode:(FCPPlatformExposureMode)mode { - (void)applyExposureMode { [_captureDevice lockForConfiguration:nil]; - switch (_exposureMode) { + switch (self.exposureMode) { case FCPPlatformExposureModeLocked: + // AVCaptureExposureModeAutoExpose automatically adjusts the exposure one time, and then + // locks exposure for the device [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; break; case FCPPlatformExposureModeAuto: 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 be3af446e7c..a1f9576131a 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 @@ -25,7 +25,6 @@ NS_ASSUME_NONNULL_BEGIN /// The API instance used to communicate with the Dart side of the plugin. Once initially set, this /// should only ever be accessed on the main thread. @property(nonatomic) FCPCameraEventApi *dartAPI; -@property(assign, nonatomic) FCPPlatformExposureMode exposureMode; @property(assign, nonatomic) FCPPlatformFocusMode focusMode; @property(assign, nonatomic) FCPPlatformFlashMode flashMode; // Format used for video and image streaming. diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index e8d3a9d3fa3..6263c6531b2 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+9 +version: 0.9.18+10 environment: sdk: ^3.4.0 From f98d144a8e74acc09676553367b077c5d70e2e98 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 14 Mar 2025 13:24:06 +0100 Subject: [PATCH 2/4] Make apply focus mode methods private and adjust tests --- .../camera/camera_avfoundation/CHANGELOG.md | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 8 ++--- ...ocusTests.swift => FLTCamFocusTests.swift} | 30 +++++++++---------- .../ios/RunnerTests/Mocks/MockFLTCam.swift | 22 +++++--------- .../Sources/camera_avfoundation/FLTCam.m | 2 ++ .../include/camera_avfoundation/FLTCam.h | 25 +++++++--------- 6 files changed, 40 insertions(+), 49 deletions(-) rename packages/camera/camera_avfoundation/example/ios/RunnerTests/{CameraFocusTests.swift => FLTCamFocusTests.swift} (86%) diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 9997ab70594..a7bf16f7c0b 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,7 +1,7 @@ ## 0.9.18+10 * Backfills unit tests for the `FLTCam` class. -* Makes `exposureMode` property private. +* Makes `exposureMode` and `focusMode` properties of `FLTCam` private. ## 0.9.18+9 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 99f42db3520..2ff3a79eba0 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -28,7 +28,7 @@ 972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972CA92A2D5A1D8C004B846F /* CameraPropertiesTests.swift */; }; 972CA92D2D5A28C4004B846F /* QueueUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 972CA92C2D5A28C4004B846F /* QueueUtilsTests.swift */; }; 977A25202D5A439300931E34 /* AvailableCamerasTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */; }; - 977A25222D5A49EC00931E34 /* CameraFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A25212D5A49EC00931E34 /* CameraFocusTests.swift */; }; + 977A25222D5A49EC00931E34 /* FLTCamFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A25212D5A49EC00931E34 /* FLTCamFocusTests.swift */; }; 977A25242D5A511600931E34 /* CameraPermissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A25232D5A511600931E34 /* CameraPermissionTests.swift */; }; 977CAC9F2D5E5180001E5DC3 /* ThreadSafeEventChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977CAC9E2D5E5180001E5DC3 /* ThreadSafeEventChannelTests.swift */; }; 978296CF2D5F744B0009BDD3 /* PhotoCaptureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978296CE2D5F744B0009BDD3 /* PhotoCaptureTests.swift */; }; @@ -119,7 +119,7 @@ 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 = ""; }; 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableCamerasTests.swift; sourceTree = ""; }; - 977A25212D5A49EC00931E34 /* CameraFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraFocusTests.swift; sourceTree = ""; }; + 977A25212D5A49EC00931E34 /* FLTCamFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamFocusTests.swift; sourceTree = ""; }; 977A25232D5A511600931E34 /* CameraPermissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPermissionTests.swift; sourceTree = ""; }; 977CAC9E2D5E5180001E5DC3 /* ThreadSafeEventChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeEventChannelTests.swift; sourceTree = ""; }; 978296CE2D5F744B0009BDD3 /* PhotoCaptureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureTests.swift; sourceTree = ""; }; @@ -197,7 +197,6 @@ 972CA92A2D5A1D8C004B846F /* CameraPropertiesTests.swift */, 972CA92C2D5A28C4004B846F /* QueueUtilsTests.swift */, 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */, - 977A25212D5A49EC00931E34 /* CameraFocusTests.swift */, 977A25232D5A511600931E34 /* CameraPermissionTests.swift */, 97C0FFAD2D5E023200A36284 /* SavePhotoDelegateTests.swift */, 977CAC9E2D5E5180001E5DC3 /* ThreadSafeEventChannelTests.swift */, @@ -208,6 +207,7 @@ E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */, E1FFEAB02D6CDE5B00B14107 /* CameraPluginInitializeCameraTests.swift */, E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */, + 977A25212D5A49EC00931E34 /* FLTCamFocusTests.swift */, E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */, ); path = RunnerTests; @@ -569,7 +569,7 @@ 7FD582272D57C020003B1200 /* MockAssetWriter.m in Sources */, 979B3E022D5BA48F009BDE1A /* CameraOrientationTests.swift in Sources */, E12C4FF62D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift in Sources */, - 977A25222D5A49EC00931E34 /* CameraFocusTests.swift in Sources */, + 977A25222D5A49EC00931E34 /* FLTCamFocusTests.swift in Sources */, 978D90B42D5F630300CD817E /* StreamingTests.swift in Sources */, 7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */, 7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift similarity index 86% rename from packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.swift rename to packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift index b275a8bc9a6..3d46e91c3fa 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraFocusTests.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamFocusTests.swift @@ -7,8 +7,8 @@ import XCTest @testable import camera_avfoundation -final class CameraFocusTests: XCTestCase { - private func createSutAndMocks() -> (FLTCam, MockCaptureDevice, MockDeviceOrientationProvider) { +final class FLTCamSetFocusModeTests: XCTestCase { + private func createCamera() -> (FLTCam, MockCaptureDevice, MockDeviceOrientationProvider) { let mockDevice = MockCaptureDevice() let mockDeviceOrientationProvider = MockDeviceOrientationProvider() @@ -21,7 +21,7 @@ final class CameraFocusTests: XCTestCase { } func testAutoFocusWithContinuousModeSupported_ShouldSetContinuousAutoFocus() { - let (camera, mockDevice, _) = createSutAndMocks() + let (camera, mockDevice, _) = createCamera() // AVCaptureFocusModeContinuousAutoFocus and AVCaptureFocusModeAutoFocus are supported. mockDevice.isFocusModeSupportedStub = { mode in mode == .continuousAutoFocus || mode == .autoFocus @@ -38,13 +38,13 @@ final class CameraFocusTests: XCTestCase { } } - camera.applyFocusMode(.auto, on: mockDevice) + camera.setFocusMode(.auto) XCTAssertTrue(setFocusModeContinuousAutoFocusCalled) } func testAutoFocusWithContinuousModeNotSupported_ShouldSetAutoFocus() { - let (camera, mockDevice, _) = createSutAndMocks() + let (camera, mockDevice, _) = createCamera() // AVCaptureFocusModeContinuousAutoFocus is not supported. // AVCaptureFocusModeAutoFocus is supported. mockDevice.isFocusModeSupportedStub = { mode in @@ -62,13 +62,13 @@ final class CameraFocusTests: XCTestCase { } } - camera.applyFocusMode(.auto, on: mockDevice) + camera.setFocusMode(.auto) XCTAssertTrue(setFocusModeAutoFocusCalled) } func testAutoFocusWithNoModeSupported_ShouldSetNothing() { - let (camera, mockDevice, _) = createSutAndMocks() + let (camera, mockDevice, _) = createCamera() // No modes are supported. mockDevice.isFocusModeSupportedStub = { _ in false @@ -79,11 +79,11 @@ final class CameraFocusTests: XCTestCase { _ in XCTFail("Unexpected call to setFocusMode") } - camera.applyFocusMode(.auto, on: mockDevice) + camera.setFocusMode(.auto) } func testLockedFocusWithModeSupported_ShouldSetModeAutoFocus() { - let (camera, mockDevice, _) = createSutAndMocks() + let (camera, mockDevice, _) = createCamera() // AVCaptureFocusModeContinuousAutoFocus and AVCaptureFocusModeAutoFocus are supported. mockDevice.isFocusModeSupportedStub = { mode in mode == .continuousAutoFocus || mode == .autoFocus @@ -91,7 +91,7 @@ final class CameraFocusTests: XCTestCase { var setFocusModeAutoFocusCalled = false - // Expect only setFocusMode:AVCaptureFocusModeAutoFocus. + // AVCaptureFocusModeAutoFocus automatically adjusts the focus one time, and then locks focus mockDevice.setFocusModeStub = { mode in if mode == .continuousAutoFocus { XCTFail("Unexpected call to setFocusMode") @@ -100,13 +100,13 @@ final class CameraFocusTests: XCTestCase { } } - camera.applyFocusMode(.locked, on: mockDevice) + camera.setFocusMode(.locked) XCTAssertTrue(setFocusModeAutoFocusCalled) } func testLockedFocusWithModeNotSupported_ShouldSetNothing() { - let (camera, mockDevice, _) = createSutAndMocks() + let (camera, mockDevice, _) = createCamera() mockDevice.isFocusModeSupportedStub = { mode in mode == .continuousAutoFocus } @@ -116,11 +116,11 @@ final class CameraFocusTests: XCTestCase { XCTFail("Unexpected call to setFocusMode") } - camera.applyFocusMode(.locked, on: mockDevice) + camera.setFocusMode(.locked) } func testSetFocusPointWithResult_SetsFocusPointOfInterest() { - let (camera, mockDevice, mockDeviceOrientationProvider) = createSutAndMocks() + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() // UI is currently in landscape left orientation. mockDeviceOrientationProvider.orientation = .landscapeLeft // Focus point of interest is supported. @@ -141,7 +141,7 @@ final class CameraFocusTests: XCTestCase { } func testSetFocusPoint_WhenNotSupported_ReturnsError() { - let (camera, mockDevice, mockDeviceOrientationProvider) = createSutAndMocks() + let (camera, mockDevice, mockDeviceOrientationProvider) = createCamera() // UI is currently in landscape left orientation. mockDeviceOrientationProvider.orientation = .landscapeLeft // Focus point of interest is not supported. diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift index 4d6f00d9766..828bcc20d97 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/Mocks/MockFLTCam.swift @@ -54,16 +54,6 @@ final class MockFLTCam: FLTCam { } } - /// The `setFocusMode` ObjC method is converted to property accessor in Swift translation - override var focusMode: FCPPlatformFocusMode { - get { - return super.focusMode - } - set { - setFocusModeStub?(newValue) - } - } - override var minimumAvailableZoomFactor: CGFloat { get { return getMinimumAvailableZoomFactorStub?() ?? super.minimumAvailableZoomFactor @@ -143,16 +133,20 @@ final class MockFLTCam: FLTCam { unlockCaptureOrientationStub?() } + override func setExposureMode(_ mode: FCPPlatformExposureMode) { + setExposureModeStub?(mode) + } + + override func setFocusMode(_ mode: FCPPlatformFocusMode) { + setFocusModeStub?(mode) + } + override func setFlashMode( _ mode: FCPPlatformFlashMode, withCompletion completion: @escaping (FlutterError?) -> Void ) { setFlashModeStub?(mode, completion) } - override func setExposureMode(_ mode: FCPPlatformExposureMode) { - setExposureModeStub?(mode) - } - override func receivedImageStreamData() { receivedImageStreamDataStub?() } 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 8ba93c09aae..85fe43779b8 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 @@ -110,6 +110,7 @@ @interface FLTCam () *captureDeviceInputFactory; @property(assign, nonatomic) FCPPlatformExposureMode exposureMode; +@property(assign, nonatomic) FCPPlatformFocusMode focusMode; @property(readonly, nonatomic) NSObject *deviceOrientationProvider; @property(nonatomic, copy) AssetWriterFactory assetWriterFactory; @property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory; @@ -1019,6 +1020,7 @@ - (void)applyFocusMode:(FCPPlatformFocusMode)focusMode [captureDevice lockForConfiguration:nil]; switch (focusMode) { case FCPPlatformFocusModeLocked: + // AVCaptureFocusModeAutoFocus automatically adjusts the focus one time, and then locks focus if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; } 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 a1f9576131a..320af629f0d 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 @@ -25,7 +25,6 @@ NS_ASSUME_NONNULL_BEGIN /// The API instance used to communicate with the Dart side of the plugin. Once initially set, this /// should only ever be accessed on the main thread. @property(nonatomic) FCPCameraEventApi *dartAPI; -@property(assign, nonatomic) FCPPlatformFocusMode focusMode; @property(assign, nonatomic) FCPPlatformFlashMode flashMode; // Format used for video and image streaming. @property(assign, nonatomic) FourCharCode videoFormat; @@ -64,16 +63,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setFlashMode:(FCPPlatformFlashMode)mode withCompletion:(void (^)(FlutterError *_Nullable))completion; - (void)setExposureMode:(FCPPlatformExposureMode)mode; -- (void)setFocusMode:(FCPPlatformFocusMode)mode; -- (void)applyFocusMode; - -/// Acknowledges the receipt of one image stream frame. -/// -/// This should be called each time a frame is received. Failing to call it may -/// cause later frames to be dropped instead of streamed. -- (void)receivedImageStreamData; - -/// Applies FocusMode on the AVCaptureDevice. +/// Sets FocusMode on the current AVCaptureDevice. /// /// If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use /// AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to @@ -83,10 +73,15 @@ NS_ASSUME_NONNULL_BEGIN /// AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not /// be set. /// -/// @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:(NSObject *)captureDevice; +/// @param mode The focus mode that should be applied. +- (void)setFocusMode:(FCPPlatformFocusMode)mode; + +/// Acknowledges the receipt of one image stream frame. +/// +/// This should be called each time a frame is received. Failing to call it may +/// cause later frames to be dropped instead of streamed. +- (void)receivedImageStreamData; + - (void)pausePreview; - (void)resumePreview; - (void)setDescriptionWhileRecording:(NSString *)cameraName From b535abd78d279f1d8ad192b0fa6d445598a0a893 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 14 Mar 2025 14:57:43 +0100 Subject: [PATCH 3/4] Make flashMode property private --- packages/camera/camera_avfoundation/CHANGELOG.md | 2 +- .../Sources/camera_avfoundation/FLTCam.m | 7 ++++--- .../include/camera_avfoundation/FLTCam.h | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index a7bf16f7c0b..49a386b8b17 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,7 +1,7 @@ ## 0.9.18+10 * Backfills unit tests for the `FLTCam` class. -* Makes `exposureMode` and `focusMode` properties of `FLTCam` private. +* Makes `exposureMode`, `focusMode`, and `flashMode` properties of `FLTCam` private. ## 0.9.18+9 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 85fe43779b8..640727be94e 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 @@ -111,6 +111,7 @@ @interface FLTCam () *captureDeviceInputFactory; @property(assign, nonatomic) FCPPlatformExposureMode exposureMode; @property(assign, nonatomic) FCPPlatformFocusMode focusMode; +@property(assign, nonatomic) FCPPlatformFlashMode flashMode; @property(readonly, nonatomic) NSObject *deviceOrientationProvider; @property(nonatomic, copy) AssetWriterFactory assetWriterFactory; @property(nonatomic, copy) InputPixelBufferAdaptorFactory inputPixelBufferAdaptorFactory; @@ -406,8 +407,8 @@ - (void)captureToFileWithCompletion:(void (^)(NSString *_Nullable, } // If the flash is in torch mode, no capture-level flash setting is needed. - if (_flashMode != FCPPlatformFlashModeTorch) { - [settings setFlashMode:FCPGetAVCaptureFlashModeForPigeonFlashMode(_flashMode)]; + if (self.flashMode != FCPPlatformFlashModeTorch) { + [settings setFlashMode:FCPGetAVCaptureFlashModeForPigeonFlashMode(self.flashMode)]; } NSError *error; NSString *path = [self getTemporaryFilePathWithExtension:extension @@ -1333,7 +1334,7 @@ - (BOOL)setupWriterForPath:(NSString *)path { [_audioOutput setSampleBufferDelegate:self queue:_captureSessionQueue]; } - if (_flashMode == FCPPlatformFlashModeTorch) { + if (self.flashMode == FCPPlatformFlashModeTorch) { [self.captureDevice lockForConfiguration:nil]; [self.captureDevice setTorchMode:AVCaptureTorchModeOn]; [self.captureDevice unlockForConfiguration]; 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 320af629f0d..197bf63bcc2 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 @@ -25,7 +25,6 @@ NS_ASSUME_NONNULL_BEGIN /// The API instance used to communicate with the Dart side of the plugin. Once initially set, this /// should only ever be accessed on the main thread. @property(nonatomic) FCPCameraEventApi *dartAPI; -@property(assign, nonatomic) FCPPlatformFlashMode flashMode; // Format used for video and image streaming. @property(assign, nonatomic) FourCharCode videoFormat; @property(assign, nonatomic) FCPPlatformImageFileFormat fileFormat; From 341d896475a44c70e3d06a8b5631eaccefc40cb7 Mon Sep 17 00:00:00 2001 From: Robert Odrowaz Date: Fri, 14 Mar 2025 15:42:38 +0100 Subject: [PATCH 4/4] Add tests for setting camera zoom level --- .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../ios/RunnerTests/FLTCamZoomTests.swift | 102 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.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 2ff3a79eba0..1adda4c2706 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ E11D6A912D82C7740031E6C5 /* FLTCamExposureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */; }; E12C4FF62D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */; }; E12C4FF82D68E85500515E70 /* MockFLTCameraPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */; }; + E16602952D8471C0003CFE12 /* FLTCamZoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */; }; E1A5F4E32D80259C0005BA64 /* FLTCamSetFlashModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */; }; E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAC2D6C8DD700B14107 /* MockFLTCam.swift */; }; E1FFEAAF2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FFEAAE2D6CDA8C00B14107 /* CameraPluginCreateCameraTests.swift */; }; @@ -149,6 +150,7 @@ E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamExposureTests.swift; sourceTree = ""; }; E12C4FF52D68C69000515E70 /* CameraPluginDelegatingMethodTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPluginDelegatingMethodTests.swift; sourceTree = ""; }; E12C4FF72D68E85500515E70 /* MockFLTCameraPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFLTCameraPermissionManager.swift; sourceTree = ""; }; + E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamZoomTests.swift; sourceTree = ""; }; E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLTCamSetFlashModeTests.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 = ""; }; @@ -209,6 +211,7 @@ E11D6A902D82C7740031E6C5 /* FLTCamExposureTests.swift */, 977A25212D5A49EC00931E34 /* FLTCamFocusTests.swift */, E1A5F4E22D80259C0005BA64 /* FLTCamSetFlashModeTests.swift */, + E16602942D8471C0003CFE12 /* FLTCamZoomTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -565,6 +568,7 @@ 977A25202D5A439300931E34 /* AvailableCamerasTests.swift in Sources */, E1FFEAAD2D6C8DD700B14107 /* MockFLTCam.swift in Sources */, 7F29EB292D26A59000740257 /* MockCameraDeviceDiscoverer.m in Sources */, + E16602952D8471C0003CFE12 /* FLTCamZoomTests.swift in Sources */, 97BD4A102D5CE13500F857D5 /* CameraSessionPresetsTests.swift in Sources */, 7FD582272D57C020003B1200 /* MockAssetWriter.m in Sources */, 979B3E022D5BA48F009BDE1A /* CameraOrientationTests.swift in Sources */, diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift new file mode 100644 index 00000000000..09906dc794f --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTCamZoomTests.swift @@ -0,0 +1,102 @@ +// 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 XCTest + +@testable import camera_avfoundation + +final class FLTCamZoomTests: XCTestCase { + private func createCamera() -> (FLTCam, MockCaptureDevice) { + let mockDevice = MockCaptureDevice() + + let configuration = FLTCreateTestCameraConfiguration() + configuration.captureDeviceFactory = { mockDevice } + let camera = FLTCreateCamWithConfiguration(configuration) + + return (camera, mockDevice) + } + + func testSetZoomLevel_setVideoZoomFactor() { + let (camera, mockDevice) = createCamera() + + mockDevice.maxAvailableVideoZoomFactor = 2.0 + mockDevice.minAvailableVideoZoomFactor = 0.0 + + let targetZoom = CGFloat(1.0) + + var setVideoZoomFactorCalled = false + mockDevice.setVideoZoomFactorStub = { zoom in + XCTAssertEqual(zoom, Float(targetZoom)) + setVideoZoomFactorCalled = true + } + + let expectation = expectation(description: "Call completed") + + camera.setZoomLevel(targetZoom) { error in + XCTAssertNil(error) + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + + XCTAssertTrue(setVideoZoomFactorCalled) + } + + func testSetZoomLevel_returnsError_forZoomLevelBlowMinimum() { + let (camera, mockDevice) = createCamera() + + // Allowed zoom range between 2.0 and 3.0 + mockDevice.maxAvailableVideoZoomFactor = 2.0 + mockDevice.minAvailableVideoZoomFactor = 3.0 + + let expectation = expectation(description: "Call completed") + + camera.setZoomLevel(CGFloat(1.0)) { error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.code, "ZOOM_ERROR") + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testSetZoomLevel_returnsError_forZoomLevelAboveMaximum() { + let (camera, mockDevice) = createCamera() + + // Allowed zoom range between 0.0 and 1.0 + mockDevice.maxAvailableVideoZoomFactor = 0.0 + mockDevice.minAvailableVideoZoomFactor = 1.0 + + let expectation = expectation(description: "Call completed") + + camera.setZoomLevel(CGFloat(2.0)) { error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.code, "ZOOM_ERROR") + expectation.fulfill() + } + + waitForExpectations(timeout: 30) + } + + func testMaximumAvailableZoomFactor_returnsDeviceMaxAvailableVideoZoomFactor() { + let (camera, mockDevice) = createCamera() + + let targetZoom = CGFloat(1.0) + + mockDevice.maxAvailableVideoZoomFactor = Float(targetZoom) + + XCTAssertEqual(camera.maximumAvailableZoomFactor, targetZoom) + } + + func testMinimumAvailableZoomFactor_returnsDeviceMinAvailableVideoZoomFactor() { + let (camera, mockDevice) = createCamera() + + let targetZoom = CGFloat(1.0) + + mockDevice.minAvailableVideoZoomFactor = Float(targetZoom) + + XCTAssertEqual(camera.minimumAvailableZoomFactor, targetZoom) + } +}