diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 6db21416fe26..413aa6b68099 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.18+8 + +* Migrates unit tests to Swift. + ## 0.9.18+7 * Fixes crash when setting `activeFormat` on `FLTCaptureDevice`. 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 a4c1ce1005a7..8d0c4d0b0d21 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,19 +3,16 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */; }; 1000364CB781922C6D6AAA4A /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DDC4CE84A8B378AE4A8CD9C /* libPods-RunnerTests.a */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 54D650172516862D30686934 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ECAF63F924EFA2D68883BA85 /* libPods-Runner.a */; }; 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 */; }; @@ -37,13 +34,17 @@ 977A25242D5A511600931E34 /* CameraPermissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977A25232D5A511600931E34 /* CameraPermissionTests.swift */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 979B3DFB2D5B6BC7009BDE1A /* ExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */; }; + 979B3DFE2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFD2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift */; }; + 979B3E002D5B9E6C009BDE1A /* CameraMethodChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3DFF2D5B9E6C009BDE1A /* CameraMethodChannelTests.swift */; }; + 979B3E022D5BA48F009BDE1A /* CameraOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 979B3E012D5BA48F009BDE1A /* CameraOrientationTests.swift */; }; + 97BD4A0E2D5CC5AE00F857D5 /* CameraSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BD4A0D2D5CC5AE00F857D5 /* CameraSettingsTests.swift */; }; + 97BD4A102D5CE13500F857D5 /* CameraSessionPresetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BD4A0F2D5CE13500F857D5 /* CameraSessionPresetsTests.swift */; }; + 97C0FFAE2D5E023200A36284 /* SavePhotoDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C0FFAD2D5E023200A36284 /* SavePhotoDelegateTests.swift */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */; }; - CEF6611A2B5E36A500D33FD4 /* CameraSessionPresetsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF661192B5E36A500D33FD4 /* CameraSessionPresetsTests.m */; }; - E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; }; E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; }; E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; }; E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; @@ -74,10 +75,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraMethodChannelTests.m; sourceTree = ""; }; 03BB76682665316900CE5A93 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 03BB766C2665316900CE5A93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraOrientationTests.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -86,7 +85,6 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -125,19 +123,23 @@ 977A25232D5A511600931E34 /* CameraPermissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPermissionTests.swift; sourceTree = ""; }; 979B3DF92D5B6BA2009BDE1A /* ExceptionCatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExceptionCatcher.h; sourceTree = ""; }; 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExceptionCatcher.m; sourceTree = ""; }; + 979B3DFC2D5B985B009BDE1A /* RunnerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RunnerTests-Bridging-Header.h"; sourceTree = ""; }; + 979B3DFD2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureSessionQueueRaceConditionTests.swift; sourceTree = ""; }; + 979B3DFF2D5B9E6C009BDE1A /* CameraMethodChannelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMethodChannelTests.swift; sourceTree = ""; }; + 979B3E012D5BA48F009BDE1A /* CameraOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraOrientationTests.swift; sourceTree = ""; }; + 97BD4A0D2D5CC5AE00F857D5 /* CameraSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSettingsTests.swift; sourceTree = ""; }; + 97BD4A0F2D5CE13500F857D5 /* CameraSessionPresetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraSessionPresetsTests.swift; sourceTree = ""; }; + 97C0FFAD2D5E023200A36284 /* SavePhotoDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePhotoDelegateTests.swift; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 97DB234B2D566D0600CEFE66 /* RunnerTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RunnerTests-Bridging-Header.h"; sourceTree = ""; }; 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreviewPauseTests.swift; sourceTree = ""; }; 9DDC4CE84A8B378AE4A8CD9C /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A8F314CD1C64E9257EBC811D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 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 = ""; }; - CEF661192B5E36A500D33FD4 /* CameraSessionPresetsTests.m */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = CameraSessionPresetsTests.m; sourceTree = ""; }; - E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = ""; }; E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = ""; }; E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; @@ -172,19 +174,19 @@ isa = PBXGroup; children = ( 7F29EB3F2D281C6D00740257 /* Mocks */, - 7D5FCCD32AEF9D0200FB7108 /* CameraSettingsTests.m */, - 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, - 033B94BD269C40A200B4DF97 /* CameraMethodChannelTests.m */, E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, - E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */, E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */, 788A065927B0E02900533D74 /* StreamingTest.m */, - CEF661192B5E36A500D33FD4 /* CameraSessionPresetsTests.m */, - 97DB234B2D566D0600CEFE66 /* RunnerTests-Bridging-Header.h */, + 979B3DFC2D5B985B009BDE1A /* RunnerTests-Bridging-Header.h */, + 979B3DFD2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift */, + 979B3DFF2D5B9E6C009BDE1A /* CameraMethodChannelTests.swift */, + 979B3E012D5BA48F009BDE1A /* CameraOrientationTests.swift */, + 97BD4A0D2D5CC5AE00F857D5 /* CameraSettingsTests.swift */, + 97BD4A0F2D5CE13500F857D5 /* CameraSessionPresetsTests.swift */, 97DB234C2D566D0700CEFE66 /* CameraPreviewPauseTests.swift */, 972CA92A2D5A1D8C004B846F /* CameraPropertiesTests.swift */, 972CA92C2D5A28C4004B846F /* QueueUtilsTests.swift */, @@ -192,6 +194,7 @@ 977A251F2D5A439300931E34 /* AvailableCamerasTests.swift */, 977A25212D5A49EC00931E34 /* CameraFocusTests.swift */, 977A25232D5A511600931E34 /* CameraPermissionTests.swift */, + 97C0FFAD2D5E023200A36284 /* SavePhotoDelegateTests.swift */, 979B3DF92D5B6BA2009BDE1A /* ExceptionCatcher.h */, 979B3DFA2D5B6BC7009BDE1A /* ExceptionCatcher.m */, ); @@ -524,37 +527,39 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, + 97BD4A0E2D5CC5AE00F857D5 /* CameraSettingsTests.swift in Sources */, 972CA92D2D5A28C4004B846F /* QueueUtilsTests.swift in Sources */, 979B3DFB2D5B6BC7009BDE1A /* ExceptionCatcher.m in Sources */, E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, 7FD83D2B2D5BA65B00F4DB7C /* MockCaptureConnection.m in Sources */, 977A25242D5A511600931E34 /* CameraPermissionTests.swift in Sources */, - 7D5FCCD42AEF9D0200FB7108 /* CameraSettingsTests.m in Sources */, 7F8FD2292D4BFABF001AF2C1 /* MockGlobalEventApi.m in Sources */, E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, 7FD582352D57D97C003B1200 /* MockCaptureDeviceFormat.m in Sources */, + 979B3DFE2D5B985B009BDE1A /* CameraCaptureSessionQueueRaceConditionTests.swift in Sources */, 7F29EB222D269ED500740257 /* MockEventChannel.m in Sources */, 7F8FD22F2D4D0B88001AF2C1 /* MockFlutterBinaryMessenger.m in Sources */, - E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, 972CA92B2D5A1D8C004B846F /* CameraPropertiesTests.swift in Sources */, E0CDBAC227CD9729002561D9 /* CameraTestUtils.m in Sources */, - 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, 7FD582202D579ECC003B1200 /* MockCapturePhotoOutput.m in Sources */, + 979B3E002D5B9E6C009BDE1A /* CameraMethodChannelTests.swift in Sources */, 97DB234D2D566D0700CEFE66 /* CameraPreviewPauseTests.swift in Sources */, 977A25202D5A439300931E34 /* AvailableCamerasTests.swift in Sources */, - CEF6611A2B5E36A500D33FD4 /* CameraSessionPresetsTests.m in Sources */, 972CA9312D5A366C004B846F /* CameraExposureTests.swift in Sources */, 7F29EB292D26A59000740257 /* MockCameraDeviceDiscoverer.m in Sources */, + 97BD4A102D5CE13500F857D5 /* CameraSessionPresetsTests.swift in Sources */, 788A065A27B0E02900533D74 /* StreamingTest.m in Sources */, 7FD582272D57C020003B1200 /* MockAssetWriter.m in Sources */, E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */, + 979B3E022D5BA48F009BDE1A /* CameraOrientationTests.swift in Sources */, 977A25222D5A49EC00931E34 /* CameraFocusTests.swift in Sources */, 7F29EB412D281C7E00740257 /* MockCaptureSession.m in Sources */, 7FD582122D579650003B1200 /* MockWritableData.m in Sources */, 7FCEDD352D43C2B900EA1CA8 /* MockDeviceOrientationProvider.m in Sources */, 7FCEDD362D43C2B900EA1CA8 /* MockCaptureDevice.m in Sources */, 7F8FD22C2D4D07DD001AF2C1 /* MockFlutterTextureRegistry.m in Sources */, + 97C0FFAE2D5E023200A36284 /* SavePhotoDelegateTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m deleted file mode 100644 index b651fec78649..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m +++ /dev/null @@ -1,72 +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 camera_avfoundation; -#if __has_include() -@import camera_avfoundation.Test; -#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 *cameraPlugin = [self createCameraPlugin]; - - XCTestExpectation *disposeExpectation = - [self expectationWithDescription:@"dispose's result block must be called"]; - XCTestExpectation *createExpectation = - [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. - [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(cameraPlugin.captureSessionQueue, - @"captureSessionQueue must not be nil after create method. "); -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift new file mode 100644 index 000000000000..bf0f7d300c7e --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.swift @@ -0,0 +1,54 @@ +// 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 CameraCaptureSessionQueueRaceConditionTests: XCTestCase { + private func createCameraPlugin() -> CameraPlugin { + return CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + } + + func testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition() { + let cameraPlugin = createCameraPlugin() + let disposeExpectation = expectation(description: "dispose's result block must be called") + let createExpectation = expectation(description: "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. + cameraPlugin.disposeCamera(0) { error in + disposeExpectation.fulfill() + } + + cameraPlugin.createCameraOnSessionQueue( + withName: "acamera", + settings: FCPPlatformMediaSettings.make( + with: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result, error in + createExpectation.fulfill() + } + + waitForExpectations(timeout: 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( + 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 deleted file mode 100644 index 8b6b7964f9f9..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraExposureTests.m +++ /dev/null @@ -1,81 +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 camera_avfoundation; -@import XCTest; -@import AVFoundation; - -#import "CameraTestUtils.h" -#import "MockCaptureDevice.h" -#import "MockDeviceOrientationProvider.h" - -@interface CameraExposureTests : XCTestCase -@property(readonly, nonatomic) FLTCam *camera; -@property(readonly, nonatomic) MockCaptureDevice *mockDevice; -@property(readonly, nonatomic) MockDeviceOrientationProvider *mockDeviceOrientationProvider; -@end - -@implementation CameraExposureTests - -- (void)setUp { - MockCaptureDevice *mockDevice = [[MockCaptureDevice alloc] init]; - _mockDeviceOrientationProvider = [[MockDeviceOrientationProvider alloc] init]; - _mockDevice = mockDevice; - - FLTCamConfiguration *configuration = FLTCreateTestCameraConfiguration(); - configuration.captureDeviceFactory = ^NSObject *_Nonnull { return mockDevice; }; - configuration.deviceOrientationProvider = _mockDeviceOrientationProvider; - _camera = FLTCreateCamWithConfiguration(configuration); -} - -- (void)testSetExposurePointWithResult_SetsExposurePointOfInterest { - // UI is currently in landscape left orientation - _mockDeviceOrientationProvider.orientation = UIDeviceOrientationLandscapeLeft; - // Exposure point of interest is supported - _mockDevice.exposurePointOfInterestSupported = YES; - - // Verify the focus point of interest has been set - __block CGPoint setPoint = CGPointZero; - _mockDevice.setExposurePointOfInterestStub = ^(CGPoint point) { - if (CGPointEqualToPoint(CGPointMake(1, 1), point)) { - setPoint = point; - } - }; - - // Run test - XCTestExpectation *completionExpectation = [self expectationWithDescription:@"Completion called"]; - [_camera setExposurePoint:[FCPPlatformPoint makeWithX:1 y:1] - withCompletion:^(FlutterError *_Nullable error) { - XCTAssertNil(error); - [completionExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; - XCTAssertEqual(setPoint.x, 1.0); - XCTAssertEqual(setPoint.y, 1.0); -} - -- (void)testSetExposurePoint_WhenNotSupported_ReturnsError { - // UI is currently in landscape left orientation - _mockDeviceOrientationProvider.orientation = UIDeviceOrientationLandscapeLeft; - // Exposure point of interest is not supported - _mockDevice.exposurePointOfInterestSupported = NO; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Completion with error"]; - - // Run - [_camera - setExposurePoint:[FCPPlatformPoint makeWithX:1 y:1] - withCompletion:^(FlutterError *_Nullable error) { - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.code, @"setExposurePointFailed"); - XCTAssertEqualObjects(error.message, @"Device does not have exposure point capabilities"); - [expectation fulfill]; - }]; - - // Verify - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m deleted file mode 100644 index 1f084f5ba750..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.m +++ /dev/null @@ -1,100 +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 camera_avfoundation; -#if __has_include() -@import camera_avfoundation.Test; -#endif -@import XCTest; -@import AVFoundation; - -#import "MockCameraDeviceDiscoverer.h" -#import "MockCaptureDevice.h" -#import "MockCaptureSession.h" -#import "MockFlutterBinaryMessenger.h" -#import "MockFlutterTextureRegistry.h" -#import "MockGlobalEventApi.h" - -@interface CameraMethodChannelTests : XCTestCase -@end - -@implementation CameraMethodChannelTests - -- (CameraPlugin *)createCameraPluginWithSession:(MockCaptureSession *)mockSession { - return [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] - messenger:[[MockFlutterBinaryMessenger alloc] init] - globalAPI:[[MockGlobalEventApi alloc] init] - deviceDiscoverer:[[MockCameraDeviceDiscoverer alloc] init] - deviceFactory:^NSObject *(NSString *name) { - return [[MockCaptureDevice alloc] init]; - } - captureSessionFactory:^NSObject *_Nonnull { - return mockSession; - } - captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; -} - -- (void)testCreate_ShouldCallResultOnMainThread { - MockCaptureSession *avCaptureSessionMock = [[MockCaptureSession alloc] init]; - avCaptureSessionMock.canSetSessionPreset = YES; - - CameraPlugin *camera = [self createCameraPluginWithSession:avCaptureSessionMock]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; - - // Set up method call - __block NSNumber *resultValue; - [camera createCameraOnSessionQueueWithName:@"acamera" - settings:[FCPPlatformMediaSettings - makeWithResolutionPreset: - FCPPlatformResolutionPresetMedium - framesPerSecond:nil - videoBitrate:nil - audioBitrate:nil - enableAudio:YES] - completion:^(NSNumber *_Nullable result, - FlutterError *_Nullable error) { - resultValue = result; - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; - - // Verify the result - XCTAssertNotNil(resultValue); -} - -- (void)testDisposeShouldDeallocCamera { - MockCaptureSession *avCaptureSessionMock = [[MockCaptureSession alloc] init]; - avCaptureSessionMock.canSetSessionPreset = YES; - - CameraPlugin *camera = [self createCameraPluginWithSession:avCaptureSessionMock]; - - XCTestExpectation *createExpectation = - [self expectationWithDescription:@"create's result block must be called"]; - [camera 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]; - XCTAssertNotNil(camera.camera); - - XCTestExpectation *disposeExpectation = - [self expectationWithDescription:@"dispose's result block must be called"]; - [camera disposeCamera:0 - completion:^(FlutterError *_Nullable error) { - [disposeExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; - XCTAssertNil(camera.camera, @"camera should be deallocated after dispose"); -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift new file mode 100644 index 000000000000..5ad126528750 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraMethodChannelTests.swift @@ -0,0 +1,78 @@ +// 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 CameraMethodChannelTests: XCTestCase { + private func createCameraPlugin(with session: MockCaptureSession) -> CameraPlugin { + return CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + deviceFactory: { _ in MockCaptureDevice() }, + captureSessionFactory: { session }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + } + + func testCreate_ShouldCallResultOnMainThread() { + let avCaptureSessionMock = MockCaptureSession() + avCaptureSessionMock.canSetSessionPreset = true + let camera = createCameraPlugin(with: avCaptureSessionMock) + let expectation = self.expectation(description: "Result finished") + + var resultValue: NSNumber? + camera.createCameraOnSessionQueue( + withName: "acamera", + settings: FCPPlatformMediaSettings.make( + with: FCPPlatformResolutionPreset.medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result, error in + resultValue = result + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotNil(resultValue) + } + + func testDisposeShouldDeallocCamera() { + let avCaptureSessionMock = MockCaptureSession() + avCaptureSessionMock.canSetSessionPreset = true + let camera = createCameraPlugin(with: avCaptureSessionMock) + let createExpectation = self.expectation(description: "create's result block must be called") + + camera.createCameraOnSessionQueue( + withName: "acamera", + settings: FCPPlatformMediaSettings.make( + with: .medium, + framesPerSecond: nil, + videoBitrate: nil, + audioBitrate: nil, + enableAudio: true + ) + ) { result, error in + createExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotNil(camera.camera) + + let disposeExpectation = self.expectation(description: "dispose's result block must be called") + camera.disposeCamera(0) { error in + disposeExpectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNil(camera.camera, "camera should be deallocated after dispose") + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m deleted file mode 100644 index 9f988c2cdfd7..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.m +++ /dev/null @@ -1,203 +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 camera_avfoundation; -#if __has_include() -@import camera_avfoundation.Test; -#endif -@import XCTest; -@import Flutter; - -#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 - -@implementation MockCamera -- (void)setDeviceOrientation:(UIDeviceOrientation)orientation { - if (self.setDeviceOrientationStub) { - self.setDeviceOrientationStub(orientation); - } -} - -- (void)setCaptureDevice:(NSObject *)device { - self.captureDevice = device; -} - -@end - -@interface MockUIDevice : UIDevice -@property(nonatomic, assign) UIDeviceOrientation mockOrientation; -@end - -@implementation MockUIDevice -- (UIDeviceOrientation)orientation { - return self.mockOrientation; -} - -@end - -#pragma mark - - -@interface CameraOrientationTests : XCTestCase -@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]; - 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 -// async events that may have been bounced between them. -- (void)waitForRoundTripWithQueue:(dispatch_queue_t)queue { - XCTestExpectation *expectation = [[XCTestExpectation alloc] initWithDescription:@"Queue flush"]; - dispatch_async(queue, ^{ - dispatch_async(dispatch_get_main_queue(), ^{ - [expectation fulfill]; - }); - }); - [self waitForExpectations:@[ expectation ]]; -} - -- (void)sendOrientation:(UIDeviceOrientation)orientation toCamera:(CameraPlugin *)cameraPlugin { - [cameraPlugin orientationChanged:[self createMockNotificationForOrientation:orientation]]; - [self waitForRoundTripWithQueue:cameraPlugin.captureSessionQueue]; -} - -- (void)testOrientationNotifications { - [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 { - [self sendOrientation:UIDeviceOrientationFaceUp toCamera:_cameraPlugin]; - - XCTAssertFalse(_eventAPI.deviceOrientationChangedCalled); -} - -- (void)testOrientationNotificationsNotCalledForFaceDown { - [self sendOrientation:UIDeviceOrientationFaceDown toCamera:_cameraPlugin]; - - XCTAssertFalse(_eventAPI.deviceOrientationChangedCalled); -} - -- (void)testOrientationUpdateMustBeOnCaptureSessionQueue { - XCTestExpectation *queueExpectation = [self - expectationWithDescription:@"Orientation update must happen on the capture session queue"]; - - CameraPlugin *plugin = - [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] - messenger:[[MockFlutterBinaryMessenger alloc] init]]; - const char *captureSessionQueueSpecific = "capture_session_queue"; - dispatch_queue_set_specific(plugin.captureSessionQueue, captureSessionQueueSpecific, - (void *)captureSessionQueueSpecific, NULL); - plugin.camera = _camera; - - _camera.setDeviceOrientationStub = ^(UIDeviceOrientation orientation) { - if (dispatch_get_specific(captureSessionQueueSpecific)) { - [queueExpectation fulfill]; - } - }; - - [plugin orientationChanged: - [self createMockNotificationForOrientation:UIDeviceOrientationLandscapeLeft]]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)testOrientationChanged_noRetainCycle { - dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); - - __weak CameraPlugin *weakPlugin; - __weak MockCaptureDevice *weakDevice = _mockDevice; - - @autoreleasepool { - 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(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, ^{ - XCTAssertFalse(setDeviceOrientationCalled); - XCTAssertFalse(weakEventAPI.deviceOrientationChangedCalled); - [expectation fulfill]; - }); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (NSNotification *)createMockNotificationForOrientation:(UIDeviceOrientation)deviceOrientation { - MockUIDevice *mockDevice = [[MockUIDevice alloc] init]; - mockDevice.mockOrientation = deviceOrientation; - - return [NSNotification notificationWithName:@"orientation_test" object:mockDevice]; -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift new file mode 100644 index 000000000000..d6198f35d885 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraOrientationTests.swift @@ -0,0 +1,166 @@ +// 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 Flutter +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 + + override var orientation: UIDeviceOrientation { + return mockOrientation + } +} + +final class CameraOrientationTests: XCTestCase { + private func createCameraPlugin() -> ( + CameraPlugin, MockCamera, MockGlobalEventApi, MockCaptureDevice, MockCameraDeviceDiscoverer + ) { + let mockDevice = MockCaptureDevice() + let mockCamera = MockCamera() + let mockEventAPI = MockGlobalEventApi() + let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() + + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: mockEventAPI, + deviceDiscoverer: mockDeviceDiscoverer, + deviceFactory: { _ in mockDevice }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + cameraPlugin.camera = mockCamera + + return (cameraPlugin, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer) + } + + 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) + } + + private func sendOrientation(_ orientation: UIDeviceOrientation, to cameraPlugin: CameraPlugin) { + cameraPlugin.orientationChanged(createMockNotification(for: orientation)) + waitForRoundTrip(with: cameraPlugin.captureSessionQueue) + } + + private func createMockNotification(for deviceOrientation: UIDeviceOrientation) -> Notification { + let mockDevice = MockUIDevice() + mockDevice.mockOrientation = deviceOrientation + return Notification(name: Notification.Name("orientation_test"), object: mockDevice) + } + + func testOrientationNotifications() { + let (cameraPlugin, _, mockEventAPI, _, _) = createCameraPlugin() + + sendOrientation(.portraitUpsideDown, to: cameraPlugin) + XCTAssertEqual(mockEventAPI.lastOrientation, .portraitDown) + sendOrientation(.portrait, to: cameraPlugin) + XCTAssertEqual(mockEventAPI.lastOrientation, .portraitUp) + sendOrientation(.landscapeLeft, to: cameraPlugin) + XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeLeft) + sendOrientation(.landscapeRight, to: cameraPlugin) + XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeRight) + } + + func testOrientationNotificationsNotCalledForFaceUp() { + let (cameraPlugin, _, mockEventAPI, _, _) = createCameraPlugin() + sendOrientation(.faceUp, to: cameraPlugin) + XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) + } + + func testOrientationNotificationsNotCalledForFaceDown() { + let (cameraPlugin, _, mockEventAPI, _, _) = createCameraPlugin() + sendOrientation(.faceDown, to: cameraPlugin) + XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) + } + + func testOrientationUpdateMustBeOnCaptureSessionQueue() { + let queueExpectation = expectation( + description: "Orientation update must happen on the capture session queue") + let (cameraPlugin, mockCamera, _, _, _) = createCameraPlugin() + let captureSessionQueueSpecific = DispatchSpecificKey() + cameraPlugin.captureSessionQueue.setSpecific( + key: captureSessionQueueSpecific, + value: ()) + + mockCamera.setDeviceOrientationStub = { orientation in + if DispatchQueue.getSpecific(key: captureSessionQueueSpecific) != nil { + queueExpectation.fulfill() + } + } + + cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) + waitForExpectations(timeout: 30, handler: nil) + } + + func testOrientationChangedNoRetainCycle() { + let (_, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer) = createCameraPlugin() + let captureSessionQueue = DispatchQueue(label: "capture_session_queue") + weak var weakPlugin: CameraPlugin? + weak var weakDevice = mockDevice + + autoreleasepool { + let cameraPlugin = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: mockEventAPI, + deviceDiscoverer: mockDeviceDiscoverer, + deviceFactory: { _ in weakDevice! }, + captureSessionFactory: { MockCaptureSession() }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + weakPlugin = cameraPlugin + cameraPlugin.captureSessionQueue = captureSessionQueue + cameraPlugin.camera = mockCamera + + cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) + } + + // Sanity check. + let cameraDeallocatedExpectation = self.expectation( + description: "Camera must have been deallocated.") + captureSessionQueue.async { + XCTAssertNil(weakPlugin) + cameraDeallocatedExpectation.fulfill() + } + // Awaiting expectation is needed. The test is flaky when checking for nil right away. + waitForExpectations(timeout: 1, handler: nil) + + var setDeviceOrientationCalled = false + mockCamera.setDeviceOrientationStub = { orientation in + if orientation == .landscapeLeft { + setDeviceOrientationCalled = true + } + } + + weak var weakEventAPI = mockEventAPI + // Must check in captureSessionQueue since orientationChanged dispatches to this queue. + let expectation = self.expectation(description: "Dispatched to capture session queue") + captureSessionQueue.async { + XCTAssertFalse(setDeviceOrientationCalled) + XCTAssertFalse(weakEventAPI?.deviceOrientationChangedCalled ?? false) + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m deleted file mode 100644 index 14b4f2e9bccf..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.m +++ /dev/null @@ -1,119 +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 camera_avfoundation; -#if __has_include() -@import camera_avfoundation.Test; -#endif - -@import AVFoundation; -@import XCTest; - -#import "CameraTestUtils.h" -#import "MockCaptureDevice.h" -#import "MockCaptureDeviceFormat.h" -#import "MockCaptureSession.h" - -/// Includes test cases related to resolution presets setting operations for FLTCam class. -@interface FLTCamSessionPresetsTest : XCTestCase -@end - -@implementation FLTCamSessionPresetsTest - -- (void)testResolutionPresetWithBestFormat_mustUpdateCaptureSessionPreset { - NSString *expectedPreset = AVCaptureSessionPresetInputPriority; - XCTestExpectation *presetExpectation = [self expectationWithDescription:@"Expected preset set"]; - XCTestExpectation *lockForConfigurationExpectation = - [self expectationWithDescription:@"Expected lockForConfiguration called"]; - - MockCaptureSession *videoSessionMock = [[MockCaptureSession alloc] init]; - videoSessionMock.setSessionPresetStub = ^(NSString *preset) { - if (preset == expectedPreset) { - [presetExpectation fulfill]; - } - }; - - MockCaptureDeviceFormat *captureFormatMock = [[MockCaptureDeviceFormat alloc] init]; - - MockCaptureDevice *captureDeviceMock = [[MockCaptureDevice alloc] init]; - captureDeviceMock.formats = @[ captureFormatMock ]; - captureDeviceMock.activeFormat = captureFormatMock; - captureDeviceMock.lockForConfigurationStub = - ^BOOL(NSError *__autoreleasing _Nullable *_Nullable error) { - [lockForConfigurationExpectation fulfill]; - return YES; - }; - - FLTCamConfiguration *configuration = FLTCreateTestCameraConfiguration(); - configuration.captureDeviceFactory = ^NSObject *_Nonnull { - return captureDeviceMock; - }; - configuration.videoDimensionsForFormat = - ^CMVideoDimensions(NSObject *format) { - CMVideoDimensions videoDimensions; - videoDimensions.width = 1; - videoDimensions.height = 1; - return videoDimensions; - }; - configuration.videoCaptureSession = videoSessionMock; - configuration.mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPresetMax); - - FLTCreateCamWithConfiguration(configuration); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)testResolutionPresetWithCanSetSessionPresetMax_mustUpdateCaptureSessionPreset { - NSString *expectedPreset = AVCaptureSessionPreset3840x2160; - XCTestExpectation *expectation = [self expectationWithDescription:@"Expected preset set"]; - - MockCaptureSession *videoSessionMock = [[MockCaptureSession alloc] init]; - - // Make sure that setting resolution preset for session always succeeds. - videoSessionMock.canSetSessionPreset = YES; - - videoSessionMock.setSessionPresetStub = ^(NSString *preset) { - if (preset == expectedPreset) { - [expectation fulfill]; - } - }; - - FLTCamConfiguration *configuration = FLTCreateTestCameraConfiguration(); - configuration.videoCaptureSession = videoSessionMock; - configuration.mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPresetMax); - configuration.captureDeviceFactory = ^NSObject * { - return [[MockCaptureDevice alloc] init]; - }; - - FLTCreateCamWithConfiguration(configuration); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)testResolutionPresetWithCanSetSessionPresetUltraHigh_mustUpdateCaptureSessionPreset { - NSString *expectedPreset = AVCaptureSessionPreset3840x2160; - XCTestExpectation *expectation = [self expectationWithDescription:@"Expected preset set"]; - - MockCaptureSession *videoSessionMock = [[MockCaptureSession alloc] init]; - - // Make sure that setting resolution preset for session always succeeds. - videoSessionMock.canSetSessionPreset = YES; - - // Expect that setting "ultraHigh" resolutionPreset correctly updates videoCaptureSession. - videoSessionMock.setSessionPresetStub = ^(NSString *preset) { - if (preset == expectedPreset) { - [expectation fulfill]; - } - }; - - FLTCamConfiguration *configuration = FLTCreateTestCameraConfiguration(); - configuration.videoCaptureSession = videoSessionMock; - configuration.mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPresetUltraHigh); - - FLTCreateCamWithConfiguration(configuration); - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift new file mode 100644 index 000000000000..4be4db51554c --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSessionPresetsTests.swift @@ -0,0 +1,91 @@ +// 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 + +/// Includes test cases related to resolution presets setting operations for FLTCam class. +final class CameraSessionPresetsTests: XCTestCase { + func testResolutionPresetWithBestFormat_mustUpdateCaptureSessionPreset() { + let expectedPreset = AVCaptureSession.Preset.inputPriority + let presetExpectation = expectation(description: "Expected preset set") + let lockForConfigurationExpectation = expectation( + description: "Expected lockForConfiguration called") + + let videoSessionMock = MockCaptureSession() + videoSessionMock.setSessionPresetStub = { preset in + if preset == expectedPreset { + presetExpectation.fulfill() + } + } + let captureFormatMock = MockCaptureDeviceFormat() + let captureDeviceMock = MockCaptureDevice() + captureDeviceMock.formats = [captureFormatMock] + captureDeviceMock.activeFormat = captureFormatMock + captureDeviceMock.lockForConfigurationStub = { error in + lockForConfigurationExpectation.fulfill() + return true + } + + let configuration = FLTCreateTestCameraConfiguration() + configuration.captureDeviceFactory = { captureDeviceMock } + configuration.videoDimensionsForFormat = { format in + return CMVideoDimensions(width: 1, height: 1) + } + configuration.videoCaptureSession = videoSessionMock + configuration.mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPreset.max) + + FLTCreateCamWithConfiguration(configuration) + + waitForExpectations(timeout: 30, handler: nil) + } + + func testResolutionPresetWithCanSetSessionPresetMax_mustUpdateCaptureSessionPreset() { + let expectedPreset = AVCaptureSession.Preset.hd4K3840x2160 + let expectation = self.expectation(description: "Expected preset set") + + let videoSessionMock = MockCaptureSession() + // Make sure that setting resolution preset for session always succeeds. + videoSessionMock.canSetSessionPreset = true + videoSessionMock.setSessionPresetStub = { preset in + if preset == expectedPreset { + expectation.fulfill() + } + } + + let configuration = FLTCreateTestCameraConfiguration() + configuration.videoCaptureSession = videoSessionMock + configuration.mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPreset.max) + configuration.captureDeviceFactory = { MockCaptureDevice() } + + FLTCreateCamWithConfiguration(configuration) + + waitForExpectations(timeout: 30, handler: nil) + } + + func testResolutionPresetWithCanSetSessionPresetUltraHigh_mustUpdateCaptureSessionPreset() { + let expectedPreset = AVCaptureSession.Preset.hd4K3840x2160 + let expectation = self.expectation(description: "Expected preset set") + + let videoSessionMock = MockCaptureSession() + // Make sure that setting resolution preset for session always succeeds. + videoSessionMock.canSetSessionPreset = true + // Expect that setting "ultraHigh" resolutionPreset correctly updates videoCaptureSession. + videoSessionMock.setSessionPresetStub = { preset in + if preset == expectedPreset { + expectation.fulfill() + } + } + + let configuration = FLTCreateTestCameraConfiguration() + configuration.videoCaptureSession = videoSessionMock + configuration.mediaSettings = FCPGetDefaultMediaSettings(FCPPlatformResolutionPreset.ultraHigh) + + FLTCreateCamWithConfiguration(configuration) + + waitForExpectations(timeout: 30, handler: nil) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m deleted file mode 100644 index 55da062cceb2..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.m +++ /dev/null @@ -1,237 +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 camera_avfoundation; -#if __has_include() -@import camera_avfoundation.Test; -#endif -@import XCTest; -@import AVFoundation; - -#import "CameraTestUtils.h" -#import "MockAssetWriter.h" -#import "MockCameraDeviceDiscoverer.h" -#import "MockCaptureDevice.h" -#import "MockCaptureSession.h" -#import "MockFlutterBinaryMessenger.h" -#import "MockFlutterTextureRegistry.h" -#import "MockGlobalEventApi.h" - -static const FCPPlatformResolutionPreset gTestResolutionPreset = FCPPlatformResolutionPresetMedium; -static const int gTestFramesPerSecond = 15; -static const int gTestVideoBitrate = 200000; -static const int gTestAudioBitrate = 32000; -static const BOOL gTestEnableAudio = YES; - -@interface CameraSettingsTests : XCTestCase -@end - -/** - * A test implemetation of `FLTCamMediaSettingsAVWrapper` - * - * This xctest-expectation-checking implementation of `FLTCamMediaSettingsAVWrapper` is injected - * into `camera-avfoundation` plugin instead of real AVFoundation-based realization. - * Such kind of Dependency Injection (DI) allows to run media-settings tests without - * any additional mocking of AVFoundation classes. - */ -@interface TestMediaSettingsAVWrapper : FLTCamMediaSettingsAVWrapper -@property(nonatomic, readonly) XCTestExpectation *lockExpectation; -@property(nonatomic, readonly) XCTestExpectation *unlockExpectation; -@property(nonatomic, readonly) XCTestExpectation *minFrameDurationExpectation; -@property(nonatomic, readonly) XCTestExpectation *maxFrameDurationExpectation; -@property(nonatomic, readonly) XCTestExpectation *beginConfigurationExpectation; -@property(nonatomic, readonly) XCTestExpectation *commitConfigurationExpectation; -@property(nonatomic, readonly) XCTestExpectation *audioSettingsExpectation; -@property(nonatomic, readonly) XCTestExpectation *videoSettingsExpectation; -@end - -@implementation TestMediaSettingsAVWrapper - -- (instancetype)initWithTestCase:(XCTestCase *)test { - _lockExpectation = [test expectationWithDescription:@"lockExpectation"]; - _unlockExpectation = [test expectationWithDescription:@"unlockExpectation"]; - _minFrameDurationExpectation = [test expectationWithDescription:@"minFrameDurationExpectation"]; - _maxFrameDurationExpectation = [test expectationWithDescription:@"maxFrameDurationExpectation"]; - _beginConfigurationExpectation = - [test expectationWithDescription:@"beginConfigurationExpectation"]; - _commitConfigurationExpectation = - [test expectationWithDescription:@"commitConfigurationExpectation"]; - _audioSettingsExpectation = [test expectationWithDescription:@"audioSettingsExpectation"]; - _videoSettingsExpectation = [test expectationWithDescription:@"videoSettingsExpectation"]; - - return self; -} - -- (BOOL)lockDevice:(AVCaptureDevice *)captureDevice error:(NSError **)outError { - [_lockExpectation fulfill]; - return YES; -} - -- (void)unlockDevice:(AVCaptureDevice *)captureDevice { - [_unlockExpectation fulfill]; -} - -- (void)beginConfigurationForSession:(NSObject *)videoCaptureSession { - [_beginConfigurationExpectation fulfill]; -} - -- (void)commitConfigurationForSession:(NSObject *)videoCaptureSession { - [_commitConfigurationExpectation fulfill]; -} - -- (void)setMinFrameDuration:(CMTime)duration onDevice:(AVCaptureDevice *)captureDevice { - // FLTCam allows to set frame rate with 1/10 precision. - CMTime expectedDuration = CMTimeMake(10, gTestFramesPerSecond * 10); - - if (duration.value == expectedDuration.value && - duration.timescale == expectedDuration.timescale) { - [_minFrameDurationExpectation fulfill]; - } -} - -- (void)setMaxFrameDuration:(CMTime)duration onDevice:(AVCaptureDevice *)captureDevice { - // FLTCam allows to set frame rate with 1/10 precision. - CMTime expectedDuration = CMTimeMake(10, gTestFramesPerSecond * 10); - - if (duration.value == expectedDuration.value && - duration.timescale == expectedDuration.timescale) { - [_maxFrameDurationExpectation fulfill]; - } -} - -- (MockAssetWriterInput *)assetWriterAudioInputWithOutputSettings: - (nullable NSDictionary *)outputSettings { - if ([outputSettings[AVEncoderBitRateKey] isEqual:@(gTestAudioBitrate)]) { - [_audioSettingsExpectation fulfill]; - } - - return [[MockAssetWriterInput alloc] init]; -} - -- (MockAssetWriterInput *)assetWriterVideoInputWithOutputSettings: - (nullable NSDictionary *)outputSettings { - if ([outputSettings[AVVideoCompressionPropertiesKey] isKindOfClass:[NSMutableDictionary class]]) { - NSDictionary *compressionProperties = outputSettings[AVVideoCompressionPropertiesKey]; - - if ([compressionProperties[AVVideoAverageBitRateKey] isEqual:@(gTestVideoBitrate)] && - [compressionProperties[AVVideoExpectedSourceFrameRateKey] - isEqual:@(gTestFramesPerSecond)]) { - [_videoSettingsExpectation fulfill]; - } - } - - return [[MockAssetWriterInput alloc] init]; -} - -- (void)addInput:(NSObject *)writerInput - toAssetWriter:(NSObject *)writer { -} - -- (NSDictionary *) - recommendedVideoSettingsForAssetWriterWithFileType:(AVFileType)fileType - forOutput:(AVCaptureVideoDataOutput *)output { - return @{}; -} - -@end - -@implementation CameraSettingsTests - -/// Expect that FPS, video and audio bitrate are passed to camera device and asset writer. -- (void)testSettings_shouldPassConfigurationToCameraDeviceAndWriter { - FCPPlatformMediaSettings *settings = - [FCPPlatformMediaSettings makeWithResolutionPreset:gTestResolutionPreset - framesPerSecond:@(gTestFramesPerSecond) - videoBitrate:@(gTestVideoBitrate) - audioBitrate:@(gTestAudioBitrate) - enableAudio:gTestEnableAudio]; - TestMediaSettingsAVWrapper *injectedWrapper = - [[TestMediaSettingsAVWrapper alloc] initWithTestCase:self]; - - FLTCamConfiguration *configuration = FLTCreateTestCameraConfiguration(); - configuration.mediaSettingsWrapper = injectedWrapper; - configuration.mediaSettings = settings; - FLTCam *camera = FLTCreateCamWithConfiguration(configuration); - - // Expect FPS configuration is passed to camera device. - [self waitForExpectations:@[ - injectedWrapper.lockExpectation, injectedWrapper.beginConfigurationExpectation, - injectedWrapper.minFrameDurationExpectation, injectedWrapper.maxFrameDurationExpectation, - injectedWrapper.commitConfigurationExpectation, injectedWrapper.unlockExpectation - ] - timeout:1 - enforceOrder:YES]; - - [camera - startVideoRecordingWithCompletion:^(FlutterError *_Nullable error) { - } - messengerForStreaming:nil]; - - [self waitForExpectations:@[ - injectedWrapper.audioSettingsExpectation, injectedWrapper.videoSettingsExpectation - ] - timeout:1]; -} - -- (void)testSettings_ShouldBeSupportedByMethodCall { - MockCaptureDevice *mockDevice = [[MockCaptureDevice alloc] init]; - MockCaptureSession *mockSession = [[MockCaptureSession alloc] init]; - mockSession.canSetSessionPreset = YES; - - CameraPlugin *camera = - [[CameraPlugin alloc] initWithRegistry:[[MockFlutterTextureRegistry alloc] init] - messenger:[[MockFlutterBinaryMessenger alloc] init] - globalAPI:[[MockGlobalEventApi alloc] init] - deviceDiscoverer:[[MockCameraDeviceDiscoverer alloc] init] - deviceFactory:^NSObject *(NSString *name) { - return mockDevice; - } - captureSessionFactory:^NSObject *_Nonnull { - return mockSession; - } - captureDeviceInputFactory:[[MockCaptureDeviceInputFactory alloc] init]]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; - - // Set up method call - FCPPlatformMediaSettings *mediaSettings = - [FCPPlatformMediaSettings makeWithResolutionPreset:gTestResolutionPreset - framesPerSecond:@(gTestFramesPerSecond) - videoBitrate:@(gTestVideoBitrate) - audioBitrate:@(gTestAudioBitrate) - enableAudio:gTestEnableAudio]; - - __block NSNumber *resultValue; - [camera createCameraOnSessionQueueWithName:@"acamera" - settings:mediaSettings - completion:^(NSNumber *result, FlutterError *error) { - XCTAssertNil(error); - resultValue = result; - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; - - // Verify the result - XCTAssertNotNil(resultValue); -} - -- (void)testSettings_ShouldSelectFormatWhichSupports60FPS { - FCPPlatformMediaSettings *settings = - [FCPPlatformMediaSettings makeWithResolutionPreset:gTestResolutionPreset - framesPerSecond:@(60) - videoBitrate:@(gTestVideoBitrate) - audioBitrate:@(gTestAudioBitrate) - enableAudio:gTestEnableAudio]; - - FLTCamConfiguration *configuration = FLTCreateTestCameraConfiguration(); - configuration.mediaSettings = settings; - FLTCam *camera = FLTCreateCamWithConfiguration(configuration); - - NSObject *range = - camera.captureDevice.activeFormat.videoSupportedFrameRateRanges[0]; - XCTAssertLessThanOrEqual(range.minFrameRate, 60); - XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60); -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift new file mode 100644 index 000000000000..eb3d06dff49f --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraSettingsTests.swift @@ -0,0 +1,203 @@ +// 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 + +private let testResolutionPreset = FCPPlatformResolutionPreset.medium +private let testFramesPerSecond = 15 +private let testVideoBitrate = 200000 +private let testAudioBitrate = 32000 +private let testEnableAudio = true + +private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper { + let lockExpectation: XCTestExpectation + let unlockExpectation: XCTestExpectation + let minFrameDurationExpectation: XCTestExpectation + let maxFrameDurationExpectation: XCTestExpectation + let beginConfigurationExpectation: XCTestExpectation + let commitConfigurationExpectation: XCTestExpectation + let audioSettingsExpectation: XCTestExpectation + let videoSettingsExpectation: XCTestExpectation + + init(test: XCTestCase) { + lockExpectation = test.expectation(description: "lockExpectation") + unlockExpectation = test.expectation(description: "unlockExpectation") + minFrameDurationExpectation = test.expectation(description: "minFrameDurationExpectation") + maxFrameDurationExpectation = test.expectation(description: "maxFrameDurationExpectation") + beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation") + commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation") + audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation") + videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation") + } + + override func lockDevice(_ captureDevice: FLTCaptureDevice) throws { + lockExpectation.fulfill() + } + + override func unlockDevice(_ captureDevice: FLTCaptureDevice) { + unlockExpectation.fulfill() + } + + override func beginConfiguration(for videoCaptureSession: FLTCaptureSession) { + beginConfigurationExpectation.fulfill() + } + + override func commitConfiguration(for videoCaptureSession: FLTCaptureSession) { + commitConfigurationExpectation.fulfill() + } + + override func setMinFrameDuration(_ duration: CMTime, on captureDevice: FLTCaptureDevice) { + // FLTCam allows to set frame rate with 1/10 precision. + let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10)) + if duration == expectedDuration { + minFrameDurationExpectation.fulfill() + } + } + + override func setMaxFrameDuration(_ duration: CMTime, on captureDevice: FLTCaptureDevice) { + // FLTCam allows to set frame rate with 1/10 precision. + let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10)) + if duration == expectedDuration { + maxFrameDurationExpectation.fulfill() + } + } + + override func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?) + -> FLTAssetWriterInput + { + if let bitrate = outputSettings?[AVEncoderBitRateKey] as? Int, bitrate == testAudioBitrate { + audioSettingsExpectation.fulfill() + } + return MockAssetWriterInput() + } + + override func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?) + -> FLTAssetWriterInput + { + if let compressionProperties = outputSettings?[AVVideoCompressionPropertiesKey] + as? [String: Any], + let bitrate = compressionProperties[AVVideoAverageBitRateKey] as? Int, + let frameRate = compressionProperties[AVVideoExpectedSourceFrameRateKey] as? Int, + bitrate == testVideoBitrate, frameRate == testFramesPerSecond + { + videoSettingsExpectation.fulfill() + } + + // AVAssetWriterInput needs these three keys, otherwise it throws. + var outputSettingsWithRequiredKeys = outputSettings ?? [:] + outputSettingsWithRequiredKeys[AVVideoCodecKey] = AVVideoCodecType.h264 + outputSettingsWithRequiredKeys[AVVideoWidthKey] = 1280 + outputSettingsWithRequiredKeys[AVVideoHeightKey] = 720 + + return MockAssetWriterInput() + } + + override func addInput(_ writerInput: FLTAssetWriterInput, to writer: FLTAssetWriter) { + // No-op. + } + + override func recommendedVideoSettingsForAssetWriter( + withFileType fileType: AVFileType, for output: AVCaptureVideoDataOutput + ) -> [String: Any]? { + return [:] + } + +} + +final class CameraSettingsTests: XCTestCase { + func testSettings_shouldPassConfigurationToCameraDeviceAndWriter() { + let settings = FCPPlatformMediaSettings.make( + with: testResolutionPreset, + framesPerSecond: NSNumber(value: testFramesPerSecond), + videoBitrate: NSNumber(value: testVideoBitrate), + audioBitrate: NSNumber(value: testAudioBitrate), + enableAudio: testEnableAudio + ) + let injectedWrapper = TestMediaSettingsAVWrapper(test: self) + + let configuration = FLTCreateTestCameraConfiguration() + configuration.mediaSettingsWrapper = injectedWrapper + configuration.mediaSettings = settings + let camera = FLTCreateCamWithConfiguration(configuration) + + // Expect FPS configuration is passed to camera device. + wait( + for: [ + injectedWrapper.lockExpectation, + injectedWrapper.beginConfigurationExpectation, + injectedWrapper.minFrameDurationExpectation, + injectedWrapper.maxFrameDurationExpectation, + injectedWrapper.commitConfigurationExpectation, + injectedWrapper.unlockExpectation, + ], timeout: 1, enforceOrder: true) + + camera.startVideoRecording( + completion: { error in + // No-op. + }, messengerForStreaming: nil) + + wait( + for: [ + injectedWrapper.audioSettingsExpectation, + injectedWrapper.videoSettingsExpectation, + ], timeout: 1) + } + + func testSettings_ShouldBeSupportedByMethodCall() { + let mockDevice = MockCaptureDevice() + let mockSession = MockCaptureSession() + mockSession.canSetSessionPreset = true + let camera = CameraPlugin( + registry: MockFlutterTextureRegistry(), + messenger: MockFlutterBinaryMessenger(), + globalAPI: MockGlobalEventApi(), + deviceDiscoverer: MockCameraDeviceDiscoverer(), + deviceFactory: { _ in mockDevice }, + captureSessionFactory: { mockSession }, + captureDeviceInputFactory: MockCaptureDeviceInputFactory() + ) + + let expectation = self.expectation(description: "Result finished") + let mediaSettings = FCPPlatformMediaSettings.make( + with: testResolutionPreset, + framesPerSecond: NSNumber(value: testFramesPerSecond), + videoBitrate: NSNumber(value: testVideoBitrate), + audioBitrate: NSNumber(value: testAudioBitrate), + enableAudio: testEnableAudio + ) + var resultValue: NSNumber? + camera.createCameraOnSessionQueue( + withName: "acamera", + settings: mediaSettings + ) { result, error in + XCTAssertNil(error) + resultValue = result + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + XCTAssertNotNil(resultValue) + } + + func testSettings_ShouldSelectFormatWhichSupports60FPS() { + let settings = FCPPlatformMediaSettings.make( + with: testResolutionPreset, + framesPerSecond: NSNumber(value: 60), + videoBitrate: NSNumber(value: testVideoBitrate), + audioBitrate: NSNumber(value: testAudioBitrate), + enableAudio: testEnableAudio + ) + + let configuration = FLTCreateTestCameraConfiguration() + configuration.mediaSettings = settings + let camera = FLTCreateCamWithConfiguration(configuration) + + let range = camera.captureDevice.activeFormat.videoSupportedFrameRateRanges[0] + XCTAssertLessThanOrEqual(range.minFrameRate, 60) + XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60) + } +} diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m deleted file mode 100644 index 94354df696b2..000000000000 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ /dev/null @@ -1,141 +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 camera_avfoundation; -#if __has_include() -@import camera_avfoundation.Test; -#endif -@import AVFoundation; -@import XCTest; - -#import "MockWritableData.h" - -@interface FLTSavePhotoDelegateTests : XCTestCase - -@end - -@implementation FLTSavePhotoDelegateTests - -- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture { - XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"Must complete with error if failed to capture photo."]; - - NSError *captureError = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; - dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] - initWithPath:@"test" - ioQueue:ioQueue - completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { - XCTAssertEqualObjects(captureError, error); - XCTAssertNil(path); - [completionExpectation fulfill]; - }]; - - [delegate handlePhotoCaptureResultWithError:captureError - photoDataProvider:^NSData * { - return nil; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { - XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"Must complete with error if failed to write file."]; - dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - - NSError *ioError = [NSError errorWithDomain:@"IOError" - code:0 - userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}]; - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] - initWithPath:@"test" - ioQueue:ioQueue - completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { - XCTAssertEqualObjects(ioError, error); - XCTAssertNil(path); - [completionExpectation fulfill]; - }]; - - MockWritableData *mockWritableData = [[MockWritableData alloc] init]; - mockWritableData.writeToFileStub = - ^BOOL(NSString *path, NSDataWritingOptions options, NSError *__autoreleasing *error) { - *error = ioError; - return NO; - }; - - [delegate handlePhotoCaptureResultWithError:nil - photoDataProvider:^NSObject * { - return mockWritableData; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { - XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"Must complete with file path if success to write file."]; - - dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - NSString *filePath = @"test"; - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] - initWithPath:filePath - ioQueue:ioQueue - completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { - XCTAssertNil(error); - XCTAssertEqualObjects(filePath, path); - [completionExpectation fulfill]; - }]; - - MockWritableData *mockWritableData = [[MockWritableData alloc] init]; - mockWritableData.writeToFileStub = - ^BOOL(NSString *path, NSDataWritingOptions options, NSError *__autoreleasing *error) { - return YES; - }; - - [delegate handlePhotoCaptureResultWithError:nil - photoDataProvider:^NSObject * { - return mockWritableData; - }]; - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -- (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue { - XCTestExpectation *dataProviderQueueExpectation = - [self expectationWithDescription:@"Data provider must run on io queue."]; - XCTestExpectation *writeFileQueueExpectation = - [self expectationWithDescription:@"File writing must run on io queue"]; - XCTestExpectation *completionExpectation = - [self expectationWithDescription:@"Must complete with file path if success to write file."]; - - dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - const char *ioQueueSpecific = "io_queue_specific"; - dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); - - MockWritableData *mockWritableData = [[MockWritableData alloc] init]; - mockWritableData.writeToFileStub = - ^BOOL(NSString *path, NSDataWritingOptions options, NSError *__autoreleasing *error) { - if (dispatch_get_specific(ioQueueSpecific)) { - [writeFileQueueExpectation fulfill]; - } - return YES; - }; - - NSString *filePath = @"test"; - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] - initWithPath:filePath - ioQueue:ioQueue - completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { - [completionExpectation fulfill]; - }]; - - [delegate handlePhotoCaptureResultWithError:nil - photoDataProvider:^NSObject * { - if (dispatch_get_specific(ioQueueSpecific)) { - [dataProviderQueueExpectation fulfill]; - } - return mockWritableData; - }]; - - [self waitForExpectationsWithTimeout:30 handler:nil]; -} - -@end diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h b/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h index 5a3f4609c5dd..4e0309c49098 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/RunnerTests-Bridging-Header.h @@ -5,19 +5,22 @@ // Sources. #import "camera_avfoundation/CameraPlugin.h" #import "camera_avfoundation/CameraPlugin_Test.h" -#import "camera_avfoundation/FLTCam.h" #import "camera_avfoundation/FLTCamConfiguration.h" +#import "camera_avfoundation/FLTCam_Test.h" #import "camera_avfoundation/FLTThreadSafeEventChannel.h" // Mocks, protocols. +#import "MockAssetWriter.h" #import "MockCameraDeviceDiscoverer.h" #import "MockCaptureDevice.h" +#import "MockCaptureDeviceFormat.h" #import "MockCaptureSession.h" #import "MockDeviceOrientationProvider.h" #import "MockEventChannel.h" #import "MockFlutterBinaryMessenger.h" #import "MockFlutterTextureRegistry.h" #import "MockGlobalEventApi.h" +#import "MockWritableData.h" // Utils. #import "CameraTestUtils.h" diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/SavePhotoDelegateTests.swift b/packages/camera/camera_avfoundation/example/ios/RunnerTests/SavePhotoDelegateTests.swift new file mode 100644 index 000000000000..02a927557aa7 --- /dev/null +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/SavePhotoDelegateTests.swift @@ -0,0 +1,104 @@ +// 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 SavePhotoDelegateTests: XCTestCase { + func testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture() { + let completionExpectation = expectation( + description: "Must complete with error if failed to capture photo.") + let captureError = NSError(domain: "test", code: 0, userInfo: nil) + let ioQueue = DispatchQueue(label: "test") + let delegate = FLTSavePhotoDelegate(path: "test", ioQueue: ioQueue) { path, error in + XCTAssertEqual(captureError, error as NSError?) + XCTAssertNil(path) + completionExpectation.fulfill() + } + + delegate.handlePhotoCaptureResult(error: captureError) { nil } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite() { + let completionExpectation = expectation( + description: "Must complete with error if failed to write file.") + let ioQueue = DispatchQueue(label: "test") + let ioError = NSError( + domain: "IOError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Localized IO Error"]) + let delegate = FLTSavePhotoDelegate(path: "test", ioQueue: ioQueue) { path, error in + XCTAssertEqual(ioError, error as NSError?) + XCTAssertNil(path) + completionExpectation.fulfill() + } + + let mockWritableData = MockWritableData() + mockWritableData.writeToFileStub = { path, options, error in + // TODO(FirentisTFW) Throw an error instead when migrating FLTWritableData to Swift + error?.pointee = ioError + return false + } + + delegate.handlePhotoCaptureResult(error: nil) { mockWritableData } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite() { + let completionExpectation = expectation( + description: "Must complete with file path if succeeds to write file.") + let ioQueue = DispatchQueue(label: "test") + let filePath = "test" + let delegate = FLTSavePhotoDelegate(path: filePath, ioQueue: ioQueue) { path, error in + XCTAssertNil(error) + XCTAssertEqual(filePath, path) + completionExpectation.fulfill() + } + + let mockWritableData = MockWritableData() + mockWritableData.writeToFileStub = { path, options, error in + return true + } + + delegate.handlePhotoCaptureResult(error: nil) { mockWritableData } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue() { + let dataProviderQueueExpectation = expectation( + description: "Data provider must run on io queue.") + let writeFileQueueExpectation = expectation(description: "File writing must run on io queue.") + let completionExpectation = expectation( + description: "Must complete with file path if success to write file.") + let ioQueue = DispatchQueue(label: "test") + let ioQueueSpecific = DispatchSpecificKey() + ioQueue.setSpecific(key: ioQueueSpecific, value: ()) + + let mockWritableData = MockWritableData() + mockWritableData.writeToFileStub = { path, options, error in + if DispatchQueue.getSpecific(key: ioQueueSpecific) != nil { + writeFileQueueExpectation.fulfill() + } + return true + } + + let filePath = "test" + let delegate = FLTSavePhotoDelegate(path: filePath, ioQueue: ioQueue) { path, error in + completionExpectation.fulfill() + } + + delegate.handlePhotoCaptureResult(error: nil) { + if DispatchQueue.getSpecific(key: ioQueueSpecific) != nil { + dataProviderQueueExpectation.fulfill() + } + return mockWritableData + } + + waitForExpectations(timeout: 30, handler: nil) + } +} 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 4e3e0311bb6f..14f5db2c7d04 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 @@ -30,14 +30,14 @@ NS_ASSUME_NONNULL_BEGIN * @result A BOOL indicating whether the device was successfully locked for configuration. */ - (BOOL)lockDevice:(NSObject *)captureDevice - error:(NSError *_Nullable *_Nullable)outError; + error:(NSError *_Nullable *_Nullable)outError NS_SWIFT_NAME(lockDevice(_:)); /** * @method unlockDevice: * @abstract Release exclusive control over device hardware properties. * @param captureDevice The capture device. */ -- (void)unlockDevice:(NSObject *)captureDevice; +- (void)unlockDevice:(NSObject *)captureDevice NS_SWIFT_NAME(unlockDevice(_:)) ; /** * @method beginConfigurationForSession: @@ -100,7 +100,7 @@ NS_ASSUME_NONNULL_BEGIN * @param writer The `AVAssetWriter` object. */ - (void)addInput:(NSObject *)writerInput - toAssetWriter:(NSObject *)writer; + toAssetWriter:(NSObject *)writer NS_SWIFT_NAME(addInput(_:to:)); /** * @method recommendedVideoSettingsForAssetWriterWithFileType:forOutput: diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTSavePhotoDelegate_Test.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTSavePhotoDelegate_Test.h index 7c242e53f58a..5d9767e00050 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTSavePhotoDelegate_Test.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTSavePhotoDelegate_Test.h @@ -22,5 +22,6 @@ /// @param error the capture error. /// @param photoDataProvider a closure that provides photo data. - (void)handlePhotoCaptureResultWithError:(NSError *)error - photoDataProvider:(NSObject * (^)(void))photoDataProvider; + photoDataProvider:(NSObject * (^)(void))photoDataProvider + NS_SWIFT_NAME(handlePhotoCaptureResult(error:photoDataProvider:)); @end diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 2dcc784d6316..0ec67eb487fd 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+7 +version: 0.9.18+8 environment: sdk: ^3.4.0