├── .gitignore ├── .idea ├── .gitignore ├── SwiftyCamera.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── .travis.yml ├── Example ├── Podfile ├── Podfile.lock ├── SwiftyCamera.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── SwiftyCamera-Example.xcscheme ├── SwiftyCamera │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── CMMotionManager+Ext.swift │ ├── DualViewController.swift │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── icon_filp.imageset │ │ │ ├── Camera flip@3x.png │ │ │ └── Contents.json │ │ ├── icon_movie_selector.imageset │ │ │ ├── Contents.json │ │ │ └── MovieSelector@2x.png │ │ ├── icon_photo.imageset │ │ │ ├── Contents.json │ │ │ └── Thumb nail@3x.png │ │ ├── icon_photo_selector.imageset │ │ │ ├── Contents.json │ │ │ └── PhotoSelector@2x.png │ │ ├── icon_shutter.imageset │ │ │ ├── Contents.json │ │ │ └── Shutter.png │ │ ├── icon_start_record.imageset │ │ │ ├── CaptureVideo@2x.png │ │ │ └── Contents.json │ │ └── icon_stop_record.imageset │ │ │ ├── CaptureStop@2x.png │ │ │ └── Contents.json │ ├── Info.plist │ ├── PreviewViewController.swift │ ├── SingleViewController.swift │ ├── VideoPlayerView.swift │ └── ViewController.swift └── Tests │ ├── Info.plist │ └── Tests.swift ├── LICENSE ├── README.md ├── Sources ├── SYBaseCamera.h ├── SYBaseCamera.m ├── SYCameraConfig.h ├── SYCameraConfig.m ├── SYCameraManager.h ├── SYCameraManager.m ├── SYLog.h ├── SYMultiCamera.h ├── SYMultiCamera.m ├── SYPreviewView.h ├── SYPreviewView.m ├── SYRecordConfig.h ├── SYRecordConfig.m ├── SYRecorder.h ├── SYRecorder.m ├── SYSingleCamera.h ├── SYSingleCamera.m ├── SwiftyCamera.h ├── UIImage+SYImage.h └── UIImage+SYImage.m ├── SwiftyCamera.podspec └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | Pods/ 40 | # 41 | # Add this line if you want to avoid checking in source code from the Xcode workspace 42 | *.xcworkspace 43 | .DS_Store 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | 64 | # Code Injection 65 | # 66 | # After new code Injection tools there's a generated folder /iOSInjectionProject 67 | # https://github.com/johnno1962/injectionforxcode 68 | 69 | iOSInjectionProject/ 70 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/SwiftyCamera.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftyCamera.xcworkspace -scheme SwiftyCamera-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '11.0' 4 | 5 | target 'SwiftyCamera_Example' do 6 | pod 'SwiftyCamera', :path => '../' 7 | pod 'SnapKit' 8 | 9 | target 'SwiftyCamera_Tests' do 10 | inherit! :search_paths 11 | 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SnapKit (5.6.0) 3 | - SwiftyCamera (1.0.0) 4 | 5 | DEPENDENCIES: 6 | - SnapKit 7 | - SwiftyCamera (from `../`) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - SnapKit 12 | 13 | EXTERNAL SOURCES: 14 | SwiftyCamera: 15 | :path: "../" 16 | 17 | SPEC CHECKSUMS: 18 | SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 19 | SwiftyCamera: dfb07826d51ec9c87296106bec3a519a8d80c642 20 | 21 | PODFILE CHECKSUM: d162fe19deb74b50fca67525c4573f4e7a8d7dd0 22 | 23 | COCOAPODS: 1.13.0 24 | -------------------------------------------------------------------------------- /Example/SwiftyCamera.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 13D5D87C10C110F56219E8B0 /* Pods_SwiftyCamera_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D7BB75DE39A9AF8A92CAA63 /* Pods_SwiftyCamera_Example.framework */; }; 11 | 1B37FE60BA37CBE66D883762 /* Pods_SwiftyCamera_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6622D29DEF113942DB5C378B /* Pods_SwiftyCamera_Tests.framework */; }; 12 | 420BD2962C0C0556003E5F92 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420BD2952C0C0556003E5F92 /* PreviewViewController.swift */; }; 13 | 420BD2982C0C05C2003E5F92 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 420BD2972C0C05C2003E5F92 /* VideoPlayerView.swift */; }; 14 | 4274F7102C33FD810015219B /* SingleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4274F70F2C33FD810015219B /* SingleViewController.swift */; }; 15 | 4274F7122C3400920015219B /* DualViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4274F7112C3400920015219B /* DualViewController.swift */; }; 16 | 42B876282C231A9700F8C49B /* CMMotionManager+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B876272C231A9700F8C49B /* CMMotionManager+Ext.swift */; }; 17 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 18 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 19 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 20 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 21 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 22 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 31 | remoteInfo = SwiftyCamera; 32 | }; 33 | /* End PBXContainerItemProxy section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 02B36889279707AC68E6CDAC /* Pods-SwiftyCamera_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyCamera_Example.debug.xcconfig"; path = "Target Support Files/Pods-SwiftyCamera_Example/Pods-SwiftyCamera_Example.debug.xcconfig"; sourceTree = ""; }; 37 | 2D0E79F5E795530B074B0544 /* Pods-SwiftyCamera_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyCamera_Tests.release.xcconfig"; path = "Target Support Files/Pods-SwiftyCamera_Tests/Pods-SwiftyCamera_Tests.release.xcconfig"; sourceTree = ""; }; 38 | 2D7BB75DE39A9AF8A92CAA63 /* Pods_SwiftyCamera_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftyCamera_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 2DC4BE471ECD0BA3560C0F3C /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 40 | 420BD2952C0C0556003E5F92 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; 41 | 420BD2972C0C05C2003E5F92 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; 42 | 4274F70F2C33FD810015219B /* SingleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleViewController.swift; sourceTree = ""; }; 43 | 4274F7112C3400920015219B /* DualViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DualViewController.swift; sourceTree = ""; }; 44 | 42B876272C231A9700F8C49B /* CMMotionManager+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMMotionManager+Ext.swift"; sourceTree = ""; }; 45 | 607FACD01AFB9204008FA782 /* SwiftyCamera_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyCamera_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 48 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 49 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 51 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 52 | 607FACE51AFB9204008FA782 /* SwiftyCamera_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyCamera_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; 55 | 6622D29DEF113942DB5C378B /* Pods_SwiftyCamera_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftyCamera_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | A2DE8153D8943235FBB17BB2 /* SwiftyCamera.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = SwiftyCamera.podspec; path = ../SwiftyCamera.podspec; sourceTree = ""; }; 57 | AD1CB7530E1250BA3EB8602A /* Pods-SwiftyCamera_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyCamera_Tests.debug.xcconfig"; path = "Target Support Files/Pods-SwiftyCamera_Tests/Pods-SwiftyCamera_Tests.debug.xcconfig"; sourceTree = ""; }; 58 | B67BD09A6BEF4D5EAD4918A2 /* Pods-SwiftyCamera_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftyCamera_Example.release.xcconfig"; path = "Target Support Files/Pods-SwiftyCamera_Example/Pods-SwiftyCamera_Example.release.xcconfig"; sourceTree = ""; }; 59 | BACEF88CC22B8BDBBE4E817A /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | 13D5D87C10C110F56219E8B0 /* Pods_SwiftyCamera_Example.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | 1B37FE60BA37CBE66D883762 /* Pods_SwiftyCamera_Tests.framework in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | 0349316081E901447D2455C8 /* Frameworks */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 2D7BB75DE39A9AF8A92CAA63 /* Pods_SwiftyCamera_Example.framework */, 86 | 6622D29DEF113942DB5C378B /* Pods_SwiftyCamera_Tests.framework */, 87 | ); 88 | name = Frameworks; 89 | sourceTree = ""; 90 | }; 91 | 607FACC71AFB9204008FA782 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 95 | 607FACD21AFB9204008FA782 /* Example for SwiftyCamera */, 96 | 607FACE81AFB9204008FA782 /* Tests */, 97 | 607FACD11AFB9204008FA782 /* Products */, 98 | 6BF777486E56F3062B799D5C /* Pods */, 99 | 0349316081E901447D2455C8 /* Frameworks */, 100 | ); 101 | sourceTree = ""; 102 | }; 103 | 607FACD11AFB9204008FA782 /* Products */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | 607FACD01AFB9204008FA782 /* SwiftyCamera_Example.app */, 107 | 607FACE51AFB9204008FA782 /* SwiftyCamera_Tests.xctest */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | 607FACD21AFB9204008FA782 /* Example for SwiftyCamera */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 420BD2972C0C05C2003E5F92 /* VideoPlayerView.swift */, 116 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 117 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 118 | 4274F70F2C33FD810015219B /* SingleViewController.swift */, 119 | 4274F7112C3400920015219B /* DualViewController.swift */, 120 | 420BD2952C0C0556003E5F92 /* PreviewViewController.swift */, 121 | 42B876272C231A9700F8C49B /* CMMotionManager+Ext.swift */, 122 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 123 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 124 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 125 | 607FACD31AFB9204008FA782 /* Supporting Files */, 126 | ); 127 | name = "Example for SwiftyCamera"; 128 | path = SwiftyCamera; 129 | sourceTree = ""; 130 | }; 131 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 607FACD41AFB9204008FA782 /* Info.plist */, 135 | ); 136 | name = "Supporting Files"; 137 | sourceTree = ""; 138 | }; 139 | 607FACE81AFB9204008FA782 /* Tests */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 607FACEB1AFB9204008FA782 /* Tests.swift */, 143 | 607FACE91AFB9204008FA782 /* Supporting Files */, 144 | ); 145 | path = Tests; 146 | sourceTree = ""; 147 | }; 148 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 607FACEA1AFB9204008FA782 /* Info.plist */, 152 | ); 153 | name = "Supporting Files"; 154 | sourceTree = ""; 155 | }; 156 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | A2DE8153D8943235FBB17BB2 /* SwiftyCamera.podspec */, 160 | 2DC4BE471ECD0BA3560C0F3C /* README.md */, 161 | BACEF88CC22B8BDBBE4E817A /* LICENSE */, 162 | ); 163 | name = "Podspec Metadata"; 164 | sourceTree = ""; 165 | }; 166 | 6BF777486E56F3062B799D5C /* Pods */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 02B36889279707AC68E6CDAC /* Pods-SwiftyCamera_Example.debug.xcconfig */, 170 | B67BD09A6BEF4D5EAD4918A2 /* Pods-SwiftyCamera_Example.release.xcconfig */, 171 | AD1CB7530E1250BA3EB8602A /* Pods-SwiftyCamera_Tests.debug.xcconfig */, 172 | 2D0E79F5E795530B074B0544 /* Pods-SwiftyCamera_Tests.release.xcconfig */, 173 | ); 174 | path = Pods; 175 | sourceTree = ""; 176 | }; 177 | /* End PBXGroup section */ 178 | 179 | /* Begin PBXNativeTarget section */ 180 | 607FACCF1AFB9204008FA782 /* SwiftyCamera_Example */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyCamera_Example" */; 183 | buildPhases = ( 184 | B0C713F024327165B170712C /* [CP] Check Pods Manifest.lock */, 185 | 607FACCC1AFB9204008FA782 /* Sources */, 186 | 607FACCD1AFB9204008FA782 /* Frameworks */, 187 | 607FACCE1AFB9204008FA782 /* Resources */, 188 | F60469398AC67AC0C4A54AB9 /* [CP] Embed Pods Frameworks */, 189 | ); 190 | buildRules = ( 191 | ); 192 | dependencies = ( 193 | ); 194 | name = SwiftyCamera_Example; 195 | productName = SwiftyCamera; 196 | productReference = 607FACD01AFB9204008FA782 /* SwiftyCamera_Example.app */; 197 | productType = "com.apple.product-type.application"; 198 | }; 199 | 607FACE41AFB9204008FA782 /* SwiftyCamera_Tests */ = { 200 | isa = PBXNativeTarget; 201 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyCamera_Tests" */; 202 | buildPhases = ( 203 | 72E4BEA1BD35DC00B562C5E5 /* [CP] Check Pods Manifest.lock */, 204 | 607FACE11AFB9204008FA782 /* Sources */, 205 | 607FACE21AFB9204008FA782 /* Frameworks */, 206 | 607FACE31AFB9204008FA782 /* Resources */, 207 | ); 208 | buildRules = ( 209 | ); 210 | dependencies = ( 211 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 212 | ); 213 | name = SwiftyCamera_Tests; 214 | productName = Tests; 215 | productReference = 607FACE51AFB9204008FA782 /* SwiftyCamera_Tests.xctest */; 216 | productType = "com.apple.product-type.bundle.unit-test"; 217 | }; 218 | /* End PBXNativeTarget section */ 219 | 220 | /* Begin PBXProject section */ 221 | 607FACC81AFB9204008FA782 /* Project object */ = { 222 | isa = PBXProject; 223 | attributes = { 224 | LastSwiftUpdateCheck = 0830; 225 | LastUpgradeCheck = 0830; 226 | ORGANIZATIONNAME = CocoaPods; 227 | TargetAttributes = { 228 | 607FACCF1AFB9204008FA782 = { 229 | CreatedOnToolsVersion = 6.3.1; 230 | DevelopmentTeam = 9HR3SDCW25; 231 | LastSwiftMigration = 0900; 232 | }; 233 | 607FACE41AFB9204008FA782 = { 234 | CreatedOnToolsVersion = 6.3.1; 235 | DevelopmentTeam = 9HR3SDCW25; 236 | LastSwiftMigration = 0900; 237 | TestTargetID = 607FACCF1AFB9204008FA782; 238 | }; 239 | }; 240 | }; 241 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftyCamera" */; 242 | compatibilityVersion = "Xcode 3.2"; 243 | developmentRegion = English; 244 | hasScannedForEncodings = 0; 245 | knownRegions = ( 246 | English, 247 | en, 248 | Base, 249 | ); 250 | mainGroup = 607FACC71AFB9204008FA782; 251 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 252 | projectDirPath = ""; 253 | projectRoot = ""; 254 | targets = ( 255 | 607FACCF1AFB9204008FA782 /* SwiftyCamera_Example */, 256 | 607FACE41AFB9204008FA782 /* SwiftyCamera_Tests */, 257 | ); 258 | }; 259 | /* End PBXProject section */ 260 | 261 | /* Begin PBXResourcesBuildPhase section */ 262 | 607FACCE1AFB9204008FA782 /* Resources */ = { 263 | isa = PBXResourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 267 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 268 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | 607FACE31AFB9204008FA782 /* Resources */ = { 273 | isa = PBXResourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | /* End PBXResourcesBuildPhase section */ 280 | 281 | /* Begin PBXShellScriptBuildPhase section */ 282 | 72E4BEA1BD35DC00B562C5E5 /* [CP] Check Pods Manifest.lock */ = { 283 | isa = PBXShellScriptBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | ); 287 | inputFileListPaths = ( 288 | ); 289 | inputPaths = ( 290 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 291 | "${PODS_ROOT}/Manifest.lock", 292 | ); 293 | name = "[CP] Check Pods Manifest.lock"; 294 | outputFileListPaths = ( 295 | ); 296 | outputPaths = ( 297 | "$(DERIVED_FILE_DIR)/Pods-SwiftyCamera_Tests-checkManifestLockResult.txt", 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | shellPath = /bin/sh; 301 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 302 | showEnvVarsInLog = 0; 303 | }; 304 | B0C713F024327165B170712C /* [CP] Check Pods Manifest.lock */ = { 305 | isa = PBXShellScriptBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | ); 309 | inputFileListPaths = ( 310 | ); 311 | inputPaths = ( 312 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 313 | "${PODS_ROOT}/Manifest.lock", 314 | ); 315 | name = "[CP] Check Pods Manifest.lock"; 316 | outputFileListPaths = ( 317 | ); 318 | outputPaths = ( 319 | "$(DERIVED_FILE_DIR)/Pods-SwiftyCamera_Example-checkManifestLockResult.txt", 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | shellPath = /bin/sh; 323 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 324 | showEnvVarsInLog = 0; 325 | }; 326 | F60469398AC67AC0C4A54AB9 /* [CP] Embed Pods Frameworks */ = { 327 | isa = PBXShellScriptBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | ); 331 | inputPaths = ( 332 | "${PODS_ROOT}/Target Support Files/Pods-SwiftyCamera_Example/Pods-SwiftyCamera_Example-frameworks.sh", 333 | "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", 334 | "${BUILT_PRODUCTS_DIR}/SwiftyCamera/SwiftyCamera.framework", 335 | ); 336 | name = "[CP] Embed Pods Frameworks"; 337 | outputPaths = ( 338 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", 339 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyCamera.framework", 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | shellPath = /bin/sh; 343 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftyCamera_Example/Pods-SwiftyCamera_Example-frameworks.sh\"\n"; 344 | showEnvVarsInLog = 0; 345 | }; 346 | /* End PBXShellScriptBuildPhase section */ 347 | 348 | /* Begin PBXSourcesBuildPhase section */ 349 | 607FACCC1AFB9204008FA782 /* Sources */ = { 350 | isa = PBXSourcesBuildPhase; 351 | buildActionMask = 2147483647; 352 | files = ( 353 | 4274F7102C33FD810015219B /* SingleViewController.swift in Sources */, 354 | 4274F7122C3400920015219B /* DualViewController.swift in Sources */, 355 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 356 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 357 | 420BD2962C0C0556003E5F92 /* PreviewViewController.swift in Sources */, 358 | 420BD2982C0C05C2003E5F92 /* VideoPlayerView.swift in Sources */, 359 | 42B876282C231A9700F8C49B /* CMMotionManager+Ext.swift in Sources */, 360 | ); 361 | runOnlyForDeploymentPostprocessing = 0; 362 | }; 363 | 607FACE11AFB9204008FA782 /* Sources */ = { 364 | isa = PBXSourcesBuildPhase; 365 | buildActionMask = 2147483647; 366 | files = ( 367 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, 368 | ); 369 | runOnlyForDeploymentPostprocessing = 0; 370 | }; 371 | /* End PBXSourcesBuildPhase section */ 372 | 373 | /* Begin PBXTargetDependency section */ 374 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 375 | isa = PBXTargetDependency; 376 | target = 607FACCF1AFB9204008FA782 /* SwiftyCamera_Example */; 377 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 378 | }; 379 | /* End PBXTargetDependency section */ 380 | 381 | /* Begin PBXVariantGroup section */ 382 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 383 | isa = PBXVariantGroup; 384 | children = ( 385 | 607FACDA1AFB9204008FA782 /* Base */, 386 | ); 387 | name = Main.storyboard; 388 | sourceTree = ""; 389 | }; 390 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 391 | isa = PBXVariantGroup; 392 | children = ( 393 | 607FACDF1AFB9204008FA782 /* Base */, 394 | ); 395 | name = LaunchScreen.xib; 396 | sourceTree = ""; 397 | }; 398 | /* End PBXVariantGroup section */ 399 | 400 | /* Begin XCBuildConfiguration section */ 401 | 607FACED1AFB9204008FA782 /* Debug */ = { 402 | isa = XCBuildConfiguration; 403 | buildSettings = { 404 | ALWAYS_SEARCH_USER_PATHS = NO; 405 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 406 | CLANG_CXX_LIBRARY = "libc++"; 407 | CLANG_ENABLE_MODULES = YES; 408 | CLANG_ENABLE_OBJC_ARC = YES; 409 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 410 | CLANG_WARN_BOOL_CONVERSION = YES; 411 | CLANG_WARN_COMMA = YES; 412 | CLANG_WARN_CONSTANT_CONVERSION = YES; 413 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 414 | CLANG_WARN_EMPTY_BODY = YES; 415 | CLANG_WARN_ENUM_CONVERSION = YES; 416 | CLANG_WARN_INFINITE_RECURSION = YES; 417 | CLANG_WARN_INT_CONVERSION = YES; 418 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 419 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 420 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 421 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 422 | CLANG_WARN_STRICT_PROTOTYPES = YES; 423 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 424 | CLANG_WARN_UNREACHABLE_CODE = YES; 425 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 426 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 427 | COPY_PHASE_STRIP = NO; 428 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 429 | ENABLE_STRICT_OBJC_MSGSEND = YES; 430 | ENABLE_TESTABILITY = YES; 431 | GCC_C_LANGUAGE_STANDARD = gnu99; 432 | GCC_DYNAMIC_NO_PIC = NO; 433 | GCC_NO_COMMON_BLOCKS = YES; 434 | GCC_OPTIMIZATION_LEVEL = 0; 435 | GCC_PREPROCESSOR_DEFINITIONS = ( 436 | "DEBUG=1", 437 | "$(inherited)", 438 | ); 439 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_UNDECLARED_SELECTOR = YES; 443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 444 | GCC_WARN_UNUSED_FUNCTION = YES; 445 | GCC_WARN_UNUSED_VARIABLE = YES; 446 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 447 | MTL_ENABLE_DEBUG_INFO = YES; 448 | ONLY_ACTIVE_ARCH = YES; 449 | SDKROOT = iphoneos; 450 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 451 | }; 452 | name = Debug; 453 | }; 454 | 607FACEE1AFB9204008FA782 /* Release */ = { 455 | isa = XCBuildConfiguration; 456 | buildSettings = { 457 | ALWAYS_SEARCH_USER_PATHS = NO; 458 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 459 | CLANG_CXX_LIBRARY = "libc++"; 460 | CLANG_ENABLE_MODULES = YES; 461 | CLANG_ENABLE_OBJC_ARC = YES; 462 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 463 | CLANG_WARN_BOOL_CONVERSION = YES; 464 | CLANG_WARN_COMMA = YES; 465 | CLANG_WARN_CONSTANT_CONVERSION = YES; 466 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 467 | CLANG_WARN_EMPTY_BODY = YES; 468 | CLANG_WARN_ENUM_CONVERSION = YES; 469 | CLANG_WARN_INFINITE_RECURSION = YES; 470 | CLANG_WARN_INT_CONVERSION = YES; 471 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 472 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 474 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 475 | CLANG_WARN_STRICT_PROTOTYPES = YES; 476 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 477 | CLANG_WARN_UNREACHABLE_CODE = YES; 478 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 479 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 480 | COPY_PHASE_STRIP = NO; 481 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 482 | ENABLE_NS_ASSERTIONS = NO; 483 | ENABLE_STRICT_OBJC_MSGSEND = YES; 484 | GCC_C_LANGUAGE_STANDARD = gnu99; 485 | GCC_NO_COMMON_BLOCKS = YES; 486 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 487 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 488 | GCC_WARN_UNDECLARED_SELECTOR = YES; 489 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 490 | GCC_WARN_UNUSED_FUNCTION = YES; 491 | GCC_WARN_UNUSED_VARIABLE = YES; 492 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 493 | MTL_ENABLE_DEBUG_INFO = NO; 494 | SDKROOT = iphoneos; 495 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 496 | VALIDATE_PRODUCT = YES; 497 | }; 498 | name = Release; 499 | }; 500 | 607FACF01AFB9204008FA782 /* Debug */ = { 501 | isa = XCBuildConfiguration; 502 | baseConfigurationReference = 02B36889279707AC68E6CDAC /* Pods-SwiftyCamera_Example.debug.xcconfig */; 503 | buildSettings = { 504 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 505 | DEVELOPMENT_TEAM = 9HR3SDCW25; 506 | INFOPLIST_FILE = SwiftyCamera/Info.plist; 507 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 508 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 509 | MODULE_NAME = ExampleApp; 510 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 513 | SWIFT_VERSION = 4.0; 514 | }; 515 | name = Debug; 516 | }; 517 | 607FACF11AFB9204008FA782 /* Release */ = { 518 | isa = XCBuildConfiguration; 519 | baseConfigurationReference = B67BD09A6BEF4D5EAD4918A2 /* Pods-SwiftyCamera_Example.release.xcconfig */; 520 | buildSettings = { 521 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 522 | DEVELOPMENT_TEAM = 9HR3SDCW25; 523 | INFOPLIST_FILE = SwiftyCamera/Info.plist; 524 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 525 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 526 | MODULE_NAME = ExampleApp; 527 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 528 | PRODUCT_NAME = "$(TARGET_NAME)"; 529 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 530 | SWIFT_VERSION = 4.0; 531 | }; 532 | name = Release; 533 | }; 534 | 607FACF31AFB9204008FA782 /* Debug */ = { 535 | isa = XCBuildConfiguration; 536 | baseConfigurationReference = AD1CB7530E1250BA3EB8602A /* Pods-SwiftyCamera_Tests.debug.xcconfig */; 537 | buildSettings = { 538 | DEVELOPMENT_TEAM = 9HR3SDCW25; 539 | FRAMEWORK_SEARCH_PATHS = ( 540 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 541 | "$(inherited)", 542 | ); 543 | GCC_PREPROCESSOR_DEFINITIONS = ( 544 | "DEBUG=1", 545 | "$(inherited)", 546 | ); 547 | INFOPLIST_FILE = Tests/Info.plist; 548 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 549 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 550 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 553 | SWIFT_VERSION = 4.0; 554 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyCamera_Example.app/SwiftyCamera_Example"; 555 | }; 556 | name = Debug; 557 | }; 558 | 607FACF41AFB9204008FA782 /* Release */ = { 559 | isa = XCBuildConfiguration; 560 | baseConfigurationReference = 2D0E79F5E795530B074B0544 /* Pods-SwiftyCamera_Tests.release.xcconfig */; 561 | buildSettings = { 562 | DEVELOPMENT_TEAM = 9HR3SDCW25; 563 | FRAMEWORK_SEARCH_PATHS = ( 564 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 565 | "$(inherited)", 566 | ); 567 | INFOPLIST_FILE = Tests/Info.plist; 568 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 569 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 570 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 571 | PRODUCT_NAME = "$(TARGET_NAME)"; 572 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 573 | SWIFT_VERSION = 4.0; 574 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyCamera_Example.app/SwiftyCamera_Example"; 575 | }; 576 | name = Release; 577 | }; 578 | /* End XCBuildConfiguration section */ 579 | 580 | /* Begin XCConfigurationList section */ 581 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftyCamera" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | 607FACED1AFB9204008FA782 /* Debug */, 585 | 607FACEE1AFB9204008FA782 /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyCamera_Example" */ = { 591 | isa = XCConfigurationList; 592 | buildConfigurations = ( 593 | 607FACF01AFB9204008FA782 /* Debug */, 594 | 607FACF11AFB9204008FA782 /* Release */, 595 | ); 596 | defaultConfigurationIsVisible = 0; 597 | defaultConfigurationName = Release; 598 | }; 599 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "SwiftyCamera_Tests" */ = { 600 | isa = XCConfigurationList; 601 | buildConfigurations = ( 602 | 607FACF31AFB9204008FA782 /* Debug */, 603 | 607FACF41AFB9204008FA782 /* Release */, 604 | ); 605 | defaultConfigurationIsVisible = 0; 606 | defaultConfigurationName = Release; 607 | }; 608 | /* End XCConfigurationList section */ 609 | }; 610 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 611 | } 612 | -------------------------------------------------------------------------------- /Example/SwiftyCamera.xcodeproj/xcshareddata/xcschemes/SwiftyCamera-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftyCamera 4 | // 5 | // Created by chenshuangma@foxmail.com on 05/25/2024. 6 | // Copyright (c) 2024 chenshuangma@foxmail.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/CMMotionManager+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CMMotionManager+Ext.swift 3 | // SwiftyCamera_Example 4 | // 5 | // Created by 马陈爽 on 2024/6/19. 6 | // Copyright © 2024 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreMotion 11 | import UIKit 12 | 13 | extension CMMotionManager { 14 | func motionStart(update:((_ orientation: UIDeviceOrientation) -> Void)?) { 15 | if !self.isAccelerometerAvailable { 16 | return 17 | } 18 | self.accelerometerUpdateInterval = 0.3 19 | self.startAccelerometerUpdates(to: OperationQueue.main) { (accelerometerData, error) in 20 | if let error = error { 21 | return 22 | } 23 | guard let acceleration = accelerometerData?.acceleration else { return } 24 | guard let completion = update else { return } 25 | var orientation: UIDeviceOrientation = .portrait 26 | if acceleration.y <= -0.75 { 27 | orientation = .portrait 28 | } else if acceleration.y >= 0.75 { 29 | orientation = .portraitUpsideDown 30 | } else if acceleration.x <= -0.75 { 31 | orientation = .landscapeLeft 32 | } else if acceleration.x >= 0.75 { 33 | orientation = .landscapeRight 34 | } else { 35 | orientation = .portrait 36 | } 37 | completion(orientation) 38 | } 39 | } 40 | 41 | func motionStop() { 42 | if self.isAccelerometerAvailable && self.isAccelerometerActive { 43 | self.stopAccelerometerUpdates() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/DualViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DualViewController.swift 3 | // SwiftyCamera_Example 4 | // 5 | // Created by 马陈爽 on 2024/7/2. 6 | // Copyright © 2024 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyCamera 11 | import SnapKit 12 | import CoreMotion 13 | 14 | class DualViewController: UIViewController { 15 | 16 | private var cameraManager: SYCameraManager! 17 | private var previewView: UIView! 18 | private var shutterBtn: UIButton! 19 | private var filpBtn: UIButton! 20 | private var albumBtn: UIButton! 21 | private var recordBtn: UIButton! 22 | private var cameraModeControl: UISegmentedControl! 23 | private var currentZoom: CGFloat = 0.0 24 | 25 | private var recordMode: SYRecordStatus = .recordNormal { 26 | didSet { 27 | refreshRecordUI() 28 | } 29 | } 30 | private var cameraMode: SYCameraMode = .photoMode { 31 | didSet { 32 | refreshCameraModeUI() 33 | } 34 | } 35 | 36 | private lazy var motion: CMMotionManager = { 37 | return CMMotionManager() 38 | }() 39 | 40 | private var isBacking: Bool = true 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | view.backgroundColor = UIColor.black 45 | setup() 46 | cameraMode = .photoMode 47 | } 48 | 49 | override func viewDidAppear(_ animated: Bool) { 50 | super.viewDidAppear(animated) 51 | if cameraManager.result == .success { 52 | cameraManager.startCapture() 53 | motion.motionStart { [weak self](orientation) in 54 | guard let `self` = self else { 55 | return 56 | } 57 | } 58 | } 59 | } 60 | 61 | override func viewWillDisappear(_ animated: Bool) { 62 | super.viewWillDisappear(animated) 63 | if cameraManager.result == .success { 64 | cameraManager.stopCapture() 65 | motion.motionStop() 66 | } 67 | } 68 | 69 | private func setup() { 70 | 71 | previewView = UIView(frame: .zero) 72 | previewView.backgroundColor = UIColor.white 73 | view.addSubview(previewView) 74 | let tapRecognzer = UITapGestureRecognizer(target: self, action: #selector(handleTapEvent(_:))) 75 | previewView.addGestureRecognizer(tapRecognzer) 76 | let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchEvent(_:))) 77 | previewView.addGestureRecognizer(pinchRecognizer) 78 | 79 | previewView.snp.makeConstraints { 80 | $0.width.equalToSuperview() 81 | $0.height.equalTo(661) 82 | $0.center.equalToSuperview() 83 | } 84 | 85 | let backView = UIView(frame: .zero) 86 | backView.backgroundColor = UIColor(red: 0.121, green: 0.121, blue: 0.121, alpha: 0.57) 87 | previewView.addSubview(backView) 88 | 89 | backView.snp.makeConstraints { 90 | $0.edges.equalToSuperview() 91 | } 92 | 93 | shutterBtn = UIButton(type: .custom) 94 | shutterBtn.setImage(UIImage(named: "icon_shutter"), for: .normal) 95 | shutterBtn.addTarget(self, action: #selector(takePhoto), for: .touchUpInside) 96 | view.addSubview(shutterBtn) 97 | 98 | shutterBtn.snp.makeConstraints { 99 | $0.width.height.equalTo(72) 100 | $0.centerX.equalToSuperview() 101 | $0.bottom.equalTo(previewView.snp.bottom).offset(-20) 102 | } 103 | 104 | filpBtn = UIButton(frame: .zero) 105 | filpBtn.setImage(UIImage(named: "icon_filp"), for: .normal) 106 | filpBtn.addTarget(self, action: #selector(cameraFilp), for: .touchUpInside) 107 | view.addSubview(filpBtn) 108 | filpBtn.snp.makeConstraints { 109 | $0.width.height.equalTo(48) 110 | $0.trailing.equalToSuperview().offset(-16) 111 | $0.centerY.equalTo(shutterBtn) 112 | } 113 | 114 | cameraModeControl = UISegmentedControl(items: [UIImage(named: "icon_photo_selector")!, UIImage(named: "icon_movie_selector")!]) 115 | cameraModeControl.selectedSegmentIndex = 0 116 | cameraModeControl.addTarget(self, action: #selector(changeCameraMode(_:)), for: .valueChanged) 117 | view.addSubview(cameraModeControl) 118 | cameraModeControl.snp.makeConstraints { 119 | $0.bottom.equalTo(shutterBtn.snp.top).offset(-20) 120 | $0.centerX.equalToSuperview() 121 | $0.size.equalTo(CGSize(width: 80, height: 40)) 122 | } 123 | 124 | recordBtn = UIButton(type: .custom) 125 | recordBtn.setImage(UIImage(named: "icon_start_record"), for: .normal) 126 | recordBtn.addTarget(self, action: #selector(handleRecordEvent(_:)), for: .touchUpInside) 127 | view.addSubview(recordBtn) 128 | recordBtn.snp.makeConstraints { 129 | $0.edges.equalTo(shutterBtn) 130 | } 131 | 132 | cameraManager = SYCameraManager() 133 | self.requestVideoPermission { [weak self](ret) in 134 | guard let `self` = self else { 135 | return 136 | } 137 | guard ret else { return } 138 | self.requestAudioPermission { [weak self](ret) in 139 | guard let `self` = self else { 140 | return 141 | } 142 | guard ret else { return } 143 | self.setupCamera() 144 | } 145 | } 146 | } 147 | 148 | private func requestVideoPermission(completion: @escaping (Bool)->Void) { 149 | let status = AVCaptureDevice.authorizationStatus(for: .video) 150 | switch status { 151 | case .notDetermined: 152 | AVCaptureDevice.requestAccess(for: .video) { (ret) in 153 | DispatchQueue.main.async { 154 | completion(ret) 155 | } 156 | 157 | } 158 | case .authorized: 159 | completion(true) 160 | default: 161 | completion(false) 162 | 163 | } 164 | } 165 | 166 | private func requestAudioPermission(completion: @escaping (Bool)->Void) { 167 | let status = AVCaptureDevice.authorizationStatus(for: .audio) 168 | switch status { 169 | case .notDetermined: 170 | AVCaptureDevice.requestAccess(for: .audio) { (ret) in 171 | DispatchQueue.main.async { 172 | completion(ret) 173 | } 174 | 175 | } 176 | case .authorized: 177 | completion(true) 178 | default: 179 | completion(false) 180 | 181 | } 182 | } 183 | 184 | private func setupCamera() { 185 | let config = SYCameraConfig() 186 | config.mode = cameraMode 187 | config.type = .dualDevice 188 | 189 | cameraManager.requestCamera(with: config) { [weak self](ret) in 190 | guard let `self` = self else { return } 191 | if ret == .success { 192 | self.cameraManager.delegate = self 193 | self.cameraManager.addPreview(to: self.previewView) 194 | self.cameraManager.startCapture() 195 | } 196 | } 197 | } 198 | 199 | private func refreshRecordUI() { 200 | if recordMode == .recordNormal { 201 | recordBtn.setImage(UIImage(named: "icon_start_record"), for: .normal) 202 | filpBtn.isHidden = false 203 | } else { 204 | recordBtn.setImage(UIImage(named: "icon_stop_record"), for: .normal) 205 | filpBtn.isHidden = true 206 | } 207 | } 208 | 209 | private func refreshCameraModeUI() { 210 | if cameraMode == .photoMode { 211 | recordBtn.isHidden = true 212 | shutterBtn.isHidden = false 213 | cameraModeControl.selectedSegmentIndex = 0 214 | } else if cameraMode == .videoMode { 215 | recordBtn.isHidden = false 216 | shutterBtn.isHidden = true 217 | cameraModeControl.selectedSegmentIndex = 1 218 | } 219 | 220 | } 221 | 222 | @objc private func takePhoto() { 223 | if cameraManager.result != .success { 224 | return 225 | } 226 | cameraManager.takePhoto() 227 | } 228 | 229 | @objc private func cameraFilp() { 230 | 231 | } 232 | 233 | @objc private func changeCameraMode(_ control: UISegmentedControl) { 234 | 235 | } 236 | 237 | @objc private func handleRecordEvent(_ button: UIButton) { 238 | 239 | } 240 | 241 | @objc private func handleTapEvent(_ sender: UITapGestureRecognizer) { 242 | 243 | } 244 | 245 | @objc private func handlePinchEvent(_ sender: UIPinchGestureRecognizer) { 246 | 247 | } 248 | 249 | } 250 | 251 | extension DualViewController: SYCameraManagerDelegate { 252 | func cameraSessionSetupResult(_ result: SYSessionSetupResult, with manager: SYCameraManager) { 253 | 254 | } 255 | 256 | func cameraDidStarted(_ manager: SYCameraManager) { 257 | 258 | } 259 | 260 | func cameraDidStoped(_ manager: SYCameraManager) { 261 | 262 | } 263 | 264 | func cameraDidFinishProcessingVideo(_ outputURL: URL?, with manager: SYCameraManager, withError error: Error?) { 265 | DispatchQueue.main.async { [weak self] in 266 | guard let `self` = self else { return } 267 | guard let outputURL = outputURL else { 268 | return 269 | } 270 | 271 | PreviewViewController.show(with: ["videoUrl": outputURL], from: self) 272 | } 273 | } 274 | 275 | func cameraDidFinishProcessingPhoto(_ image: UIImage?, withMetaData metaData: [AnyHashable : Any]?, with manager: SYCameraManager, withError error: Error?) { 276 | DispatchQueue.main.async { [weak self] in 277 | guard let `self` = self else { return } 278 | guard let image = image else { 279 | return 280 | } 281 | 282 | PreviewViewController.show(with: ["image": image], from: self) 283 | } 284 | } 285 | 286 | 287 | func cameraDidChange(_ mode: SYCameraMode, with manager: SYCameraManager) { 288 | DispatchQueue.main.async { 289 | self.cameraMode = mode 290 | } 291 | 292 | } 293 | 294 | func cameraRecordStatusDidChange(_ status: SYRecordStatus, with manager: SYCameraManager) { 295 | DispatchQueue.main.async { 296 | self.recordMode = status; 297 | } 298 | } 299 | 300 | func cameraDidChangedFocus(_ value: CGPoint, mode: AVCaptureDevice.FocusMode, with manager: SYCameraManager) { 301 | print("ViewController cameraDidChangedFocus value = \(value), mode = \(mode)") 302 | } 303 | 304 | func cameraDidChangedExposure(_ value: CGPoint, mode: AVCaptureDevice.ExposureMode, with manager: SYCameraManager) { 305 | print("ViewController cameraDidChangedExposure value = \(value), mode = \(mode)") 306 | } 307 | 308 | func cameraDidChangedZoom(_ value: CGFloat, with manager: SYCameraManager) { 309 | print("ViewController cameraDidChangedZoom value = \(value)") 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_filp.imageset/Camera flip@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_filp.imageset/Camera flip@3x.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_filp.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "Camera flip@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_movie_selector.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "MovieSelector@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_movie_selector.imageset/MovieSelector@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_movie_selector.imageset/MovieSelector@2x.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_photo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "Thumb nail@3x.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_photo.imageset/Thumb nail@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_photo.imageset/Thumb nail@3x.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_photo_selector.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "PhotoSelector@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_photo_selector.imageset/PhotoSelector@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_photo_selector.imageset/PhotoSelector@2x.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_shutter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "Shutter.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_shutter.imageset/Shutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_shutter.imageset/Shutter.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_start_record.imageset/CaptureVideo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_start_record.imageset/CaptureVideo@2x.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_start_record.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "CaptureVideo@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_stop_record.imageset/CaptureStop@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/machenshuang/SwiftyCamera/b48eb640b73049b0c8df2f1eff789c128fea20cf/Example/SwiftyCamera/Images.xcassets/icon_stop_record.imageset/CaptureStop@2x.png -------------------------------------------------------------------------------- /Example/SwiftyCamera/Images.xcassets/icon_stop_record.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "CaptureStop@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | This app requires access to your camera to take photos and videos. 27 | NSMicrophoneUsageDescription 28 | This app requires access to your camera to take photos and videos. 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationPortrait 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/PreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewViewViewController.swift 3 | // SwiftyCamera_Example 4 | // 5 | // Created by 马陈爽 on 2024/6/2. 6 | // Copyright © 2024 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SnapKit 11 | 12 | class PreviewViewController: UIViewController { 13 | 14 | private lazy var imageView: UIImageView = { 15 | return UIImageView(frame: .zero) 16 | }() 17 | 18 | private lazy var videoView: VideoPlayerView = { 19 | return VideoPlayerView(withURL: nil, frame: .zero) 20 | }() 21 | 22 | var image: UIImage? 23 | var videoUrl: URL? 24 | 25 | override func viewDidLoad() { 26 | self.view.backgroundColor = .white 27 | super.viewDidLoad() 28 | if let image = image { 29 | view.addSubview(imageView) 30 | imageView.image = image 31 | imageView.contentMode = .scaleAspectFit 32 | imageView.snp.makeConstraints { 33 | $0.edges.equalToSuperview() 34 | } 35 | } else if let videoUrl = videoUrl { 36 | view.addSubview(videoView) 37 | videoView.updateUrl(url: videoUrl) 38 | videoView.snp.makeConstraints { 39 | $0.edges.equalToSuperview() 40 | } 41 | videoView.play() 42 | } 43 | } 44 | 45 | static func show(with params: [String: Any], from vc: UIViewController) { 46 | let target = PreviewViewController() 47 | target.image = params["image"] as? UIImage 48 | target.videoUrl = params["videoUrl"] as? URL 49 | vc.navigationController?.pushViewController(target, animated: true) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/SingleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleViewController.swift 3 | // SwiftyCamera_Example 4 | // 5 | // Created by 马陈爽 on 2024/7/2. 6 | // Copyright © 2024 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftyCamera 11 | import SnapKit 12 | import CoreMotion 13 | 14 | class SingleViewController: UIViewController { 15 | 16 | private var cameraManager: SYCameraManager! 17 | private var previewView: UIView! 18 | private var shutterBtn: UIButton! 19 | private var filpBtn: UIButton! 20 | private var albumBtn: UIButton! 21 | private var recordBtn: UIButton! 22 | private var cameraModeControl: UISegmentedControl! 23 | private var currentZoom: CGFloat = 0.0 24 | 25 | private var recordMode: SYRecordStatus = .recordNormal { 26 | didSet { 27 | refreshRecordUI() 28 | } 29 | } 30 | private var cameraMode: SYCameraMode = .photoMode { 31 | didSet { 32 | refreshCameraModeUI() 33 | } 34 | } 35 | 36 | private lazy var motion: CMMotionManager = { 37 | return CMMotionManager() 38 | }() 39 | 40 | private var isBacking: Bool = true 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | view.backgroundColor = UIColor.black 45 | setup() 46 | cameraMode = .photoMode 47 | } 48 | 49 | override func viewDidAppear(_ animated: Bool) { 50 | super.viewDidAppear(animated) 51 | if cameraManager.result == .success { 52 | cameraManager.startCapture() 53 | motion.motionStart { [weak self](orientation) in 54 | guard let `self` = self else { 55 | return 56 | } 57 | } 58 | } 59 | } 60 | 61 | override func viewWillDisappear(_ animated: Bool) { 62 | super.viewWillDisappear(animated) 63 | if cameraManager.result == .success { 64 | cameraManager.stopCapture() 65 | motion.motionStop() 66 | } 67 | } 68 | 69 | private func setup() { 70 | 71 | previewView = UIView(frame: .zero) 72 | previewView.backgroundColor = UIColor.white 73 | view.addSubview(previewView) 74 | let tapRecognzer = UITapGestureRecognizer(target: self, action: #selector(handleTapEvent(_:))) 75 | previewView.addGestureRecognizer(tapRecognzer) 76 | let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchEvent(_:))) 77 | previewView.addGestureRecognizer(pinchRecognizer) 78 | 79 | previewView.snp.makeConstraints { 80 | $0.width.equalToSuperview() 81 | $0.height.equalTo(661) 82 | $0.center.equalToSuperview() 83 | } 84 | 85 | let backView = UIView(frame: .zero) 86 | backView.backgroundColor = UIColor(red: 0.121, green: 0.121, blue: 0.121, alpha: 0.57) 87 | previewView.addSubview(backView) 88 | 89 | backView.snp.makeConstraints { 90 | $0.edges.equalToSuperview() 91 | } 92 | 93 | shutterBtn = UIButton(type: .custom) 94 | shutterBtn.setImage(UIImage(named: "icon_shutter"), for: .normal) 95 | shutterBtn.addTarget(self, action: #selector(takePhoto), for: .touchUpInside) 96 | view.addSubview(shutterBtn) 97 | 98 | shutterBtn.snp.makeConstraints { 99 | $0.width.height.equalTo(72) 100 | $0.centerX.equalToSuperview() 101 | $0.bottom.equalTo(previewView.snp.bottom).offset(-20) 102 | } 103 | 104 | filpBtn = UIButton(frame: .zero) 105 | filpBtn.setImage(UIImage(named: "icon_filp"), for: .normal) 106 | filpBtn.addTarget(self, action: #selector(cameraFilp), for: .touchUpInside) 107 | view.addSubview(filpBtn) 108 | filpBtn.snp.makeConstraints { 109 | $0.width.height.equalTo(48) 110 | $0.trailing.equalToSuperview().offset(-16) 111 | $0.centerY.equalTo(shutterBtn) 112 | } 113 | 114 | cameraModeControl = UISegmentedControl(items: [UIImage(named: "icon_photo_selector")!, UIImage(named: "icon_movie_selector")!]) 115 | cameraModeControl.selectedSegmentIndex = 0 116 | cameraModeControl.addTarget(self, action: #selector(changeCameraMode(_:)), for: .valueChanged) 117 | view.addSubview(cameraModeControl) 118 | cameraModeControl.snp.makeConstraints { 119 | $0.bottom.equalTo(shutterBtn.snp.top).offset(-20) 120 | $0.centerX.equalToSuperview() 121 | $0.size.equalTo(CGSize(width: 80, height: 40)) 122 | } 123 | 124 | recordBtn = UIButton(type: .custom) 125 | recordBtn.setImage(UIImage(named: "icon_start_record"), for: .normal) 126 | recordBtn.addTarget(self, action: #selector(handleRecordEvent(_:)), for: .touchUpInside) 127 | view.addSubview(recordBtn) 128 | recordBtn.snp.makeConstraints { 129 | $0.edges.equalTo(shutterBtn) 130 | } 131 | 132 | cameraManager = SYCameraManager() 133 | self.requestVideoPermission { [weak self](ret) in 134 | guard let `self` = self else { 135 | return 136 | } 137 | guard ret else { return } 138 | self.requestAudioPermission { [weak self](ret) in 139 | guard let `self` = self else { 140 | return 141 | } 142 | guard ret else { return } 143 | self.setupCamera() 144 | } 145 | } 146 | } 147 | 148 | private func requestVideoPermission(completion: @escaping (Bool)->Void) { 149 | let status = AVCaptureDevice.authorizationStatus(for: .video) 150 | switch status { 151 | case .notDetermined: 152 | AVCaptureDevice.requestAccess(for: .video) { (ret) in 153 | DispatchQueue.main.async { 154 | completion(ret) 155 | } 156 | 157 | } 158 | case .authorized: 159 | completion(true) 160 | default: 161 | completion(false) 162 | 163 | } 164 | } 165 | 166 | private func requestAudioPermission(completion: @escaping (Bool)->Void) { 167 | let status = AVCaptureDevice.authorizationStatus(for: .audio) 168 | switch status { 169 | case .notDetermined: 170 | AVCaptureDevice.requestAccess(for: .audio) { (ret) in 171 | DispatchQueue.main.async { 172 | completion(ret) 173 | } 174 | 175 | } 176 | case .authorized: 177 | completion(true) 178 | default: 179 | completion(false) 180 | 181 | } 182 | } 183 | 184 | private func setupCamera() { 185 | let config = SYCameraConfig() 186 | config.mode = cameraMode 187 | cameraManager.requestCamera(with: config) { [weak self](ret) in 188 | guard let `self` = self else { return } 189 | if ret == .success { 190 | self.cameraManager.delegate = self 191 | self.cameraManager.addPreview(to: self.previewView) 192 | self.cameraManager.startCapture() 193 | } 194 | } 195 | } 196 | 197 | private func refreshRecordUI() { 198 | if recordMode == .recordNormal { 199 | recordBtn.setImage(UIImage(named: "icon_start_record"), for: .normal) 200 | filpBtn.isHidden = false 201 | } else { 202 | recordBtn.setImage(UIImage(named: "icon_stop_record"), for: .normal) 203 | filpBtn.isHidden = true 204 | } 205 | } 206 | 207 | private func refreshCameraModeUI() { 208 | if cameraMode == .photoMode { 209 | recordBtn.isHidden = true 210 | shutterBtn.isHidden = false 211 | cameraModeControl.selectedSegmentIndex = 0 212 | } else if cameraMode == .videoMode { 213 | recordBtn.isHidden = false 214 | shutterBtn.isHidden = true 215 | cameraModeControl.selectedSegmentIndex = 1 216 | } 217 | 218 | } 219 | 220 | @objc private func takePhoto() { 221 | if cameraManager.result != .success { 222 | return 223 | } 224 | cameraManager.takePhoto() 225 | } 226 | 227 | @objc private func cameraFilp() { 228 | if cameraManager.result == .success { 229 | isBacking = !isBacking 230 | cameraManager.changeCameraPosition(isBacking ? .back : .front) 231 | } 232 | } 233 | 234 | @objc private func changeCameraMode(_ control: UISegmentedControl) { 235 | if cameraManager.result != .success { 236 | return 237 | } 238 | if control.selectedSegmentIndex == 0 { 239 | cameraManager.changeCameraMode(.photoMode, withSessionPreset: nil) 240 | } else if control.selectedSegmentIndex == 1 { 241 | cameraManager.changeCameraMode(.videoMode, withSessionPreset: nil) 242 | } 243 | } 244 | 245 | @objc private func handleRecordEvent(_ button: UIButton) { 246 | if cameraManager.result != .success { 247 | return 248 | } 249 | if recordMode == .recordNormal { 250 | cameraManager.startRecord() 251 | } else { 252 | cameraManager.stopRecord() 253 | } 254 | } 255 | 256 | @objc private func handleTapEvent(_ sender: UITapGestureRecognizer) { 257 | if cameraManager.result != .success { 258 | return 259 | } 260 | let point = sender.location(in: previewView) 261 | cameraManager.focus(with: point, mode: .autoFocus) 262 | cameraManager.exposure(with: point, mode: .autoExpose) 263 | } 264 | 265 | @objc private func handlePinchEvent(_ sender: UIPinchGestureRecognizer) { 266 | 267 | if cameraManager.result != .success { 268 | return 269 | } 270 | 271 | let scale = sender.scale 272 | currentZoom = scale; 273 | cameraManager.setZoom(currentZoom, withAnimated: true) 274 | } 275 | 276 | } 277 | 278 | extension SingleViewController: SYCameraManagerDelegate { 279 | func cameraSessionSetupResult(_ result: SYSessionSetupResult, with manager: SYCameraManager) { 280 | 281 | } 282 | 283 | func cameraDidStarted(_ manager: SYCameraManager) { 284 | 285 | } 286 | 287 | func cameraDidStoped(_ manager: SYCameraManager) { 288 | 289 | } 290 | 291 | func cameraDidFinishProcessingVideo(_ outputURL: URL?, with manager: SYCameraManager, withError error: Error?) { 292 | DispatchQueue.main.async { [weak self] in 293 | guard let `self` = self else { return } 294 | guard let outputURL = outputURL else { 295 | return 296 | } 297 | 298 | PreviewViewController.show(with: ["videoUrl": outputURL], from: self) 299 | } 300 | } 301 | 302 | func cameraDidFinishProcessingPhoto(_ image: UIImage?, withMetaData metaData: [AnyHashable : Any]?, with manager: SYCameraManager, withError error: Error?) { 303 | DispatchQueue.main.async { [weak self] in 304 | guard let `self` = self else { return } 305 | guard let image = image else { 306 | return 307 | } 308 | 309 | PreviewViewController.show(with: ["image": image], from: self) 310 | } 311 | } 312 | 313 | 314 | func cameraDidChange(_ mode: SYCameraMode, with manager: SYCameraManager) { 315 | DispatchQueue.main.async { 316 | self.cameraMode = mode 317 | } 318 | 319 | } 320 | 321 | func cameraRecordStatusDidChange(_ status: SYRecordStatus, with manager: SYCameraManager) { 322 | DispatchQueue.main.async { 323 | self.recordMode = status; 324 | } 325 | } 326 | 327 | func cameraDidChangedFocus(_ value: CGPoint, mode: AVCaptureDevice.FocusMode, with manager: SYCameraManager) { 328 | print("ViewController cameraDidChangedFocus value = \(value), mode = \(mode)") 329 | } 330 | 331 | func cameraDidChangedExposure(_ value: CGPoint, mode: AVCaptureDevice.ExposureMode, with manager: SYCameraManager) { 332 | print("ViewController cameraDidChangedExposure value = \(value), mode = \(mode)") 333 | } 334 | 335 | func cameraDidChangedZoom(_ value: CGFloat, with manager: SYCameraManager) { 336 | print("ViewController cameraDidChangedZoom value = \(value)") 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/VideoPlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerView.swift 3 | // marki 4 | // 5 | // Created by 马陈爽 on 2023/10/18. 6 | // Copyright © 2023 marki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | protocol VideoPlayerDelegate: NSObjectProtocol { 13 | func videoDidLoaded(_ container: VideoPlayerView, success: Bool) 14 | func videoDidPlayed(_ container: VideoPlayerView) 15 | func videoDidPaused(_ container: VideoPlayerView) 16 | func videoDidReplay(_ container: VideoPlayerView) 17 | func videoDidPlayToEndTime(_ container: VideoPlayerView) 18 | func videoChangedMute(_ container: VideoPlayerView, muted: Bool) 19 | func videoChangedProgress(_ container: VideoPlayerView, progress: Double, time: Int) 20 | } 21 | 22 | class VideoPlayerView: UIView { 23 | 24 | private(set) var url: URL? 25 | private var playerItem: AVPlayerItem? 26 | private var player: AVPlayer? 27 | private var playerLayer: AVPlayerLayer? 28 | 29 | weak var delegate: VideoPlayerDelegate? 30 | 31 | private var resumePlay: Bool = false 32 | 33 | var videoSize: CGSize { 34 | if let videoTrack = playerItem?.asset.tracks(withMediaType: .video).first { 35 | let videoSize = videoTrack.naturalSize 36 | let transform = videoTrack.preferredTransform 37 | let transformedSize = videoSize.applying(transform) 38 | let videoWidth = abs(transformedSize.width) 39 | let videoHeight = abs(transformedSize.height) 40 | 41 | return CGSize(width: videoWidth, height: videoHeight) 42 | } 43 | return .zero 44 | } 45 | 46 | var playedDuration: Int? { 47 | if let player = player { 48 | let time = CMTimeGetSeconds(player.currentTime()) 49 | if !time.isNaN { 50 | return Int(ceil(time)) 51 | } 52 | return nil 53 | } 54 | return nil 55 | } 56 | 57 | var videoDuration: Int? { 58 | if let playerItem = playerItem { 59 | let time = CMTimeGetSeconds(playerItem.duration) 60 | if !time.isNaN { 61 | return Int(ceil(time)) 62 | } 63 | return nil 64 | } 65 | return nil 66 | } 67 | 68 | init(withURL url: URL?, frame: CGRect) { 69 | self.url = url 70 | super.init(frame: frame) 71 | setup() 72 | } 73 | 74 | deinit { 75 | unregister() 76 | if let playerLayer = playerLayer { 77 | playerLayer.removeFromSuperlayer() 78 | self.playerLayer = nil 79 | } 80 | } 81 | 82 | required init?(coder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | private func setup() { 87 | // 创建AVPlayerItem 88 | if let url = url { 89 | let asset = AVURLAsset(url: url) 90 | playerItem = AVPlayerItem(asset: asset) 91 | // 创建AVPlayer 92 | player = AVPlayer(playerItem: playerItem) 93 | register() 94 | } 95 | // 创建AVPlayerLayer 96 | playerLayer = AVPlayerLayer(player: player) 97 | playerLayer!.videoGravity = .resizeAspect 98 | playerLayer!.frame = self.bounds 99 | self.layer.addSublayer(playerLayer!) 100 | } 101 | 102 | func updateUrl(url: URL?) { 103 | guard let url = url else { 104 | return 105 | } 106 | if let originUrl = self.url, originUrl.relativePath == url.relativePath { 107 | return 108 | } 109 | self.url = url 110 | unregister() 111 | let asset = AVURLAsset(url: url) 112 | playerItem = AVPlayerItem(asset: asset) 113 | 114 | if player == nil { 115 | player = AVPlayer(playerItem: playerItem) 116 | playerLayer?.player = player 117 | } else { 118 | // 创建AVPlayer 119 | player?.replaceCurrentItem(with: playerItem) 120 | } 121 | register() 122 | } 123 | 124 | 125 | 126 | override func layoutSubviews() { 127 | super.layoutSubviews() 128 | if let playerLayer = playerLayer { 129 | playerLayer.frame = self.bounds 130 | } 131 | 132 | } 133 | 134 | func play() { 135 | guard let player = player else { return } 136 | self.resumePlay = true 137 | player.play() 138 | 139 | } 140 | 141 | func pause() { 142 | guard let player = player else { return } 143 | self.resumePlay = false 144 | player.pause() 145 | } 146 | 147 | func replay() { 148 | guard let player = player else { return } 149 | pause() 150 | player.seek(to: kCMTimeZero) 151 | play() 152 | } 153 | 154 | var isMute: Bool { 155 | set { 156 | guard let player = player else { return } 157 | player.isMuted = newValue 158 | } 159 | get { 160 | guard let player = player else { return false } 161 | return player.isMuted 162 | } 163 | } 164 | 165 | private var statusObs: NSKeyValueObservation? 166 | private var controlStatusObs: NSKeyValueObservation? 167 | private var timeObserver: Any? 168 | 169 | private func register() { 170 | guard let player = self.player else { return } 171 | 172 | statusObs = player.observe(\.status, changeHandler: { [weak self](_, change) in 173 | guard let `self` = self else { return } 174 | if player.status == .readyToPlay { 175 | self.delegate?.videoDidLoaded(self, success: true) 176 | player.isMuted = false 177 | } else { 178 | self.delegate?.videoDidLoaded(self, success: false) 179 | } 180 | }) 181 | 182 | controlStatusObs = player.observe(\.timeControlStatus, changeHandler: { [weak self](_, change) in 183 | guard let `self` = self else { return } 184 | switch player.timeControlStatus { 185 | case .playing: 186 | self.delegate?.videoDidPlayed(self) 187 | self.delegate?.videoChangedMute(self, muted: player.isMuted) 188 | case .paused: 189 | self.delegate?.videoDidPaused(self) 190 | default: 191 | break 192 | } 193 | }) 194 | 195 | 196 | let interval = CMTime(value: 1, timescale: 1) // 1秒 197 | timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [weak self] (time) in 198 | guard let self = self else { return } 199 | guard let playerItem = self.playerItem else { return } 200 | // 每次触发间隔内执行的闭包 201 | let currentTime = CMTimeGetSeconds(time) 202 | let totalTime = CMTimeGetSeconds(playerItem.duration) 203 | if !currentTime.isNaN && !totalTime.isNaN { 204 | let progress = currentTime / totalTime 205 | self.delegate?.videoChangedProgress(self, progress: progress, time:Int(ceil(currentTime))) 206 | } 207 | 208 | } 209 | 210 | // 播放结束 211 | NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: OperationQueue.main) { [weak self] (noti) in 212 | guard let `self` = self else { 213 | return 214 | } 215 | if self.resumePlay { 216 | self.replay() 217 | } 218 | self.delegate?.videoDidPlayToEndTime(self) 219 | } 220 | } 221 | 222 | private func unregister() { 223 | if let player = player { 224 | if let timeObserver = timeObserver { 225 | player.removeTimeObserver(timeObserver) 226 | } 227 | player.replaceCurrentItem(with: nil) 228 | self.player = nil 229 | } 230 | 231 | statusObs = nil 232 | controlStatusObs = nil 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Example/SwiftyCamera/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftyCamera 4 | // 5 | // Created by chenshuangma@foxmail.com on 05/25/2024. 6 | // Copyright (c) 2024 chenshuangma@foxmail.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SnapKit 11 | 12 | 13 | class ViewController: UIViewController { 14 | 15 | private var tableView: UITableView! 16 | private var items: [String] = ["普通相机", "双摄相机"] 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | setupViews() 21 | } 22 | 23 | private func setupViews() { 24 | tableView = UITableView(frame: .zero) 25 | tableView.delegate = self 26 | tableView.dataSource = self 27 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 28 | self.view.addSubview(tableView) 29 | 30 | tableView.snp.makeConstraints { 31 | $0.edges.equalToSuperview() 32 | } 33 | } 34 | 35 | 36 | } 37 | 38 | extension ViewController: UITableViewDelegate, UITableViewDataSource { 39 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 40 | return items.count 41 | } 42 | 43 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 44 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 45 | cell.textLabel?.text = items[indexPath.row] 46 | return cell 47 | } 48 | 49 | func numberOfSections(in tableView: UITableView) -> Int { 50 | return 1 51 | } 52 | 53 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 54 | switch indexPath.row { 55 | case 0: 56 | self.navigationController?.pushViewController(SingleViewController(), animated: true) 57 | break 58 | case 1: 59 | self.navigationController?.pushViewController(DualViewController(), animated: true) 60 | break 61 | default: 62 | break 63 | } 64 | } 65 | 66 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 67 | return 60 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftyCamera 3 | 4 | class Tests: XCTestCase { 5 | 6 | override func setUp() { 7 | super.setUp() 8 | // Put setup code here. This method is called before the invocation of each test method in the class. 9 | } 10 | 11 | override func tearDown() { 12 | // Put teardown code here. This method is called after the invocation of each test method in the class. 13 | super.tearDown() 14 | } 15 | 16 | func testExample() { 17 | // This is an example of a functional test case. 18 | XCTAssert(true, "Pass") 19 | } 20 | 21 | func testPerformanceExample() { 22 | // This is an example of a performance test case. 23 | self.measure() { 24 | // Put the code you want to measure the time of here. 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 chenshuangma@foxmail.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | theme: smartblue 3 | --- 4 | 5 | # SwiftyCamera 6 | 7 | [SwiftyCamera](https://github.com/machenshuang/SwiftyCamera) 是基于 Objective-C 开发的一个轻量级相机 SDK,其目的是想让开发相机应用的开发者更快速的集成相机功能。 8 | 9 | ## 集成方式 10 | 11 | 在 Podfile 中引入: 12 | ```ruby 13 | pod 'SwiftyCamera' :git => 'https://github.com/machenshuang/SwiftyCamera', :branch => 'master' 14 | ``` 15 | 16 | ## 功能介绍 17 | 18 | SwiftyCamera 通过 SYCameraConfig 配置相机的参数,使用 SYCameraManager 来管理相机对象,并用 SYCameraManagerDelegate 将相机的生命周期和状态回调给使用者。 19 | 20 | ### 构建相机 21 | 22 | 构建单摄相机的过程: 23 | ```swift 24 | let config = SYCameraConfig() 25 | config.type = .singleDevice // 单摄模式 26 | config.mode = .photoMode // 拍照模式 27 | cameraManager.requestCamera(with: config) { [weak self](ret) in 28 | guard let `self` = self else { return } 29 | if ret == .success { 30 | // 设置 delegate 31 | self.cameraManager.delegate = self 32 | // 将预览视图添加到 View 上 33 | self.cameraManager.addPreview(to: self.previewView) 34 | // 启动相机 35 | self.cameraManager.startCapture() 36 | } 37 | } 38 | ``` 39 | 40 | 构建双摄相机的过程: 41 | ```swift 42 | let config = SYCameraConfig() 43 | config.type = .dualDevice // 双摄模式 44 | config.mode = .photoMode // 拍照模式 45 | cameraManager.requestCamera(with: config) { [weak self](ret) in 46 | guard let `self` = self else { return } 47 | if ret == .success { 48 | // 设置 delegate 49 | self.cameraManager.delegate = self 50 | // 将预览视图添加到 View 上 51 | self.cameraManager.addPreview(to: self.previewView) 52 | // 启动相机 53 | self.cameraManager.startCapture() 54 | } 55 | } 56 | ``` 57 | 58 | 构建和启动结果会通过 SYCameraManagerDelegate 方法回调: 59 | ```swift 60 | extension ViewController: SYCameraManagerDelegate { 61 | /// 相机配置结果 62 | /// - Parameters: 63 | /// - result: SYSessionSetupResult 64 | /// - manager: SYCameraManager 65 | func cameraSessionSetupResult(_ result: SYSessionSetupResult, with manager: SYCameraManager) { 66 | 67 | } 68 | 69 | /// 相机已启动 70 | /// - Parameter manager: SYCameraManager 71 | func cameraDidStarted(_ manager: SYCameraManager) { 72 | 73 | } 74 | 75 | /// 相机已停止 76 | /// - Parameter manager: SYCameraManager 77 | func cameraDidStoped(_ manager: SYCameraManager) { 78 | 79 | } 80 | } 81 | 82 | ``` 83 | 84 | ### 切换摄像头 85 | 86 | 摄像头切换的过程: 87 | ```swift 88 | @objc private func cameraFilp() { 89 | // 判断相机状态 90 | if cameraManager.result == .success { 91 | isBacking = !isBacking 92 | // 切换摄像头 93 | cameraManager.changeCameraPosition(isBacking ? .back : .front) 94 | } 95 | } 96 | ``` 97 | 98 | 99 | 100 | ### 切换拍照和录制模式 101 | 切换拍照和录制模式的过程: 102 | ```swift 103 | @objc private func changeCameraMode(_ control: UISegmentedControl) { 104 | if cameraManager.result != .success { 105 | return 106 | } 107 | if control.selectedSegmentIndex == 0 { 108 | // 切换拍照模式 109 | cameraManager.changeCameraMode(.photoMode, withSessionPreset: nil) 110 | } else if control.selectedSegmentIndex == 1 { 111 | // 切换录制模式 112 | cameraManager.changeCameraMode(.videoMode, withSessionPreset: nil) 113 | } 114 | } 115 | ``` 116 | 117 | 摄像头切换后的结果会通过 SYCameraManagerDelegate 方法回调: 118 | ```swift 119 | extension ViewController: SYCameraManagerDelegate { 120 | /// 相机设备切换改变 121 | /// - Parameters: 122 | /// - backFacing: 是否是后置 123 | /// - manager: SYCameraManager 124 | func cameraDidChangedPosition(_ backFacing: Bool, with manager: SYCameraManager) { 125 | 126 | } 127 | } 128 | ``` 129 | 130 | ### 调整焦点和曝光 131 | 调整焦点和曝光的过程: 132 | ```swift 133 | @objc private func handleTapEvent(_ sender: UITapGestureRecognizer) { 134 | if cameraManager.result != .success { 135 | return 136 | } 137 | let point = sender.location(in: previewView) 138 | cameraManager.focus(with: point, mode: .autoFocus) 139 | cameraManager.exposure(with: point, mode: .autoExpose) 140 | } 141 | ``` 142 | 调整焦点和曝光的结果会通过 SYCameraManagerDelegate 方法回调: 143 | ```swift 144 | extension ViewController: SYCameraManagerDelegate { 145 | /// 相机焦点调整改变 146 | /// - Parameters: 147 | /// - value: 位置 148 | /// - mode: 模式 149 | /// - manager: SYCameraManager 150 | func cameraDidChangedFocus(_ value: CGPoint, mode: AVCaptureDevice.FocusMode, with manager: SYCameraManager) { 151 | 152 | } 153 | 154 | /// 相机曝光值调整改变 155 | /// - Parameters: 156 | /// - value: 曝光值 157 | /// - mode: 模式 158 | /// - manager: SYCameraManager 159 | func cameraDidChangedExposure(_ value: CGPoint, mode: AVCaptureDevice.ExposureMode, with manager: SYCameraManager) { 160 | 161 | } 162 | } 163 | ``` 164 | 165 | ### 调整焦距 166 | 调整焦距的过程: 167 | ```objc 168 | @objc private func handlePinchEvent(_ sender: UIPinchGestureRecognizer) { 169 | 170 | if cameraManager.result != .success { 171 | return 172 | } 173 | 174 | let scale = sender.scale 175 | currentZoom = scale; 176 | cameraManager.setZoom(currentZoom, withAnimated: true) 177 | } 178 | ``` 179 | 调整焦距的结果会通过 SYCameraManagerDelegate 方法回调: 180 | ```swift 181 | extension ViewController: SYCameraManagerDelegate { 182 | /// 相机焦距调整改变 183 | /// - Parameters: 184 | /// - value: 焦距 185 | /// - manager: SYCameraManager 186 | func cameraDidChangedZoom(_ value: CGFloat, with manager: SYCameraManager) { 187 | 188 | } 189 | } 190 | ``` 191 | 192 | ### 拍照 193 | 194 | 拍照的过程: 195 | ```swift 196 | @objc private func takePhoto() { 197 | if cameraManager.result != .success { 198 | return 199 | } 200 | cameraManager.takePhoto() 201 | } 202 | ``` 203 | 204 | 拍照的结果会通过 SYCameraManagerDelegate 方法回调: 205 | ```swift 206 | extension ViewController: SYCameraManagerDelegate { 207 | /// 相机拍照结果 208 | /// - Parameters: 209 | /// - image: 图片 210 | /// - metaData: 摘要 211 | /// - manager: SYCameraManager 212 | /// - error: 错误 213 | func cameraDidFinishProcessingPhoto(_ image: UIImage?, withMetaData metaData: [AnyHashable : Any]?, with manager: SYCameraManager, withError error: Error?) { 214 | 215 | } 216 | } 217 | ``` 218 | ### 切换录制模式 219 | 220 | 切换录制模式的过程: 221 | ```swift 222 | @objc private func changeCameraMode(_ control: UISegmentedControl) { 223 | if cameraManager.result != .success { 224 | return 225 | } 226 | if control.selectedSegmentIndex == 0 { 227 | cameraManager.changeCameraMode(.photoMode, withSessionPreset: nil) 228 | } else if control.selectedSegmentIndex == 1 { 229 | cameraManager.changeCameraMode(.videoMode, withSessionPreset: nil) 230 | } 231 | } 232 | ``` 233 | 模式切换的结果会通过 SYCameraManagerDelegate 方法回调: 234 | ```swift 235 | extension ViewController: SYCameraManagerDelegate { 236 | /// 相机模式改变 237 | /// - Parameters: 238 | /// - mode: 模式 239 | /// - manager: SYCameraManager 240 | func cameraDidChange(_ mode: SYCameraMode, with manager: SYCameraManager) { 241 | 242 | } 243 | } 244 | ``` 245 | 246 | ## 开始录制和结束录制 247 | 248 | 开始录制和结束录制的过程: 249 | ```swift 250 | @objc private func handleRecordEvent(_ button: UIButton) { 251 | if cameraManager.result != .success { 252 | return 253 | } 254 | if recordMode == .recordNormal { 255 | cameraManager.startRecord() 256 | } else { 257 | cameraManager.stopRecord() 258 | } 259 | } 260 | ``` 261 | 262 | 录制状态和录制结果会通过 SYCameraManagerDelegate 方法回调: 263 | ```swift 264 | extension ViewController: SYCameraManagerDelegate { 265 | 266 | /// 相机录制结果 267 | /// - Parameters: 268 | /// - outputURL: 保存路径 269 | /// - manager: SYCameraManager 270 | /// - error: error 271 | func cameraDidFinishProcessingVideo(_ outputURL: URL?, with manager: SYCameraManager, withError error: Error?) { 272 | 273 | } 274 | 275 | /// 相机录制状态改变 276 | /// - Parameters: 277 | /// - status: 录制状态 278 | /// - manager: SYCameraManager 279 | func cameraRecordStatusDidChange(_ status: SYRecordStatus, with manager: SYCameraManager) { 280 | 281 | } 282 | } 283 | ``` 284 | 285 | ## Author 286 | 287 | chenshuangma@foxmail.com 288 | 289 | ## License 290 | 291 | SwiftyCamera is available under the MIT license. See the LICENSE file for more info. 292 | -------------------------------------------------------------------------------- /Sources/SYBaseCamera.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYBaseCamera.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2023/4/6. 6 | // 7 | 8 | #import 9 | #import 10 | #import "SYCameraConfig.h" 11 | #import "SYPreviewView.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @protocol SYCameraDelegate 16 | 17 | @required 18 | - (void)cameraSessionSetupResult:(SYSessionSetupResult)result; 19 | - (void)cameraDidStarted; 20 | - (void)cameraDidStoped; 21 | - (void)cameraDidFinishProcessingPhoto:(AVCapturePhoto *_Nullable)photo 22 | withPosition:(AVCaptureDevicePosition)position 23 | error:(NSError *_Nullable)error; 24 | 25 | - (void)cameraCaptureVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer; 26 | - (void)cameraCaptureAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer; 27 | - (void)cameraDidChangePosition:(BOOL)backFacing; 28 | - (void)cameraDidChangeMode:(SYCameraMode)mode; 29 | - (void)cameraDidChangeFocus:(CGPoint)value mode:(AVCaptureFocusMode)mode; 30 | - (void)cameraDidChangeZoom:(CGFloat)value; 31 | - (void)cameraDidChangeExposure:(CGPoint)value mode:(AVCaptureExposureMode)mode; 32 | - (void)camerahDidChangeFlash:(AVCaptureFlashMode)mode; 33 | - (void)cameraDidChangeEV:(CGFloat)value; 34 | - (void)cameraWillProcessPhoto; 35 | - (SYPreviewView *)getVideoPreviewForPosition:(AVCaptureDevicePosition)position; 36 | 37 | @end 38 | 39 | typedef struct SYCameraDelegateMap { 40 | unsigned int cameraDidStarted : 1; 41 | unsigned int cameraDidStoped : 1; 42 | unsigned int cameraCaptureVideoSampleBuffer : 1; 43 | unsigned int cameraCaptureAudioSampleBuffer : 1; 44 | unsigned int cameraDidFinishProcessingPhoto : 1; 45 | unsigned int changedPosition : 1; 46 | unsigned int changedFocus : 1; 47 | unsigned int changedZoom : 1; 48 | unsigned int changedExposure : 1; 49 | unsigned int changedFlash : 1; 50 | unsigned int changedEV : 1; 51 | unsigned int cameraWillProcessPhoto : 1; 52 | unsigned int cameraDidChangeMode: 1; 53 | unsigned int getVideoPreviewForPosition: 1; 54 | unsigned int cameraSessionSetupResult: 1; 55 | } SYCameraDelegateMap; 56 | 57 | @interface SYBaseCamera : NSObject 58 | 59 | @property (nonatomic, assign) AVCaptureFlashMode flashMode; 60 | @property (nonatomic, assign) CGFloat ev; 61 | @property (nonatomic, strong) AVCaptureSession *session; 62 | @property (nonatomic, assign) AVCaptureVideoOrientation orientation; 63 | @property (nonatomic, assign, readonly) CGFloat zoom; 64 | @property (nonatomic, assign, readonly) CGFloat minZoom; 65 | @property (nonatomic, assign, readonly) CGFloat maxZoom; 66 | @property (nonatomic, assign, readonly) AVCaptureDevicePosition cameraPosition; 67 | @property (nonatomic, assign, readonly) SYCameraMode mode; 68 | @property (nonatomic, strong, readonly) dispatch_queue_t sessionQueue; 69 | @property (nonatomic, strong, readonly) dispatch_queue_t cameraProcessQueue; 70 | @property (nonatomic, strong, readonly) dispatch_queue_t captureQueue; 71 | @property (nonatomic, assign, readonly) SYCameraDelegateMap delegateMap; 72 | 73 | 74 | @property (nullable, nonatomic, weak) id delegate; 75 | 76 | +(SYBaseCamera * _Nullable)createCameraWithConfig:(SYCameraConfig *)config withDelegate:(id)delegate; 77 | 78 | - (instancetype)initWithSessionPreset:(AVCaptureSessionPreset)sessionPreset 79 | cameraPosition:(AVCaptureDevicePosition)cameraPosition 80 | withMode:(SYCameraMode)mode 81 | withDelegate:(id)delegate; 82 | 83 | - (void)setupCaptureSession; 84 | - (BOOL)setupCameraDevice; 85 | - (void)setupVideoDeviceInput; 86 | - (void)setupVideoOutput; 87 | - (void)setupPhotoOutput; 88 | - (void)startCapture; 89 | - (void)stopCapture; 90 | - (void)changeCameraPosition:(AVCaptureDevicePosition)position; 91 | - (void)changeCameraMode:(SYCameraMode)mode 92 | withSessionPreset:(AVCaptureSessionPreset)sessionPreset; 93 | - (void)addMicrophoneWith:(void(^)(void))completion; 94 | - (void)removeMicrophoneWith:(void(^)(void))completion; 95 | - (void)focusWithPoint:(CGPoint)point mode:(AVCaptureFocusMode)mode; 96 | - (void)exposureWithPoint:(CGPoint)point mode:(AVCaptureExposureMode)mode; 97 | - (void)takePhoto; 98 | - (void)setZoom:(CGFloat)zoom withAnimated:(BOOL)animated; 99 | - (AVCaptureDevice *)fetchCameraDeviceWithPosition:(AVCaptureDevicePosition)position; 100 | 101 | 102 | @end 103 | 104 | NS_ASSUME_NONNULL_END 105 | -------------------------------------------------------------------------------- /Sources/SYBaseCamera.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYBaseCamera.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2023/4/6. 6 | // 7 | 8 | #import "SYBaseCamera.h" 9 | #import "SYLog.h" 10 | #import "SYSingleCamera.h" 11 | #import "SYMultiCamera.h" 12 | 13 | static NSString *TAG = @"SYBaseCamera"; 14 | 15 | @interface SYBaseCamera () 16 | { 17 | AVCaptureDeviceInput *_audioInput; 18 | AVCaptureAudioDataOutput *_audioOutput; 19 | } 20 | 21 | @property (nonatomic, assign, readwrite) AVCaptureDevicePosition cameraPosition; 22 | @property (nonatomic, assign, readwrite) SYCameraMode mode; 23 | @property (nonatomic, strong, readwrite) dispatch_queue_t sessionQueue; 24 | @property (nonatomic, strong, readwrite) dispatch_queue_t cameraProcessQueue; 25 | @property (nonatomic, strong, readwrite) dispatch_queue_t captureQueue; 26 | @property (nonatomic, assign, readwrite) SYCameraDelegateMap delegateMap; 27 | 28 | @end 29 | 30 | 31 | @implementation SYBaseCamera 32 | 33 | + (SYBaseCamera *)createCameraWithConfig:(SYCameraConfig *)config withDelegate:(id)delegate 34 | { 35 | SYBaseCamera *camera; 36 | AVCaptureSessionPreset preset = config.sessionPreset; 37 | AVCaptureDevicePosition position = config.devicePosition; 38 | if (position == AVCaptureDevicePositionUnspecified) { 39 | position = AVCaptureDevicePositionBack; 40 | } 41 | 42 | SYCameraMode mode = config.mode; 43 | 44 | if (preset == nil) { 45 | if (mode == SYPhotoMode) { 46 | preset = AVCaptureSessionPresetPhoto; 47 | } else { 48 | preset = AVCaptureSessionPresetHigh; 49 | } 50 | } 51 | switch (config.type) { 52 | case SYSingleDevice: { 53 | camera = [[SYSingleCamera alloc] initWithSessionPreset:preset cameraPosition:position withMode:mode withDelegate:delegate]; 54 | break; 55 | } 56 | case SYDualDevice: { 57 | if (@available(iOS 13.0, *)) { 58 | camera = [[SYMultiCamera alloc] initWithSessionPreset:preset cameraPosition:position withMode:mode withDelegate:delegate]; 59 | } 60 | break; 61 | } 62 | } 63 | return camera; 64 | } 65 | 66 | - (instancetype)initWithSessionPreset:(AVCaptureSessionPreset)sessionPreset 67 | cameraPosition:(AVCaptureDevicePosition)cameraPosition 68 | withMode:(SYCameraMode)mode 69 | withDelegate:(id)delegate 70 | { 71 | self = [super init]; 72 | if (self) { 73 | _ev = 0.5; 74 | _sessionQueue = dispatch_queue_create("com.machenshuang.camera.AVCameraSessionQueue", DISPATCH_QUEUE_SERIAL); 75 | _cameraProcessQueue = dispatch_queue_create("com.machenshuang.camera.AVCameraCameraProcessingQueue", DISPATCH_QUEUE_SERIAL); 76 | _captureQueue = dispatch_queue_create("com.machenshuang.camera.AVCameraCaptureQueue", DISPATCH_QUEUE_SERIAL); 77 | 78 | _cameraPosition = cameraPosition; 79 | _mode = mode; 80 | self.delegate = delegate; 81 | [self setupCaptureSession]; 82 | __weak typeof(self)weakSelf = self; 83 | dispatch_async(_sessionQueue, ^{ 84 | __strong typeof(weakSelf)strongSelf = weakSelf; 85 | 86 | if (![strongSelf setupCameraDevice]) { 87 | SYLog(TAG, "initWithSessionPreset setupCameraDevice failure"); 88 | if (strongSelf->_delegateMap.cameraSessionSetupResult) { 89 | [strongSelf.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 90 | } 91 | return; 92 | } 93 | [strongSelf configureSesson: sessionPreset]; 94 | }); 95 | 96 | [[NSNotificationCenter defaultCenter] addObserver:self 97 | selector:@selector(handleCaptureSessionNotification:) 98 | name:nil 99 | object:_session]; 100 | } 101 | return self; 102 | } 103 | 104 | 105 | - (void)setDelegate:(id)delegate 106 | { 107 | _delegate = delegate; 108 | _delegateMap.cameraDidStarted = [delegate respondsToSelector:@selector(cameraDidStarted)]; 109 | _delegateMap.cameraDidStoped = [delegate respondsToSelector:@selector(cameraDidStoped)]; 110 | _delegateMap.cameraCaptureVideoSampleBuffer = [delegate respondsToSelector:@selector(cameraCaptureVideoSampleBuffer:)]; 111 | _delegateMap.cameraCaptureAudioSampleBuffer = [delegate respondsToSelector:@selector(cameraCaptureAudioSampleBuffer:)]; 112 | _delegateMap.cameraDidFinishProcessingPhoto = [delegate respondsToSelector:@selector(cameraDidFinishProcessingPhoto:withPosition:error:)]; 113 | _delegateMap.changedPosition = [delegate respondsToSelector:@selector(cameraDidChangePosition:)]; 114 | _delegateMap.changedFocus = [delegate respondsToSelector:@selector(cameraDidChangeFocus:mode:)]; 115 | _delegateMap.changedZoom = [delegate respondsToSelector:@selector(cameraDidChangeZoom:)]; 116 | _delegateMap.changedExposure = [delegate respondsToSelector:@selector(cameraDidChangeExposure:mode:)]; 117 | _delegateMap.changedFlash = [delegate respondsToSelector:@selector(camerahDidChangeFlash:)]; 118 | _delegateMap.changedEV = [delegate respondsToSelector:@selector(cameraDidChangeEV:)]; 119 | _delegateMap.cameraWillProcessPhoto = [delegate respondsToSelector:@selector(cameraWillProcessPhoto)]; 120 | _delegateMap.cameraDidChangeMode = [delegate respondsToSelector:@selector(cameraDidChangeMode:)]; 121 | _delegateMap.getVideoPreviewForPosition = [delegate respondsToSelector:@selector(getVideoPreviewForPosition:)]; 122 | _delegateMap.cameraSessionSetupResult = [delegate respondsToSelector:@selector(cameraSessionSetupResult:)]; 123 | } 124 | 125 | - (void)changeCameraMode:(SYCameraMode)mode 126 | withSessionPreset:(AVCaptureSessionPreset)sessionPreset 127 | { 128 | __weak typeof(self)weakSelf = self; 129 | dispatch_async(_sessionQueue, ^{ 130 | __strong typeof(weakSelf)strongSelf = weakSelf; 131 | if (strongSelf->_mode == mode) { 132 | if (strongSelf->_delegateMap.cameraDidChangeMode) { 133 | [strongSelf.delegate cameraDidChangeMode:mode]; 134 | } 135 | return; 136 | } 137 | strongSelf->_mode = mode; 138 | [strongSelf configureSesson:sessionPreset]; 139 | if (strongSelf->_delegateMap.cameraDidChangeMode) { 140 | [strongSelf.delegate cameraDidChangeMode:mode]; 141 | } 142 | }); 143 | } 144 | 145 | - (void)configureSesson:(AVCaptureSessionPreset)sessionPreset 146 | { 147 | SYLog(TAG, "configureSesson beginConfiguration"); 148 | [_session beginConfiguration]; 149 | [self setupVideoDeviceInput]; 150 | [self setupVideoOutput]; 151 | [self setupPhotoOutput]; 152 | if (@available(iOS 13.0, *)) { 153 | if (![_session isKindOfClass:[AVCaptureMultiCamSession class]]) { 154 | [_session setSessionPreset:sessionPreset]; 155 | } 156 | } else { 157 | [_session setSessionPreset:sessionPreset]; 158 | } 159 | [_session commitConfiguration]; 160 | [self exposureWithPoint:CGPointMake(0.5, 0.5) mode:AVCaptureExposureModeAutoExpose]; 161 | [self setZoom:1.0 withAnimated:NO]; 162 | SYLog(TAG, "configureSesson commitConfiguration"); 163 | } 164 | 165 | - (void)setupVideoDeviceInput 166 | { 167 | 168 | } 169 | 170 | - (void)setupVideoOutput 171 | { 172 | 173 | } 174 | 175 | - (void)setupPhotoOutput 176 | { 177 | 178 | } 179 | 180 | - (BOOL)setupCameraDevice 181 | { 182 | return NO; 183 | } 184 | 185 | - (void)setupCaptureSession 186 | { 187 | 188 | } 189 | 190 | - (void)addMicrophoneWith:(void (^)(void))completion 191 | { 192 | __weak typeof(self)weakSelf = self; 193 | dispatch_async(_sessionQueue, ^{ 194 | __strong typeof(weakSelf)strongSelf = weakSelf; 195 | if (strongSelf->_audioInput == nil) { 196 | AVCaptureDevice* audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; 197 | NSError *error = nil; 198 | AVCaptureDeviceInput* audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error]; 199 | if (error) { 200 | SYLog(TAG, "addMicrophoneWith deviceInputWithDevice error = %@", error.description); 201 | } 202 | if ([strongSelf->_session canAddInput:audioDeviceInput]) { 203 | [strongSelf->_session addInput:audioDeviceInput]; 204 | strongSelf->_audioInput = audioDeviceInput; 205 | SYLog(TAG, "configureAudioDeviceInput addAudioInput"); 206 | } else { 207 | SYLog(TAG, "addMicrophoneWith can not addAudioInput"); 208 | } 209 | } 210 | 211 | if (strongSelf->_audioOutput == nil) { 212 | strongSelf->_audioOutput = [[AVCaptureAudioDataOutput alloc] init]; 213 | [strongSelf->_audioOutput setSampleBufferDelegate:self queue:strongSelf->_cameraProcessQueue]; 214 | if ([strongSelf->_session canAddOutput:strongSelf->_audioOutput]) { 215 | [strongSelf->_session addOutput:strongSelf->_audioOutput]; 216 | } else { 217 | SYLog(TAG, "addMicrophoneWith addAudioOutput"); 218 | } 219 | } 220 | completion(); 221 | }); 222 | } 223 | 224 | - (void)removeMicrophoneWith:(void (^)(void))completion 225 | { 226 | __weak typeof(self)weakSelf = self; 227 | dispatch_async(_sessionQueue, ^{ 228 | __strong typeof(weakSelf)strongSelf = weakSelf; 229 | if (strongSelf->_audioInput) { 230 | [strongSelf->_session removeInput:strongSelf->_audioInput]; 231 | strongSelf->_audioInput = nil; 232 | SYLog(TAG, "removeMicrophoneWith removeAudioDevice"); 233 | } 234 | 235 | if (strongSelf->_audioOutput) { 236 | [strongSelf->_session removeOutput:strongSelf->_audioOutput]; 237 | strongSelf->_audioOutput = nil; 238 | SYLog(TAG, "removeMicrophoneWith removeAudioOutput"); 239 | } 240 | completion(); 241 | }); 242 | } 243 | 244 | 245 | - (void)startCapture 246 | { 247 | __weak typeof(self)weakSelf = self; 248 | dispatch_async(_sessionQueue, ^{ 249 | __strong typeof(weakSelf)strongSelf = weakSelf; 250 | if (![strongSelf->_session isRunning]) { 251 | [strongSelf->_session startRunning]; 252 | } 253 | if (strongSelf->_delegate) { 254 | [strongSelf->_delegate cameraDidStarted]; 255 | } 256 | }); 257 | } 258 | 259 | - (void)stopCapture 260 | { 261 | __weak typeof(self)weakSelf = self; 262 | dispatch_async(_sessionQueue, ^{ 263 | __strong typeof(weakSelf)strongSelf = weakSelf; 264 | if ([strongSelf->_session isRunning]) { 265 | [strongSelf->_session stopRunning]; 266 | } 267 | if (strongSelf->_delegate) { 268 | [strongSelf->_delegate cameraDidStoped]; 269 | } 270 | }); 271 | } 272 | 273 | - (void)handleCaptureSessionNotification:(NSNotification *)noti 274 | { 275 | if ([noti.name isEqualToString:AVCaptureSessionRuntimeErrorNotification]) { 276 | 277 | } else if ([noti.name isEqualToString:AVCaptureSessionDidStartRunningNotification]) { 278 | if (self.delegate) { 279 | [self.delegate cameraDidStarted]; 280 | } 281 | } else if ([noti.name isEqualToString:AVCaptureSessionDidStopRunningNotification]) { 282 | if (self.delegate) { 283 | [self.delegate cameraDidStoped]; 284 | } 285 | } 286 | } 287 | 288 | - (void)changeCameraPosition:(AVCaptureDevicePosition)position 289 | { 290 | if (position == _cameraPosition) { 291 | if (self.delegateMap.changedPosition) { 292 | [self->_delegate cameraDidChangePosition:self->_cameraPosition == AVCaptureDevicePositionBack ? YES : NO]; 293 | } 294 | return; 295 | } 296 | 297 | self->_cameraPosition = position; 298 | BOOL _ = [self setupCameraDevice]; 299 | __weak typeof(self)weakSelf = self; 300 | dispatch_async(_sessionQueue, ^{ 301 | __strong typeof(weakSelf)strongSelf = weakSelf; 302 | [self setupVideoDeviceInput]; 303 | [strongSelf->_session commitConfiguration]; 304 | if (strongSelf->_delegateMap.changedPosition) { 305 | [strongSelf->_delegate cameraDidChangePosition:strongSelf->_cameraPosition == AVCaptureDevicePositionBack ? YES : NO]; 306 | } 307 | }); 308 | } 309 | 310 | - (void)focusWithPoint:(CGPoint)point mode:(AVCaptureFocusMode)mode 311 | { 312 | } 313 | 314 | - (void)exposureWithPoint:(CGPoint)point mode:(AVCaptureExposureMode)mode 315 | { 316 | } 317 | 318 | - (void)setExposureMode:(AVCaptureExposureMode)mode 319 | { 320 | // AVCaptureDevice *inputCamera = [self getEnableCameraDeviceWithPosition:_cameraPosition]; 321 | // if (!inputCamera) { 322 | // return; 323 | // } 324 | // if (_mode == SYElectronicScreen) { 325 | // if ([inputCamera isExposureModeSupported:AVCaptureExposureModeCustom]) { 326 | // inputCamera.exposureMode = AVCaptureExposureModeCustom; 327 | // CMTime min = inputCamera.activeFormat.minExposureDuration; 328 | // CMTime max = inputCamera.activeFormat.maxExposureDuration; 329 | // CMTime time = CMTimeMakeWithSeconds(0.06, 1000); 330 | // if (CMTimeCompare(time, min) == NSOrderedAscending) { 331 | // time = min; 332 | // } 333 | // if (CMTimeCompare(time, max) == NSOrderedDescending) { 334 | // time = max; 335 | // } 336 | // [inputCamera setExposureModeCustomWithDuration:time ISO:inputCamera.activeFormat.minISO completionHandler:^(CMTime syncTime) { 337 | // }]; 338 | // } 339 | // } else { 340 | // if ([inputCamera isExposureModeSupported:mode]) { 341 | // [inputCamera setExposureMode:mode]; 342 | // } 343 | // } 344 | } 345 | 346 | - (void)setEv:(CGFloat)value 347 | { 348 | _ev = value; 349 | } 350 | 351 | - (void)setZoom:(CGFloat)zoom withAnimated:(BOOL)animated 352 | { 353 | } 354 | 355 | - (CGFloat)zoom 356 | { 357 | return 1; 358 | } 359 | 360 | - (CGFloat)maxZoom 361 | { 362 | return 1; 363 | } 364 | 365 | - (CGFloat)minZoom { 366 | return 1; 367 | } 368 | 369 | - (void)takePhoto 370 | { 371 | } 372 | 373 | - (void)extractInfoFromSampleBuffer:(CMSampleBufferRef)sampleBuffer 374 | { 375 | CFDictionaryRef dictRef = CMCopyDictionaryOfAttachments(NULL, sampleBuffer, kCMAttachmentMode_ShouldPropagate); 376 | NSDictionary *dict = [[NSDictionary alloc] initWithDictionary:(__bridge NSDictionary *)dictRef]; 377 | CFRelease(dictRef); 378 | NSDictionary *exifDict = dict[(NSString *)kCGImagePropertyExifDictionary]; 379 | if (exifDict != nil) { 380 | CGFloat brightness = [exifDict[(NSString *)kCGImagePropertyExifBrightnessValue] floatValue]; 381 | } 382 | } 383 | 384 | - (AVCaptureDevice *)fetchCameraDeviceWithPosition:(AVCaptureDevicePosition)position { 385 | AVCaptureDevice *device; 386 | if (position == AVCaptureDevicePositionBack) { 387 | NSArray *deviceType; 388 | if (@available(iOS 13.0, *)) { 389 | deviceType = @[AVCaptureDeviceTypeBuiltInTripleCamera, AVCaptureDeviceTypeBuiltInDualWideCamera, AVCaptureDeviceTypeBuiltInWideAngleCamera]; 390 | AVCaptureDeviceDiscoverySession *deviceSession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceType mediaType:AVMediaTypeVideo position:position]; 391 | device = deviceSession.devices.firstObject; 392 | } else { 393 | deviceType = @[AVCaptureDeviceTypeBuiltInDualCamera, AVCaptureDeviceTypeBuiltInWideAngleCamera]; 394 | AVCaptureDeviceDiscoverySession *deviceSession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceType mediaType:AVMediaTypeVideo position:position]; 395 | device = deviceSession.devices.firstObject; 396 | } 397 | } else { 398 | AVCaptureDevice *frontDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:position]; 399 | device = frontDevice; 400 | } 401 | return device; 402 | } 403 | 404 | #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate 405 | - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection 406 | { 407 | 408 | if ([output isKindOfClass: [AVCaptureVideoDataOutput class]] && _delegateMap.cameraCaptureVideoSampleBuffer){ 409 | //[self extractInfoFromSampleBuffer:sampleBuffer]; 410 | [_delegate cameraCaptureVideoSampleBuffer:sampleBuffer]; 411 | } else if ([output isKindOfClass: [AVCaptureAudioDataOutput class]] && _delegateMap.cameraCaptureAudioSampleBuffer) { 412 | [_delegate cameraCaptureAudioSampleBuffer:sampleBuffer]; 413 | } 414 | } 415 | 416 | #pragma mark - AVCapturePhotoCaptureDelegate 417 | - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error 418 | { 419 | if (_delegateMap.cameraDidFinishProcessingPhoto) { 420 | [_delegate cameraDidFinishProcessingPhoto:photo withPosition:_cameraPosition error:error]; 421 | } 422 | } 423 | 424 | - (void)captureOutput:(AVCapturePhotoOutput *)output willCapturePhotoForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings 425 | { 426 | if (_delegateMap.cameraWillProcessPhoto) { 427 | [self->_delegate cameraWillProcessPhoto]; 428 | } 429 | } 430 | 431 | @end 432 | -------------------------------------------------------------------------------- /Sources/SYCameraConfig.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYCameraConfig.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/5/26. 6 | // 7 | 8 | #import 9 | #import 10 | #import "SYPreviewView.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | 15 | typedef NS_ENUM(NSUInteger, SYDeviceType) { 16 | SYSingleDevice, // 单摄 17 | SYDualDevice, // 前后摄 18 | }; 19 | 20 | typedef NS_ENUM(NSUInteger, SYSessionSetupResult) { 21 | SYSessionSetupSuccess, // 成功 22 | SYSessionSetupNotAuthorized, // 无权限 23 | SYSessionSetupConfigurationFailed, // 配置失败 24 | SYSessionSetupMultiCamNotSupported, // 不支持双摄 25 | }; 26 | 27 | 28 | typedef NS_ENUM(NSUInteger, SYCameraMode) { 29 | SYPhotoMode, // 拍照模式 30 | SYVideoMode, // 录制模式 31 | }; 32 | 33 | typedef NS_ENUM(NSUInteger, SYRecordStatus) { 34 | SYRecordNormal, 35 | SYRecordPause, 36 | SYRecording, 37 | }; 38 | 39 | @interface SYCameraConfig : NSObject 40 | 41 | @property (nonatomic, assign) SYDeviceType type; 42 | @property (nonatomic, assign) SYCameraMode mode; 43 | @property (nonatomic, assign) AVCaptureDevicePosition devicePosition; 44 | @property (nonatomic, copy, nullable) AVCaptureSessionPreset sessionPreset; 45 | @property (nonatomic, copy, nullable) NSDictionary *previewViewRects; 46 | 47 | 48 | - (instancetype)init; 49 | 50 | /// 初始化相机配置 51 | /// - Parameters: 52 | /// - devicePosition: AVCaptureDevicePosition 53 | /// - mode: 相机模式,SYCameraMode 54 | - (instancetype)initWithMode:(SYCameraMode)mode 55 | withPosition:(AVCaptureDevicePosition)devicePosition; 56 | 57 | /// 初始化相机配置 58 | /// - Parameters: 59 | /// - devicePosition: AVCaptureDevicePosition 60 | /// - mode: 相机模式,SYCameraMode 61 | /// - type: 摄像头类型 SYDeviceType 62 | - (instancetype)initWithMode:(SYCameraMode)mode 63 | withType:(SYDeviceType)type 64 | withPosition:(AVCaptureDevicePosition)devicePosition; 65 | @end 66 | 67 | NS_ASSUME_NONNULL_END 68 | -------------------------------------------------------------------------------- /Sources/SYCameraConfig.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYCameraConfig.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/5/26. 6 | // 7 | 8 | #import "SYCameraConfig.h" 9 | #import 10 | 11 | @implementation SYCameraConfig 12 | 13 | - (instancetype)init 14 | { 15 | self = [self initWithMode:SYPhotoMode withPosition:AVCaptureDevicePositionBack]; 16 | return self; 17 | } 18 | 19 | - (instancetype)initWithMode:(SYCameraMode)mode 20 | withPosition:(AVCaptureDevicePosition)devicePosition 21 | { 22 | self = [self initWithMode:mode withType:SYSingleDevice withPosition:devicePosition]; 23 | return self; 24 | } 25 | 26 | - (instancetype)initWithMode:(SYCameraMode)mode 27 | withType:(SYDeviceType)type 28 | withPosition:(AVCaptureDevicePosition)devicePosition 29 | { 30 | self = [super init]; 31 | if (self) { 32 | self.mode = mode; 33 | self.type = type; 34 | self.devicePosition = devicePosition; 35 | } 36 | return self; 37 | } 38 | 39 | 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /Sources/SYCameraManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYCameraManager.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/5/25. 6 | // 7 | 8 | #import 9 | #import 10 | #import 11 | #import "SYCameraConfig.h" 12 | @class SYCameraManager; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | 17 | 18 | @protocol SYCameraManagerDelegate 19 | 20 | @required 21 | 22 | 23 | /// 相机配置结果 24 | /// - Parameters: 25 | /// - result: SYSessionSetupResult 26 | /// - manager: SYCameraManager 27 | - (void)cameraSessionSetupResult:(SYSessionSetupResult)result withManager:(SYCameraManager *)manager; 28 | 29 | /// 相机已启动 30 | /// - Parameter manager: SYCameraManager 31 | - (void)cameraDidStarted:(SYCameraManager *)manager; 32 | 33 | 34 | /// 相机已停止 35 | /// - Parameter manager: SYCameraManager 36 | - (void)cameraDidStoped:(SYCameraManager *)manager; 37 | 38 | 39 | /// 相机拍照结果 40 | /// - Parameters: 41 | /// - image: 图片 42 | /// - metaData: 摘要 43 | /// - manager: SYCameraManager 44 | /// - error: 错误 45 | - (void)cameraDidFinishProcessingPhoto:(UIImage *_Nullable)image 46 | withMetaData:(NSDictionary *_Nullable)metaData 47 | withManager:(SYCameraManager *)manager 48 | withError:(NSError *_Nullable)error; 49 | 50 | /// 相机录制结果 51 | /// - Parameters: 52 | /// - outputURL: 保存路径 53 | /// - manager: SYCameraManager 54 | /// - error: error 55 | - (void)cameraDidFinishProcessingVideo:(NSURL *_Nullable)outputURL 56 | withManager:(SYCameraManager *)manager 57 | withError:(NSError *_Nullable)error; 58 | @optional 59 | 60 | /// 输出采样的数据 61 | /// - Parameter sampleBuffer: CMSampleBufferRef 62 | - (void)cameraDidOutputSampleBuffer:(CMSampleBufferRef _Nullable)sampleBuffer; 63 | 64 | 65 | /// 相机设备切换改变 66 | /// - Parameters: 67 | /// - backFacing: 设备位置 68 | /// - manager: SYCameraManager 69 | - (void)cameraDidChangedPosition:(BOOL)backFacing 70 | withManager:(SYCameraManager *)manager; 71 | 72 | 73 | /// 相机焦点调整改变 74 | /// - Parameters: 75 | /// - value: 位置 76 | /// - mode: 模式 77 | /// - manager: SYCameraManager 78 | - (void)cameraDidChangedFocus:(CGPoint)value mode:(AVCaptureFocusMode)mode 79 | withManager:(SYCameraManager *)manager; 80 | 81 | 82 | /// 相机焦距调整改变 83 | /// - Parameters: 84 | /// - value: 焦距 85 | /// - manager: SYCameraManager 86 | - (void)cameraDidChangedZoom:(CGFloat)value 87 | withManager:(SYCameraManager *)manager; 88 | 89 | 90 | /// 相机曝光值调整改变 91 | /// - Parameters: 92 | /// - value: 曝光值 93 | /// - mode: 模式 94 | /// - manager: SYCameraManager 95 | - (void)cameraDidChangedExposure:(CGPoint)value mode:(AVCaptureExposureMode)mode 96 | withManager:(SYCameraManager *)manager; 97 | 98 | /// 相机闪光灯状态改变 99 | /// - Parameters: 100 | /// - mode: 模式 101 | /// - manager: SYCameraManager 102 | - (void)camerahDidChangedFlash:(AVCaptureFlashMode)mode 103 | withManager:(SYCameraManager *)manager; 104 | 105 | /// 相机 ev 值改变 106 | /// - Parameters: 107 | /// - value: ev 值 108 | /// - manager: SYCameraManager 109 | - (void)cameraDidChangedEV:(CGFloat)value 110 | withManager:(SYCameraManager *)manager; 111 | 112 | /// 即将拍照 113 | /// - Parameter manager: SYCameraManager 114 | - (void)cameraWillCapturePhoto:(SYCameraManager *)manager; 115 | 116 | 117 | /// 相机模式改变 118 | /// - Parameters: 119 | /// - mode: 模式 120 | /// - manager: SYCameraManager 121 | - (void)cameraDidChangeMode:(SYCameraMode)mode 122 | withManager:(SYCameraManager *)manager; 123 | 124 | /// 相机录制状态改变 125 | /// - Parameters: 126 | /// - status: 录制状态 127 | /// - manager: SYCameraManager 128 | - (void)cameraRecordStatusDidChange:(SYRecordStatus)status 129 | withManager:(SYCameraManager *)manager; 130 | 131 | 132 | @end 133 | 134 | @interface SYCameraManager : NSObject 135 | 136 | @property (nullable, nonatomic, weak) id delegate; 137 | @property (nonatomic, assign, readonly) SYSessionSetupResult result; 138 | @property (nonatomic, assign, readonly) SYRecordStatus recordStatus; 139 | @property (nonatomic, assign, readonly) SYCameraMode cameraMode; 140 | 141 | @property (nonatomic, assign, readonly) CGFloat zoom; 142 | @property (nonatomic, assign, readonly) CGFloat minZoom; 143 | @property (nonatomic, assign, readonly) CGFloat maxZoom; 144 | 145 | 146 | /// 是否支持多摄像头 147 | + (BOOL)isMultiCamSupported; 148 | 149 | /// 创建相机 150 | /// - Parameters: 151 | /// - config: SYCameraConfig 152 | /// - completion: 创建回调 153 | - (void)requestCameraWithConfig:(SYCameraConfig *)config withCompletion:(void(^)(SYSessionSetupResult result))completion; 154 | 155 | 156 | /// 将预览视图添加到 View 上 157 | /// - Parameter view: 展示的 View 158 | - (void)addPreviewToView:(UIView *)view; 159 | 160 | /// 启动相机流 161 | - (void)startCapture; 162 | 163 | /// 停止相机流 164 | - (void)stopCapture; 165 | 166 | /// 切换相机前后置 167 | /// - Parameter position: AVCaptureDevicePosition 168 | - (void)changeCameraPosition:(AVCaptureDevicePosition)position; 169 | 170 | 171 | /// 切换模式 172 | /// - Parameters: 173 | /// - mode: SYCameraMode 174 | /// - preset: AVCaptureSessionPreset 175 | - (void)changeCameraMode:(SYCameraMode)mode 176 | withSessionPreset:(nullable AVCaptureSessionPreset)preset; 177 | 178 | 179 | /// 调整相机焦距 180 | /// - Parameters: 181 | /// - point: 焦距位置 182 | /// - mode: 模式 183 | - (void)focusWithPoint:(CGPoint)point mode:(AVCaptureFocusMode)mode; 184 | 185 | 186 | /// 调整相机曝光 187 | /// - Parameters: 188 | /// - point: 曝光位置 189 | /// - mode: 模式 190 | - (void)exposureWithPoint:(CGPoint)point mode:(AVCaptureExposureMode)mode; 191 | 192 | /// 拍照 193 | - (void)takePhoto; 194 | 195 | /// 开始录屏 196 | - (void)startRecord; 197 | 198 | /// 结束录屏 199 | - (void)stopRecord; 200 | 201 | 202 | /// 调整缩放值 203 | /// - Parameters: 204 | /// - zoom: value 205 | /// - animated: 是否带动画 206 | - (void)setZoom:(CGFloat)zoom withAnimated:(BOOL)animated; 207 | 208 | @end 209 | 210 | NS_ASSUME_NONNULL_END 211 | -------------------------------------------------------------------------------- /Sources/SYCameraManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYCameraManager.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/5/25. 6 | // 7 | 8 | #import "SYCameraManager.h" 9 | #import "SYSingleCamera.h" 10 | #import "SYPreviewView.h" 11 | #import "SYRecorder.h" 12 | #import "UIImage+SYImage.h" 13 | #import "SYLog.h" 14 | #import "SYMultiCamera.h" 15 | 16 | typedef struct SYCameraManagerDelegateMap { 17 | unsigned int cameraDidStarted : 1; 18 | unsigned int cameraDidStoped : 1; 19 | unsigned int cameraDidOutputSampleBuffer : 1; 20 | unsigned int cameraDidFinishProcessingPhoto : 1; 21 | unsigned int changedPosition : 1; 22 | unsigned int changedFocus : 1; 23 | unsigned int changedZoom : 1; 24 | unsigned int changedExposure : 1; 25 | unsigned int changedFlash : 1; 26 | unsigned int changedEV : 1; 27 | unsigned int cameraWillCapturePhoto : 1; 28 | unsigned int cameraDidChangeMode: 1; 29 | unsigned int cameraDidFinishProcessingVideo: 1; 30 | unsigned int cameraRecordStatusDidChange: 1; 31 | unsigned int cameraSessionSetupResult: 1; 32 | } SYCameraManagerDelegateMap; 33 | 34 | static NSString * TAG = @"SYCameraManager"; 35 | 36 | @interface SYCameraManager () { 37 | SYBaseCamera *_camera; 38 | NSDictionary *_previewViews; 39 | SYCameraManagerDelegateMap _delegateCache; 40 | SYDeviceType _deviceType; 41 | SYRecorder *_recorder; 42 | CGSize _sampleBufferSize; 43 | BOOL _audioProcessing; 44 | NSMutableDictionary *_previewViewRects; 45 | NSMutableDictionary *_multiPhotos; 46 | } 47 | 48 | @property (nonatomic, assign, readwrite) SYSessionSetupResult result; 49 | @property (nonatomic, assign, readwrite) SYRecordStatus recordStatus; 50 | 51 | @property (nonatomic, assign, readwrite) CGFloat zoom; 52 | @property (nonatomic, assign, readwrite) CGFloat minZoom; 53 | @property (nonatomic, assign, readwrite) CGFloat maxZoom; 54 | 55 | @end 56 | 57 | @implementation SYCameraManager 58 | 59 | + (BOOL)isMultiCamSupported { 60 | if (@available(iOS 13.0, *)) { 61 | return AVCaptureMultiCamSession.isMultiCamSupported; 62 | } else { 63 | return NO; 64 | } 65 | } 66 | 67 | - (instancetype)init { 68 | self = [super init]; 69 | if (self) { 70 | _sampleBufferSize = CGSizeMake(1080, 1920); 71 | _deviceType = SYSingleDevice; 72 | _recordStatus = SYRecordNormal; 73 | _multiPhotos = [NSMutableDictionary dictionary]; 74 | _previewViewRects = [NSMutableDictionary dictionary]; 75 | } 76 | return self; 77 | } 78 | 79 | - (void)requestCameraWithConfig:(SYCameraConfig *)config 80 | withCompletion:(void(^)(SYSessionSetupResult result))completion { 81 | self.result = SYSessionSetupSuccess; 82 | AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; 83 | if (status != AVAuthorizationStatusAuthorized) { 84 | self.result = SYSessionSetupNotAuthorized; 85 | completion(self.result); 86 | return; 87 | } 88 | 89 | if (config.mode == SYVideoMode) { 90 | status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; 91 | if (status != AVAuthorizationStatusAuthorized) { 92 | self.result = SYSessionSetupNotAuthorized; 93 | completion(self.result); 94 | return; 95 | } 96 | } 97 | _deviceType = config.type; 98 | if (config.previewViewRects) { 99 | _previewViewRects = [NSMutableDictionary dictionaryWithDictionary:config.previewViewRects]; 100 | } 101 | [self configurePreviewView]; 102 | 103 | if (_deviceType == SYDualDevice && config.mode == SYVideoMode) { 104 | SYLog(TAG, "requestCameraWithConfig 双摄暂不支持录制模式"); 105 | self.result = SYSessionSetupConfigurationFailed; 106 | completion(self.result); 107 | } 108 | 109 | if (_deviceType == SYDualDevice && ![SYCameraManager isMultiCamSupported]) { 110 | SYLog(TAG, "requestCameraWithConfig 该设备不支持双摄模式"); 111 | self.result = SYSessionSetupMultiCamNotSupported; 112 | completion(self.result); 113 | return; 114 | } 115 | _camera = [SYBaseCamera createCameraWithConfig:config withDelegate:self]; 116 | if (_camera) { 117 | completion(self.result); 118 | } else { 119 | self.result = SYSessionSetupConfigurationFailed; 120 | completion(self.result); 121 | } 122 | } 123 | 124 | - (void)configurePreviewView { 125 | switch (_deviceType) { 126 | case SYSingleDevice: { 127 | SYPreviewView *previewView = [SYPreviewView new]; 128 | _previewViews = @{ 129 | @(AVCaptureDevicePositionFront): previewView, 130 | @(AVCaptureDevicePositionBack): previewView, 131 | }; 132 | break; 133 | } 134 | case SYDualDevice: { 135 | SYPreviewView *frontPreviewView = [SYPreviewView new]; 136 | SYPreviewView *backPreviewView = [SYPreviewView new]; 137 | _previewViews = @{ 138 | @(AVCaptureDevicePositionFront): frontPreviewView, 139 | @(AVCaptureDevicePositionBack): backPreviewView, 140 | }; 141 | break; 142 | } 143 | 144 | } 145 | } 146 | 147 | - (void)changeCameraMode:(SYCameraMode)mode withSessionPreset:(AVCaptureSessionPreset)preset { 148 | if (!_camera) { 149 | SYLog(TAG, "changeCameraMode camera is nil"); 150 | return; 151 | } 152 | 153 | if (@available(iOS 13.0, *)) { 154 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 155 | SYLog(TAG, "changeCameraMode 双摄暂不支持切换模式"); 156 | return; 157 | } 158 | } 159 | 160 | AVCaptureSessionPreset sessionPreset; ; 161 | if (preset == nil) { 162 | if (mode == SYPhotoMode) { 163 | sessionPreset = AVCaptureSessionPresetPhoto; 164 | } else { 165 | sessionPreset = AVCaptureSessionPresetHigh; 166 | } 167 | } else { 168 | sessionPreset = preset; 169 | } 170 | 171 | [_camera changeCameraMode:mode withSessionPreset:sessionPreset]; 172 | } 173 | 174 | - (void)addPreviewToView:(UIView *)view { 175 | switch (_deviceType) { 176 | case SYSingleDevice: { 177 | [self addSinglePreviewViewToView:view]; 178 | break; 179 | } 180 | case SYDualDevice: { 181 | [self addDualPreviewViewToView:view]; 182 | break; 183 | } 184 | } 185 | } 186 | 187 | - (void)addSinglePreviewViewToView:(UIView *)view { 188 | SYPreviewView *previewView = _previewViews[@(_camera.cameraPosition)]; 189 | if (previewView.superview) { 190 | [previewView removeFromSuperview]; 191 | } 192 | [view addSubview:previewView]; 193 | 194 | // 有性能优化,精准控制布局 195 | previewView.translatesAutoresizingMaskIntoConstraints = NO; 196 | [view addConstraints:@[ 197 | [NSLayoutConstraint constraintWithItem:previewView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeLeading multiplier:1 constant:0], 198 | [NSLayoutConstraint constraintWithItem:previewView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTrailing multiplier:1 constant:0], 199 | [NSLayoutConstraint constraintWithItem:previewView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeBottom multiplier:1 constant:0], 200 | [NSLayoutConstraint constraintWithItem:previewView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeTop multiplier:1 constant:0], 201 | ]]; 202 | } 203 | 204 | - (void)addDualPreviewViewToView:(UIView *)view { 205 | SYPreviewView *backPreviewView = _previewViews[@(AVCaptureDevicePositionBack)]; 206 | SYPreviewView *frontPreviewView = _previewViews[@(AVCaptureDevicePositionFront)]; 207 | if (backPreviewView.superview) { 208 | [backPreviewView removeFromSuperview]; 209 | } 210 | if (frontPreviewView.superview) { 211 | [frontPreviewView removeFromSuperview]; 212 | } 213 | 214 | switch (_camera.cameraPosition) { 215 | case AVCaptureDevicePositionFront: { 216 | [view addSubview:frontPreviewView]; 217 | [view addSubview:backPreviewView]; 218 | break; 219 | } 220 | default: { 221 | [view addSubview:backPreviewView]; 222 | [view addSubview:frontPreviewView]; 223 | break; 224 | } 225 | } 226 | 227 | CGRect backRect; 228 | CGRect frontRect; 229 | 230 | if (_previewViewRects[@(AVCaptureDevicePositionBack)]) { 231 | backRect = [_previewViewRects[@(AVCaptureDevicePositionBack)] CGRectValue]; 232 | } else { 233 | backRect = CGRectMake(0, 0, 1, 0.5); 234 | _previewViewRects[@(AVCaptureDevicePositionBack)] = [NSValue valueWithCGRect:backRect]; 235 | } 236 | 237 | if (_previewViewRects[@(AVCaptureDevicePositionFront)]) { 238 | frontRect = [_previewViewRects[@(AVCaptureDevicePositionFront)] CGRectValue]; 239 | } else { 240 | frontRect = CGRectMake(0, 0.5, 1, 0.5); 241 | _previewViewRects[@(AVCaptureDevicePositionFront)] = [NSValue valueWithCGRect:frontRect]; 242 | } 243 | 244 | 245 | 246 | backPreviewView.translatesAutoresizingMaskIntoConstraints = NO; 247 | [view addConstraints:@[ 248 | [NSLayoutConstraint constraintWithItem:backPreviewView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:(backRect.origin.x + backRect.size.width / 2) / 0.5 constant:0], 249 | [NSLayoutConstraint constraintWithItem:backPreviewView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterY multiplier:(backRect.origin.y + backRect.size.height / 2) / 0.5 constant:0], 250 | [NSLayoutConstraint constraintWithItem:backPreviewView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeWidth multiplier:backRect.size.width constant:0], 251 | [NSLayoutConstraint constraintWithItem:backPreviewView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeHeight multiplier:backRect.size.height constant:0], 252 | ]]; 253 | 254 | frontPreviewView.translatesAutoresizingMaskIntoConstraints = NO; 255 | [view addConstraints:@[ 256 | [NSLayoutConstraint constraintWithItem:frontPreviewView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:(frontRect.origin.x + frontRect.size.width / 2 ) / 0.5 constant:0], 257 | [NSLayoutConstraint constraintWithItem:frontPreviewView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterY multiplier:(frontRect.origin.y + frontRect.size.height / 2) / 0.5 constant:0], 258 | [NSLayoutConstraint constraintWithItem:frontPreviewView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeWidth multiplier:frontRect.size.width constant:0], 259 | [NSLayoutConstraint constraintWithItem:frontPreviewView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeHeight multiplier:frontRect.size.height constant:0], 260 | ]]; 261 | 262 | } 263 | 264 | - (void)setDelegate:(id)delegate { 265 | _delegate = delegate; 266 | _delegateCache.cameraDidStarted = [delegate respondsToSelector:@selector(cameraDidStarted:)]; 267 | _delegateCache.cameraDidStoped = [delegate respondsToSelector:@selector(cameraDidStoped:)]; 268 | _delegateCache.cameraDidOutputSampleBuffer = [delegate respondsToSelector:@selector(cameraDidOutputSampleBuffer:)]; 269 | _delegateCache.cameraDidFinishProcessingPhoto = [delegate respondsToSelector:@selector(cameraDidFinishProcessingPhoto:withMetaData:withManager:withError:)]; 270 | _delegateCache.changedPosition = [delegate respondsToSelector:@selector(cameraDidChangedPosition:withManager:)]; 271 | _delegateCache.changedFocus = [delegate respondsToSelector:@selector(cameraDidChangedFocus:mode:withManager:)]; 272 | _delegateCache.changedZoom = [delegate respondsToSelector:@selector(cameraDidChangedZoom:withManager:)]; 273 | _delegateCache.changedExposure = [delegate respondsToSelector:@selector(cameraDidChangedExposure:mode:withManager:)]; 274 | _delegateCache.changedFlash = [delegate respondsToSelector:@selector(camerahDidChangedFlash:withManager:)]; 275 | _delegateCache.changedEV = [delegate respondsToSelector:@selector(cameraDidChangedEV:withManager:)]; 276 | _delegateCache.cameraWillCapturePhoto = [delegate respondsToSelector:@selector(cameraWillCapturePhoto:)]; 277 | _delegateCache.cameraDidChangeMode = [delegate respondsToSelector:@selector(cameraDidChangeMode:withManager:)]; 278 | _delegateCache.cameraDidFinishProcessingVideo = [delegate respondsToSelector:@selector(cameraDidFinishProcessingVideo:withManager:withError:)]; 279 | _delegateCache.cameraRecordStatusDidChange = [delegate respondsToSelector:@selector(cameraRecordStatusDidChange:withManager:)]; 280 | _delegateCache.cameraSessionSetupResult = [delegate respondsToSelector:@selector(cameraSessionSetupResult:withManager:)]; 281 | } 282 | 283 | - (void)changeCameraPosition:(AVCaptureDevicePosition)position { 284 | if (_camera == nil) { 285 | SYLog(TAG, "changeCameraPosition 相机对象为 nil"); 286 | return; 287 | } 288 | 289 | if (@available(iOS 13.0, *)) { 290 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 291 | SYLog(TAG, "changeCameraPosition 双摄暂不支持切换前后摄"); 292 | return; 293 | } 294 | } 295 | 296 | [_camera changeCameraPosition:position]; 297 | } 298 | 299 | - (void)exposureWithPoint:(CGPoint)point mode:(AVCaptureExposureMode)mode { 300 | if (_camera == nil) { 301 | SYLog(TAG, "exposureWithPoint 相机对象为 nil"); 302 | return; 303 | } 304 | 305 | if (@available(iOS 13.0, *)) { 306 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 307 | SYLog(TAG, "exposureWithPoint 双摄暂不支持切调整曝光"); 308 | return; 309 | } 310 | } 311 | 312 | [_camera exposureWithPoint:point mode:mode]; 313 | } 314 | 315 | - (void)focusWithPoint:(CGPoint)point mode:(AVCaptureFocusMode)mode { 316 | if (_camera == nil) { 317 | SYLog(TAG, "focusWithPoint 相机对象为 nil"); 318 | return; 319 | } 320 | 321 | if (@available(iOS 13.0, *)) { 322 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 323 | SYLog(TAG, "focusWithPoint 双摄暂不支持切调整焦点"); 324 | return; 325 | } 326 | } 327 | 328 | [_camera focusWithPoint:point mode:mode]; 329 | } 330 | 331 | 332 | - (void)startCapture { 333 | if (_camera == nil) { 334 | SYLog(TAG, "startCapture 相机对象为 nil"); 335 | return; 336 | } 337 | [_camera startCapture]; 338 | } 339 | 340 | - (void)stopCapture { 341 | if (_camera == nil) { 342 | SYLog(TAG, "stopCapture 相机对象为 nil"); 343 | return; 344 | } 345 | [_camera stopCapture]; 346 | } 347 | 348 | - (void)takePhoto { 349 | if (_camera == nil) { 350 | SYLog(TAG, "takePhoto 相机对象为 nil"); 351 | return; 352 | } 353 | [_camera takePhoto]; 354 | } 355 | 356 | - (void)startRecord { 357 | if (_camera == nil) { 358 | SYLog(TAG, "startRecord 相机对象为 nil"); 359 | if (_delegateCache.cameraRecordStatusDidChange) { 360 | [_delegate cameraRecordStatusDidChange:_recordStatus withManager:self]; 361 | } 362 | return; 363 | } 364 | 365 | if (@available(iOS 13.0, *)) { 366 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 367 | SYLog(TAG, "changeCameraPosition 双摄暂不支持录制"); 368 | if (_delegateCache.cameraRecordStatusDidChange) { 369 | [_delegate cameraRecordStatusDidChange:_recordStatus withManager:self]; 370 | } 371 | return; 372 | } 373 | } 374 | 375 | if (_audioProcessing) { 376 | return; 377 | } 378 | _audioProcessing = YES; 379 | __weak typeof(self)weakSelf = self; 380 | [_camera addMicrophoneWith:^{ 381 | dispatch_async(dispatch_get_main_queue(), ^{ 382 | __strong typeof(weakSelf)strongSelf = weakSelf; 383 | strongSelf->_recordStatus = SYRecording; 384 | [strongSelf realStartRecord]; 385 | strongSelf->_audioProcessing = NO; 386 | if (strongSelf->_delegateCache.cameraRecordStatusDidChange) { 387 | [strongSelf->_delegate cameraRecordStatusDidChange:strongSelf->_recordStatus withManager:strongSelf]; 388 | } 389 | }); 390 | }]; 391 | } 392 | 393 | - (void)realStartRecord { 394 | SYRecordConfig *config = [[SYRecordConfig alloc] initWithSize:_sampleBufferSize]; 395 | _recorder = [[SYRecorder alloc] initWithConfig:config]; 396 | [_recorder startRecord]; 397 | } 398 | 399 | - (void)stopRecord { 400 | if (_camera == nil) { 401 | SYLog(TAG, "stopRecord 相机对象为 nil"); 402 | if (_delegateCache.cameraRecordStatusDidChange) { 403 | [_delegate cameraRecordStatusDidChange:_recordStatus withManager:self]; 404 | } 405 | return; 406 | } 407 | 408 | if (@available(iOS 13.0, *)) { 409 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 410 | SYLog(TAG, "changeCameraPosition 双摄暂不支持录制"); 411 | if (_delegateCache.cameraRecordStatusDidChange) { 412 | [_delegate cameraRecordStatusDidChange:_recordStatus withManager:self]; 413 | } 414 | return; 415 | } 416 | } 417 | 418 | if (_audioProcessing) { 419 | if (_delegateCache.cameraRecordStatusDidChange) { 420 | [_delegate cameraRecordStatusDidChange:_recordStatus withManager:self]; 421 | } 422 | return; 423 | } 424 | _audioProcessing = YES; 425 | 426 | 427 | __weak typeof(self)weakSelf = self; 428 | [_camera removeMicrophoneWith:^{ 429 | dispatch_async(dispatch_get_main_queue(), ^{ 430 | __strong typeof(weakSelf)strongSelf = weakSelf; 431 | strongSelf->_recordStatus = SYRecordNormal; 432 | if (strongSelf->_recorder) { 433 | [strongSelf realStopRecordWithCompletion:^{}]; 434 | } 435 | strongSelf->_audioProcessing = NO; 436 | if (strongSelf->_delegateCache.cameraRecordStatusDidChange) { 437 | [strongSelf->_delegate cameraRecordStatusDidChange:strongSelf->_recordStatus withManager:self]; 438 | } 439 | }); 440 | }]; 441 | } 442 | 443 | - (void)realStopRecordWithCompletion:(void(^)(void))completion { 444 | __weak typeof(self)weakSelf = self; 445 | [_recorder stopRecordWithCompletion:^(NSURL * _Nullable outputURL, BOOL ret) { 446 | __strong typeof(weakSelf)strongSelf = weakSelf; 447 | strongSelf->_recorder = nil; 448 | dispatch_async(dispatch_get_main_queue(), ^{ 449 | completion(); 450 | }); 451 | if (strongSelf->_delegateCache.cameraDidFinishProcessingVideo) { 452 | [strongSelf->_delegate cameraDidFinishProcessingVideo:outputURL withManager:strongSelf withError:nil]; 453 | } 454 | }]; 455 | } 456 | 457 | - (void)pauseRecord { 458 | if (_camera == nil || _recorder == nil) { 459 | return; 460 | } 461 | _recordStatus = SYRecordPause; 462 | } 463 | 464 | - (void)resumeRecord { 465 | if (_camera == nil || _recorder == nil) { 466 | return; 467 | } 468 | _recordStatus = SYRecording; 469 | } 470 | 471 | - (UIImage *)imageFromPixelBuffer:(CVPixelBufferRef) pixelbuffer { 472 | CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pixelbuffer]; 473 | CIContext *context = [CIContext contextWithOptions:nil]; 474 | CGImageRef videoImage = [context createCGImage:ciImage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(pixelbuffer), CVPixelBufferGetHeight(pixelbuffer))]; 475 | UIImage *image = [UIImage imageWithCGImage:videoImage]; 476 | CGImageRelease(videoImage); 477 | return image; 478 | } 479 | 480 | - (AVCaptureVideoOrientation)convertOrientation:(UIDeviceOrientation)orientation { 481 | AVCaptureVideoOrientation videoOrientation = AVCaptureVideoOrientationPortrait; 482 | switch (orientation) { 483 | case UIDeviceOrientationPortrait: { 484 | videoOrientation = AVCaptureVideoOrientationPortrait; 485 | break; 486 | } 487 | case UIDeviceOrientationLandscapeLeft: { 488 | videoOrientation = AVCaptureVideoOrientationLandscapeRight; 489 | break; 490 | } 491 | case UIDeviceOrientationLandscapeRight: { 492 | videoOrientation = AVCaptureVideoOrientationLandscapeLeft; 493 | break; 494 | } 495 | case UIDeviceOrientationPortraitUpsideDown: { 496 | videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown; 497 | break; 498 | } 499 | default: { 500 | break; 501 | } 502 | } 503 | return videoOrientation; 504 | } 505 | 506 | - (SYCameraMode)cameraMode { 507 | if (_camera) { 508 | return _camera.mode; 509 | } else { 510 | return SYPhotoMode; 511 | } 512 | } 513 | 514 | - (CGFloat)zoom { 515 | if (!_camera) { 516 | return 1.0; 517 | } 518 | return [_camera zoom]; 519 | } 520 | 521 | - (CGFloat)minZoom { 522 | if (!_camera) { 523 | return 1.0; 524 | } 525 | return [_camera minZoom]; 526 | } 527 | 528 | - (CGFloat)maxZoom { 529 | if (!_camera) { 530 | return 1.0; 531 | } 532 | return [_camera maxZoom]; 533 | } 534 | 535 | - (void)setZoom:(CGFloat)zoom withAnimated:(BOOL)animated { 536 | if (!_camera) { 537 | SYLog(TAG, "setZoom 相机对象为 nil"); 538 | return; 539 | } 540 | if (@available(iOS 13.0, *)) { 541 | if ([_camera isKindOfClass:[SYMultiCamera class]]) { 542 | SYLog(TAG, "changeCameraPosition 双摄暂不支持调整焦距"); 543 | return; 544 | } 545 | } 546 | [_camera setZoom:zoom withAnimated:animated]; 547 | } 548 | 549 | - (void)handleSingleCameraPhoto:(AVCapturePhoto *)photo withPosition:(AVCaptureDevicePosition)position { 550 | NSData *imageData = [photo fileDataRepresentation]; 551 | if (imageData == nil) { 552 | [_delegate cameraDidFinishProcessingPhoto:nil withMetaData:nil withManager:self withError:nil]; 553 | return; 554 | } 555 | SYPreviewView *previewView = _previewViews[@(position)]; 556 | UIImage *image = [[UIImage alloc] initWithData:imageData]; 557 | CGFloat ratio = CGRectGetWidth(previewView.frame) / CGRectGetHeight(previewView.frame); 558 | UIImage *fixImage = [image fixImageWithRatio:ratio isFront:position == AVCaptureDevicePositionFront]; 559 | [_delegate cameraDidFinishProcessingPhoto:fixImage withMetaData:photo.metadata withManager:self withError:nil]; 560 | } 561 | 562 | - (void)handleDualCameraPhoto:(AVCapturePhoto *)photo withPosition:(AVCaptureDevicePosition)position { 563 | if (_camera.cameraPosition == position) { 564 | _multiPhotos = [NSMutableDictionary dictionaryWithDictionary:@{@(position): photo}]; 565 | } else { 566 | _multiPhotos[@(position)] = photo; 567 | } 568 | 569 | if (_multiPhotos.count != 2) { 570 | return; 571 | } 572 | 573 | AVCapturePhoto *backPhoto = _multiPhotos[@(AVCaptureDevicePositionBack)]; 574 | AVCapturePhoto *frontPhoto = _multiPhotos[@(AVCaptureDevicePositionFront)]; 575 | [_multiPhotos removeAllObjects]; 576 | 577 | NSData *backImgData = [backPhoto fileDataRepresentation]; 578 | NSData *frontImgData = [frontPhoto fileDataRepresentation]; 579 | 580 | if (!backImgData || !frontImgData) { 581 | [_delegate cameraDidFinishProcessingPhoto:nil withMetaData:nil withManager:self withError:nil]; 582 | return; 583 | } 584 | 585 | UIImage *backImage = [[UIImage alloc] initWithData:backImgData]; 586 | UIImage *frontImage = [[UIImage alloc] initWithData:frontImgData]; 587 | SYPreviewView *backView = _previewViews[@(AVCaptureDevicePositionBack)]; 588 | SYPreviewView *frontView = _previewViews[@(AVCaptureDevicePositionFront)]; 589 | CGFloat backRatio = CGRectGetWidth(backView.frame) / CGRectGetHeight(backView.frame); 590 | CGFloat frontRatio = CGRectGetWidth(frontView.frame) / CGRectGetHeight(frontView.frame); 591 | UIImage *backFixImage = [backImage fixImageWithRatio:backRatio isFront:NO]; 592 | UIImage *frontFixImage = [frontImage fixImageWithRatio:frontRatio isFront:YES]; 593 | UIImage *productImage; 594 | if (position == AVCaptureDevicePositionFront) { 595 | productImage = [UIImage stitchDualImages:@[backFixImage, frontFixImage] andRects:@[_previewViewRects[@(AVCaptureDevicePositionBack)], _previewViewRects[@(AVCaptureDevicePositionFront)]]]; 596 | } else { 597 | productImage = [UIImage stitchDualImages:@[frontFixImage, backFixImage] andRects:@[_previewViewRects[@(AVCaptureDevicePositionFront)], _previewViewRects[@(AVCaptureDevicePositionBack)]]]; 598 | } 599 | [_delegate cameraDidFinishProcessingPhoto:productImage withMetaData:nil withManager:self withError:nil]; 600 | } 601 | 602 | #pragma mark - SYCameraDelegate 603 | 604 | - (void)cameraDidFinishProcessingPhoto:(AVCapturePhoto *)photo withPosition:(AVCaptureDevicePosition)position error:(NSError *)error { 605 | if (_delegateCache.cameraDidFinishProcessingPhoto) { 606 | if (error) { 607 | [_delegate cameraDidFinishProcessingPhoto:nil withMetaData:nil withManager:self withError:error]; 608 | [_multiPhotos removeAllObjects]; 609 | return; 610 | } 611 | 612 | if (photo == nil) { 613 | [_delegate cameraDidFinishProcessingPhoto:nil withMetaData:nil withManager:self withError:error]; 614 | [_multiPhotos removeAllObjects]; 615 | return; 616 | } 617 | 618 | switch (_deviceType) { 619 | case SYSingleDevice: { 620 | [self handleSingleCameraPhoto:photo withPosition:position]; 621 | break; 622 | } 623 | case SYDualDevice: { 624 | [self handleDualCameraPhoto:photo withPosition:position]; 625 | break; 626 | } 627 | } 628 | } 629 | } 630 | 631 | - (void)cameraDidStarted { 632 | if (_delegateCache.cameraDidStarted) { 633 | [_delegate cameraDidStarted:self]; 634 | } 635 | } 636 | 637 | - (void)cameraDidStoped { 638 | if (_delegateCache.cameraDidStoped) { 639 | [_delegate cameraDidStoped:self]; 640 | } 641 | } 642 | 643 | - (void)cameraCaptureVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer { 644 | if (_camera && _recorder && _camera.mode == SYVideoMode && _recordStatus == SYRecording) { 645 | [_recorder appendVideo:sampleBuffer]; 646 | } 647 | if (_delegateCache.cameraDidOutputSampleBuffer) { 648 | [_delegate cameraDidOutputSampleBuffer:sampleBuffer]; 649 | } 650 | } 651 | 652 | - (void)cameraCaptureAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer { 653 | if (_camera && _recorder && _camera.mode == SYVideoMode && _recordStatus == SYRecording) { 654 | [_recorder appendAudio:sampleBuffer]; 655 | } 656 | } 657 | 658 | - (void)cameraDidChangePosition:(BOOL)backFacing { 659 | if (_delegateCache.changedPosition) { 660 | [_delegate cameraDidChangedPosition:backFacing withManager:self]; 661 | } 662 | } 663 | 664 | - (void)cameraDidChangeFocus:(CGPoint)value mode:(AVCaptureFocusMode)mode { 665 | if (_delegateCache.changedFocus) { 666 | [_delegate cameraDidChangedFocus:value mode:mode withManager:self]; 667 | } 668 | } 669 | 670 | - (void)cameraDidChangeZoom:(CGFloat)value { 671 | if (_delegateCache.changedZoom) { 672 | [_delegate cameraDidChangedZoom:value withManager:self]; 673 | } 674 | } 675 | 676 | - (void)cameraDidChangeExposure:(CGPoint)value mode:(AVCaptureExposureMode)mode { 677 | if (_delegateCache.changedExposure) { 678 | [_delegate cameraDidChangedExposure:value mode:mode withManager:self]; 679 | } 680 | } 681 | 682 | - (void)camerahDidChangeFlash:(AVCaptureFlashMode)mode { 683 | if (_delegateCache.changedFlash) { 684 | [_delegate camerahDidChangedFlash:mode withManager:self]; 685 | } 686 | } 687 | 688 | - (void)cameraDidChangeEV:(CGFloat)value { 689 | if (_delegateCache.changedEV) { 690 | [_delegate cameraDidChangedEV:value withManager:self]; 691 | } 692 | } 693 | 694 | - (void)cameraWillProcessPhoto { 695 | if (_delegateCache.cameraWillCapturePhoto) { 696 | [_delegate cameraWillCapturePhoto:self]; 697 | } 698 | } 699 | 700 | - (void)cameraDidChangeMode:(SYCameraMode)mode { 701 | if (_delegateCache.cameraDidChangeMode) { 702 | [_delegate cameraDidChangeMode:mode withManager:self]; 703 | } 704 | } 705 | 706 | - (SYPreviewView *)getVideoPreviewForPosition:(AVCaptureDevicePosition)position { 707 | return _previewViews[@(position)]; 708 | } 709 | 710 | - (void)cameraSessionSetupResult:(SYSessionSetupResult)result { 711 | self.result = result; 712 | } 713 | 714 | 715 | 716 | @end 717 | -------------------------------------------------------------------------------- /Sources/SYLog.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYLog.h 3 | // Pods 4 | // 5 | // Created by 马陈爽 on 2024/6/26. 6 | // 7 | 8 | #ifndef SYLog_h 9 | #define SYLog_h 10 | 11 | #ifdef DEBUG 12 | #define SYLog(tag, fmt, ...) NSLog((@"[SwiftyCamera][%@] " fmt), tag, ##__VA_ARGS__) 13 | #else 14 | #define SYLog(tag, fmt, ...) do { } while (0) 15 | #endif 16 | 17 | #endif /* SYLog_h */ 18 | -------------------------------------------------------------------------------- /Sources/SYMultiCamera.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYMultiCamera.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/7/1. 6 | // 7 | 8 | #import "SYBaseCamera.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | API_AVAILABLE(ios(13.0), macCatalyst(14.0), tvos(17.0)) API_UNAVAILABLE(macos, visionos) API_UNAVAILABLE(watchos) 13 | @interface SYMultiCamera : SYBaseCamera 14 | 15 | 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /Sources/SYMultiCamera.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYMultiCamera.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/7/1. 6 | // 7 | 8 | #import "SYMultiCamera.h" 9 | #import "SYLog.h" 10 | 11 | static NSString *TAG = @"SYMultiCamera"; 12 | 13 | @interface SYMultiCamera () { 14 | AVCaptureDevice *_frontDevice; 15 | AVCaptureDevice *_backDevice; 16 | AVCaptureDeviceInput *_frontVideoInput; 17 | AVCaptureDeviceInput *_backVideoInput; 18 | AVCaptureVideoDataOutput *_frontVideoOutput; 19 | AVCaptureVideoDataOutput *_backVideoOutput; 20 | AVCapturePhotoOutput *_frontPhotoOutput; 21 | AVCapturePhotoOutput *_backPhotoOutput; 22 | } 23 | 24 | @end 25 | 26 | @implementation SYMultiCamera 27 | 28 | - (void)dealloc { 29 | [_frontVideoOutput setSampleBufferDelegate:nil queue:nil]; 30 | [_backVideoOutput setSampleBufferDelegate:nil queue:nil]; 31 | } 32 | 33 | - (void)setupCaptureSession { 34 | self.session = [[AVCaptureMultiCamSession alloc] init]; 35 | if (self.delegateMap.getVideoPreviewForPosition) { 36 | SYPreviewView *backView = [self.delegate getVideoPreviewForPosition:AVCaptureDevicePositionBack]; 37 | SYPreviewView *frontView = [self.delegate getVideoPreviewForPosition:AVCaptureDevicePositionFront]; 38 | 39 | [backView.previewLayer setSessionWithNoConnection:self.session]; 40 | [frontView.previewLayer setSessionWithNoConnection:self.session]; 41 | } 42 | 43 | } 44 | 45 | - (BOOL)setupCameraDevice { 46 | SYLog(TAG, "setupCameraDevice"); 47 | _frontDevice = [self fetchCameraDeviceWithPosition:AVCaptureDevicePositionFront]; 48 | _backDevice = [self fetchCameraDeviceWithPosition:AVCaptureDevicePositionBack]; 49 | if (_frontDevice && _backDevice) { 50 | return YES; 51 | } else { 52 | return NO; 53 | } 54 | } 55 | 56 | - (void)setupVideoDeviceInput { 57 | SYLog(TAG, "setupVideoDeviceInput"); 58 | 59 | NSError *error = nil; 60 | AVCaptureDeviceInput *frontVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_frontDevice error:&error]; 61 | 62 | if (error) { 63 | SYLog(TAG, "setupVideoDeviceInput initWithDevice frontVideoInput failure,error = %@", error.description); 64 | if (self.delegateMap.cameraSessionSetupResult) { 65 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 66 | } 67 | return; 68 | } 69 | 70 | AVCaptureDeviceInput *backVideoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_backDevice error:&error]; 71 | 72 | if (error) { 73 | SYLog(TAG, "setupVideoDeviceInput initWithDevice backVideoInput failure,error = %@", error.description); 74 | if (self.delegateMap.cameraSessionSetupResult) { 75 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 76 | } 77 | return; 78 | } 79 | 80 | if (_frontVideoInput) { 81 | [self.session removeInput:_frontVideoInput]; 82 | } 83 | 84 | if ([self.session canAddInput:frontVideoInput]) { 85 | [self.session addInputWithNoConnections:frontVideoInput]; 86 | _frontVideoInput = frontVideoInput; 87 | } else { 88 | if (frontVideoInput) { 89 | [self.session addInputWithNoConnections:_frontVideoInput]; 90 | } else { 91 | if (self.delegateMap.cameraSessionSetupResult) { 92 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 93 | } 94 | } 95 | SYLog(TAG, "setupVideoDeviceInput addFrontInput failure"); 96 | } 97 | 98 | if (_backVideoInput) { 99 | [self.session removeInput:_backVideoInput]; 100 | } 101 | 102 | if ([self.session canAddInput:backVideoInput]) { 103 | [self.session addInputWithNoConnections:backVideoInput]; 104 | _backVideoInput = backVideoInput; 105 | } else { 106 | if (_backVideoInput) { 107 | [self.session addInput:_backVideoInput]; 108 | } else { 109 | if (self.delegateMap.cameraSessionSetupResult) { 110 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 111 | } 112 | } 113 | SYLog(TAG, "setupVideoDeviceInput addBackInput failure"); 114 | } 115 | } 116 | 117 | - (void)setupVideoOutput { 118 | SYLog(TAG, "setupVideoOutput"); 119 | 120 | AVCaptureInputPort *frontDeviceVideoPort = [[_frontVideoInput portsWithMediaType:AVMediaTypeVideo sourceDeviceType:_frontDevice.deviceType sourceDevicePosition:_frontDevice.position] firstObject]; 121 | AVCaptureInputPort *backDeviceVideoPort = [[_backVideoInput portsWithMediaType:AVMediaTypeVideo sourceDeviceType:_backDevice.deviceType sourceDevicePosition:_backDevice.position] firstObject]; 122 | 123 | if (frontDeviceVideoPort == nil || backDeviceVideoPort == nil) { 124 | SYLog(TAG, "setupVideoOutput input port is nil"); 125 | if (self.delegateMap.cameraSessionSetupResult) { 126 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 127 | } 128 | } 129 | 130 | if (_frontVideoOutput == nil) { 131 | _frontVideoOutput = [[AVCaptureVideoDataOutput alloc] init]; 132 | [_frontVideoOutput setAlwaysDiscardsLateVideoFrames:NO]; 133 | [_frontVideoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; 134 | [_frontVideoOutput setSampleBufferDelegate:self queue:self.cameraProcessQueue]; 135 | if ([self.session canAddOutput:_frontVideoOutput]) { 136 | [self.session addOutputWithNoConnections:_frontVideoOutput]; 137 | } else { 138 | SYLog(TAG, "setupVideoOutput addFrontOutput failure"); 139 | if (self.delegateMap.cameraSessionSetupResult) { 140 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 141 | } 142 | } 143 | 144 | AVCaptureConnection *frontInputConnection = [[AVCaptureConnection alloc] initWithInputPorts:@[frontDeviceVideoPort] output:_frontVideoOutput]; 145 | if ([self.session canAddConnection:frontInputConnection]) { 146 | [self.session addConnection:frontInputConnection]; 147 | [frontInputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait]; 148 | [frontInputConnection setAutomaticallyAdjustsVideoMirroring:NO]; 149 | [frontInputConnection setVideoMirrored:YES]; 150 | } else { 151 | SYLog(TAG, "setupVideoOutput addFrontConnection failure"); 152 | if (self.delegateMap.cameraSessionSetupResult) { 153 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 154 | } 155 | } 156 | 157 | if (self.delegateMap.getVideoPreviewForPosition) { 158 | __block AVCaptureConnection *frontVideoPreviewLayerConnection; 159 | __weak typeof(self) weakSelf = self; 160 | dispatch_sync(dispatch_get_main_queue(), ^{ 161 | __strong typeof(weakSelf) strongSelf = weakSelf; 162 | AVCaptureVideoPreviewLayer *layer = [strongSelf.delegate getVideoPreviewForPosition:AVCaptureDevicePositionFront].previewLayer; 163 | frontVideoPreviewLayerConnection = [[AVCaptureConnection alloc] initWithInputPort:frontDeviceVideoPort videoPreviewLayer:layer]; 164 | }); 165 | if ([self.session canAddConnection:frontVideoPreviewLayerConnection]) { 166 | [self.session addConnection:frontVideoPreviewLayerConnection]; 167 | } else { 168 | SYLog(TAG, "setupVideoOutput addFrontPreviewConnection failure"); 169 | if (self.delegateMap.cameraSessionSetupResult) { 170 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 171 | } 172 | } 173 | } 174 | 175 | } 176 | 177 | if (_backVideoOutput == nil) { 178 | _backVideoOutput = [[AVCaptureVideoDataOutput alloc] init]; 179 | [_backVideoOutput setAlwaysDiscardsLateVideoFrames:NO]; 180 | [_backVideoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; 181 | [_backVideoOutput setSampleBufferDelegate:self queue:self.cameraProcessQueue]; 182 | if ([self.session canAddOutput:_backVideoOutput]) { 183 | [self.session addOutputWithNoConnections:_backVideoOutput]; 184 | } else { 185 | SYLog(TAG, "setupVideoOutput addFrontOutput failure"); 186 | if (self.delegateMap.cameraSessionSetupResult) { 187 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 188 | } 189 | } 190 | 191 | AVCaptureConnection *backInputConnection = [[AVCaptureConnection alloc] initWithInputPorts:@[backDeviceVideoPort] output:_backVideoOutput]; 192 | if ([self.session canAddConnection:backInputConnection]) { 193 | [self.session addConnection:backInputConnection]; 194 | [backInputConnection setVideoOrientation:AVCaptureVideoOrientationPortrait]; 195 | } else { 196 | SYLog(TAG, "setupVideoOutput addBackConnection failure"); 197 | if (self.delegateMap.cameraSessionSetupResult) { 198 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 199 | } 200 | } 201 | 202 | if (self.delegateMap.getVideoPreviewForPosition) { 203 | __block AVCaptureConnection *backVideoPreviewLayerConnection; 204 | __weak typeof(self) weakSelf = self; 205 | dispatch_sync(dispatch_get_main_queue(), ^{ 206 | __strong typeof(weakSelf) strongSelf = weakSelf; 207 | AVCaptureVideoPreviewLayer *layer = [strongSelf.delegate getVideoPreviewForPosition:AVCaptureDevicePositionBack].previewLayer; 208 | backVideoPreviewLayerConnection = [[AVCaptureConnection alloc] initWithInputPort:backDeviceVideoPort videoPreviewLayer:layer]; 209 | }); 210 | if ([self.session canAddConnection:backVideoPreviewLayerConnection]) { 211 | [self.session addConnection:backVideoPreviewLayerConnection]; 212 | } else { 213 | SYLog(TAG, "setupVideoOutput addBackPreviewConnection failure"); 214 | if (self.delegateMap.cameraSessionSetupResult) { 215 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | - (void)setupPhotoOutput { 223 | SYLog(TAG, "setupPhotoOutput"); 224 | if (_frontPhotoOutput == nil) { 225 | _frontPhotoOutput = [AVCapturePhotoOutput new]; 226 | [_frontPhotoOutput setHighResolutionCaptureEnabled:YES]; 227 | if ([self.session canAddOutput:_frontPhotoOutput]) { 228 | [self.session addOutput:_frontPhotoOutput]; 229 | } else { 230 | SYLog(TAG, "setupPhotoOutput addFrontOutput failure"); 231 | if (self.delegateMap.cameraSessionSetupResult) { 232 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 233 | } 234 | } 235 | } 236 | 237 | if (_backPhotoOutput == nil) { 238 | _backPhotoOutput = [AVCapturePhotoOutput new]; 239 | [_backPhotoOutput setHighResolutionCaptureEnabled:YES]; 240 | if ([self.session canAddOutput:_backPhotoOutput]) { 241 | [self.session addOutput:_backPhotoOutput]; 242 | } else { 243 | SYLog(TAG, "setupPhotoOutput addBackOutput failure"); 244 | if (self.delegateMap.cameraSessionSetupResult) { 245 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 246 | } 247 | } 248 | } 249 | } 250 | 251 | - (void)takePhoto { 252 | __weak typeof(self)weakSelf = self; 253 | dispatch_async(self.captureQueue, ^{ 254 | __strong typeof(weakSelf)strongSelf = weakSelf; 255 | 256 | if (strongSelf.mode != SYPhotoMode) { 257 | SYLog(TAG, "takePhoto 非拍照模式"); 258 | return; 259 | } 260 | 261 | if (strongSelf.cameraPosition == AVCaptureDevicePositionBack) { 262 | [strongSelf takeBackCameraPhoto]; 263 | } else if (strongSelf.cameraPosition == AVCaptureDevicePositionFront) { 264 | [strongSelf takeFrontCameraPhoto]; 265 | } 266 | }); 267 | } 268 | 269 | - (void)takeFrontCameraPhoto { 270 | NSDictionary *dict = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; 271 | AVCapturePhotoSettings *setting = [AVCapturePhotoSettings photoSettingsWithFormat:dict]; 272 | 273 | // 设置高清晰 274 | [setting setHighResolutionPhotoEnabled:YES]; 275 | 276 | if ([self->_frontDevice hasFlash]) { 277 | [setting setFlashMode:self.flashMode]; 278 | } 279 | 280 | AVCaptureConnection *frontPhotoOutputConnection = [self->_frontPhotoOutput connectionWithMediaType:AVMediaTypeVideo]; 281 | if (frontPhotoOutputConnection) { 282 | frontPhotoOutputConnection.videoMirrored = YES; 283 | } 284 | [self->_frontPhotoOutput capturePhotoWithSettings:setting delegate:self]; 285 | } 286 | 287 | - (void)takeBackCameraPhoto { 288 | NSDictionary *dict = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; 289 | AVCapturePhotoSettings *setting = [AVCapturePhotoSettings photoSettingsWithFormat:dict]; 290 | 291 | // 设置高清晰 292 | [setting setHighResolutionPhotoEnabled:YES]; 293 | 294 | if ([self->_backDevice hasFlash]) { 295 | [setting setFlashMode:self.flashMode]; 296 | } 297 | 298 | [self->_backPhotoOutput capturePhotoWithSettings:setting delegate:self]; 299 | } 300 | 301 | 302 | - (void)captureOutput:(AVCapturePhotoOutput *)output didFinishProcessingPhoto:(AVCapturePhoto *)photo error:(NSError *)error { 303 | if (!self.delegateMap.cameraDidFinishProcessingPhoto) { 304 | return; 305 | } 306 | 307 | if (output == _frontPhotoOutput) { 308 | [self.delegate cameraDidFinishProcessingPhoto:photo withPosition:AVCaptureDevicePositionFront error:error]; 309 | if (self.cameraPosition == AVCaptureDevicePositionFront) { 310 | [self takeBackCameraPhoto]; 311 | } 312 | } else if (output == _backPhotoOutput) { 313 | [self.delegate cameraDidFinishProcessingPhoto:photo withPosition:AVCaptureDevicePositionBack error:error]; 314 | if (self.cameraPosition == AVCaptureDevicePositionBack) { 315 | [self takeFrontCameraPhoto]; 316 | } 317 | } 318 | } 319 | 320 | 321 | 322 | @end 323 | -------------------------------------------------------------------------------- /Sources/SYPreviewView.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYPreviewView.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/5/25. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface SYPreviewView : UIView 14 | 15 | @property (nonnull, nonatomic, copy, readonly) AVCaptureVideoPreviewLayer *previewLayer; 16 | @property (nullable, nonatomic, strong) AVCaptureSession *session; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /Sources/SYPreviewView.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYPreviewView.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/5/25. 6 | // 7 | 8 | #import "SYPreviewView.h" 9 | 10 | @interface SYPreviewView () 11 | 12 | @property (nonnull, nonatomic, copy, readwrite) AVCaptureVideoPreviewLayer *previewLayer; 13 | 14 | @end 15 | 16 | @implementation SYPreviewView 17 | 18 | - (instancetype)init 19 | { 20 | self = [super init]; 21 | if (self) { 22 | self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; 23 | } 24 | return self; 25 | } 26 | 27 | + (Class)layerClass 28 | { 29 | return [AVCaptureVideoPreviewLayer class]; 30 | } 31 | 32 | - (AVCaptureVideoPreviewLayer *)previewLayer 33 | { 34 | return (AVCaptureVideoPreviewLayer *)self.layer; 35 | } 36 | 37 | - (void)setSession:(AVCaptureSession *)session 38 | { 39 | self.previewLayer.session = session; 40 | } 41 | 42 | - (AVCaptureSession *)session 43 | { 44 | return self.previewLayer.session; 45 | } 46 | 47 | 48 | 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Sources/SYRecordConfig.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYRecordConfig.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/8. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface SYRecordConfig : NSObject 13 | 14 | @property (nonatomic, assign) NSUInteger bitrate; 15 | @property (nonatomic, assign) NSUInteger gop; 16 | @property (nonatomic, assign) CGSize size; 17 | @property (nonatomic, assign) NSUInteger frameRate; 18 | 19 | - (instancetype)initWithSize:(CGSize)size; 20 | - (instancetype)initWithSize:(CGSize)size withBitrate:(NSUInteger)bitrate withGop:(NSUInteger)gop withFrameRate:(NSUInteger)frameRate; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Sources/SYRecordConfig.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYRecorderConfig.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/8. 6 | // 7 | 8 | #import "SYRecordConfig.h" 9 | 10 | @implementation SYRecordConfig 11 | 12 | - (instancetype)initWithSize:(CGSize)size 13 | { 14 | self = [self initWithSize:size withBitrate:3000 withGop:30 withFrameRate:30]; 15 | return self; 16 | } 17 | - (instancetype)initWithSize:(CGSize)size withBitrate:(NSUInteger)bitrate withGop:(NSUInteger)gop withFrameRate:(NSUInteger)frameRate 18 | { 19 | self = [super init]; 20 | if (self) { 21 | _bitrate = bitrate; 22 | _gop = gop; 23 | _size = size; 24 | _frameRate = frameRate; 25 | } 26 | return self; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Sources/SYRecorder.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYRecorder.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/2. 6 | // 7 | 8 | #import 9 | #import "SYRecordConfig.h" 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface SYRecorder : NSObject 15 | 16 | - (instancetype)initWithConfig:(SYRecordConfig *)config; 17 | - (void)startRecord; 18 | - (void)stopRecordWithCompletion:(void(^)(NSURL * _Nullable, BOOL))completion; 19 | - (void)pauseRecord; 20 | - (void)resumeRecord; 21 | - (void)appendVideo:(CMSampleBufferRef)sampleBuffer; 22 | - (void)appendAudio:(CMSampleBufferRef)sampleBuffer; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /Sources/SYRecorder.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYRecorder.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/2. 6 | // 7 | 8 | #import "SYRecorder.h" 9 | #import 10 | #import "SYLog.h" 11 | 12 | static NSString * TAG = @"SYRecorder"; 13 | 14 | @interface SYRecorder () 15 | { 16 | AVAssetWriter *_writer; 17 | AVAssetWriterInput *_videoWriterInput; 18 | AVAssetWriterInput *_audioWriterInput; 19 | AVAssetWriterInputPixelBufferAdaptor *_adaptor; 20 | dispatch_queue_t _recordQueue; 21 | SYRecordConfig *_config; 22 | bool _canWriting; 23 | } 24 | 25 | @end 26 | 27 | @implementation SYRecorder 28 | 29 | - (instancetype)initWithConfig:(SYRecordConfig *)config 30 | { 31 | self = [super init]; 32 | if (self) { 33 | _recordQueue = dispatch_queue_create("com.machenshuang.camera.SYRecorder", DISPATCH_QUEUE_SERIAL); 34 | _config = config; 35 | [self configure]; 36 | } 37 | return self; 38 | } 39 | 40 | - (void)startRecord 41 | { 42 | __weak typeof(self)weakSelf = self; 43 | dispatch_async(_recordQueue, ^{ 44 | __strong typeof(weakSelf)strongSelf = weakSelf; 45 | strongSelf->_canWriting = YES; 46 | SYLog(TAG, "startRecord canWriting = %d", strongSelf->_canWriting); 47 | }); 48 | } 49 | 50 | - (void)stopRecordWithCompletion:(void (^)(NSURL * _Nullable, BOOL))completion 51 | { 52 | __weak typeof(self)weakSelf = self; 53 | dispatch_async(_recordQueue, ^{ 54 | __strong typeof(weakSelf)strongSelf = weakSelf; 55 | strongSelf->_canWriting = NO; 56 | SYLog(TAG, "stopRecordWithCompletion canWriting = %d", strongSelf->_canWriting); 57 | [strongSelf->_writer finishWritingWithCompletionHandler:^{ 58 | if (strongSelf->_writer.status == AVAssetWriterStatusCompleted) { 59 | completion(strongSelf->_writer.outputURL, YES); 60 | } else { 61 | SYLog(TAG, "stopRecordWithCompletion failure, status = %ld", (long)strongSelf->_writer.status); 62 | completion(nil, NO); 63 | } 64 | }]; 65 | }); 66 | } 67 | 68 | - (void)pauseRecord 69 | { 70 | __weak typeof(self)weakSelf = self; 71 | dispatch_async(_recordQueue, ^{ 72 | __strong typeof(weakSelf)strongSelf = weakSelf; 73 | strongSelf->_canWriting = NO; 74 | SYLog(TAG, "startRecord canWriting = %d", strongSelf->_canWriting); 75 | }); 76 | } 77 | 78 | - (void)resumeRecord 79 | { 80 | __weak typeof(self)weakSelf = self; 81 | dispatch_async(_recordQueue, ^{ 82 | __strong typeof(weakSelf)strongSelf = weakSelf; 83 | strongSelf->_canWriting = YES; 84 | SYLog(TAG, "startRecord canWriting = %d", strongSelf->_canWriting); 85 | }); 86 | } 87 | 88 | - (void)configure 89 | { 90 | __weak typeof(self)weakSelf = self; 91 | dispatch_async(_recordQueue, ^{ 92 | __strong typeof(weakSelf)strongSelf = weakSelf; 93 | if (!strongSelf->_writer) { 94 | AVMediaType mediaType = AVFileTypeMPEG4; 95 | NSError *error; 96 | 97 | strongSelf->_writer = [[AVAssetWriter alloc] initWithURL:[strongSelf getURL] fileType:mediaType error:&error]; 98 | // 适合网络播放 99 | strongSelf->_writer.shouldOptimizeForNetworkUse = YES; 100 | } 101 | 102 | [strongSelf addVideoWriterInput]; 103 | [strongSelf addAudioWriterInput]; 104 | [strongSelf configureAdaptor]; 105 | SYLog(TAG, "configure"); 106 | }); 107 | 108 | } 109 | 110 | - (void)appendVideo:(CMSampleBufferRef)sampleBuffer 111 | { 112 | CFRetain(sampleBuffer); 113 | __weak typeof(self)weakSelf = self; 114 | dispatch_async(_recordQueue, ^{ 115 | __strong typeof(weakSelf)strongSelf = weakSelf; 116 | 117 | if (!strongSelf->_canWriting) { 118 | CFRelease(sampleBuffer); 119 | return; 120 | } 121 | CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); 122 | if (strongSelf->_writer.status == AVAssetWriterStatusUnknown) { 123 | [strongSelf->_writer startWriting]; 124 | [strongSelf->_writer startSessionAtSourceTime:timestamp]; 125 | CFRelease(sampleBuffer); 126 | SYLog(TAG, "appendVideo writer status is AVAssetWriterStatusUnknown"); 127 | return; 128 | } 129 | 130 | if (strongSelf->_writer.status != AVAssetWriterStatusWriting) { 131 | CFRelease(sampleBuffer); 132 | SYLog(TAG, "appendVideo writer status is %ld, error = %@", (long)strongSelf->_writer.status, strongSelf->_writer.error.description); 133 | return; 134 | } 135 | 136 | if (!strongSelf->_videoWriterInput.isReadyForMoreMediaData) { 137 | CFRelease(sampleBuffer); 138 | SYLog(TAG, "appendVideo writer status is not readyForMoreMediaData"); 139 | return; 140 | } 141 | 142 | CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); 143 | [self->_adaptor appendPixelBuffer:pixelBuffer withPresentationTime:timestamp]; 144 | CFRelease(sampleBuffer); 145 | }); 146 | } 147 | 148 | - (void)appendAudio:(CMSampleBufferRef)sampleBuffer 149 | { 150 | CFRetain(sampleBuffer); 151 | __weak typeof(self)weakSelf = self; 152 | dispatch_async(_recordQueue, ^{ 153 | __strong typeof(weakSelf)strongSelf = weakSelf; 154 | if (!strongSelf->_canWriting) { 155 | CFRelease(sampleBuffer); 156 | return; 157 | } 158 | 159 | if (strongSelf->_writer.status != AVAssetWriterStatusWriting) { 160 | CFRelease(sampleBuffer); 161 | SYLog(TAG, "appendAudio writer status is %ld", (long)strongSelf->_writer.status); 162 | return; 163 | } 164 | 165 | if (!strongSelf->_audioWriterInput.isReadyForMoreMediaData) { 166 | CFRelease(sampleBuffer); 167 | SYLog(TAG, "appendAudio writer status is not readyForMoreMediaData"); 168 | return; 169 | } 170 | 171 | [strongSelf->_audioWriterInput appendSampleBuffer:sampleBuffer]; 172 | CFRelease(sampleBuffer); 173 | }); 174 | } 175 | 176 | - (void)addVideoWriterInput 177 | { 178 | if (!_videoWriterInput) { 179 | _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:[self fetchVideoSetting]]; 180 | _videoWriterInput.expectsMediaDataInRealTime = YES; 181 | } 182 | 183 | if ([_writer canAddInput:_videoWriterInput]) { 184 | [_writer addInput:_videoWriterInput]; 185 | } else { 186 | SYLog(TAG, "addVideoWriterInput addInput failure"); 187 | } 188 | } 189 | 190 | - (void)addAudioWriterInput 191 | { 192 | if (!_audioWriterInput) { 193 | _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:[self fetchAudioSetting]]; 194 | _audioWriterInput.expectsMediaDataInRealTime = YES; 195 | } 196 | 197 | if ([_writer canAddInput:_audioWriterInput]) { 198 | [_writer addInput:_audioWriterInput]; 199 | } else { 200 | SYLog(TAG, "addAudioWriterInput addInput failure"); 201 | } 202 | } 203 | 204 | - (void)configureAdaptor 205 | { 206 | if (!_adaptor) { 207 | NSDictionary *sourcePixelBufferAttributes = @{ 208 | (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA), 209 | (id)kCVPixelBufferWidthKey: @(_config.size.width), 210 | (id)kCVPixelBufferHeightKey: @(_config.size.height), 211 | }; 212 | _adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput sourcePixelBufferAttributes:sourcePixelBufferAttributes]; 213 | } 214 | } 215 | 216 | - (NSURL *)getURL 217 | { 218 | NSString *filename = [NSString stringWithFormat:@"%ld", (long)(CFAbsoluteTimeGetCurrent() * 1000)]; 219 | NSString *path = [[NSTemporaryDirectory() stringByAppendingPathComponent:filename] stringByAppendingPathExtension:@"MP4"]; 220 | NSURL *documentURL = [[NSURL alloc] initFileURLWithPath:path]; 221 | SYLog(TAG, "getURL url = %@", documentURL.path); 222 | return documentURL; 223 | } 224 | 225 | - (NSDictionary *)fetchVideoSetting 226 | { 227 | NSMutableDictionary *compressDict = [NSMutableDictionary dictionaryWithDictionary: @{ 228 | AVVideoAverageBitRateKey: @(_config.bitrate*1024), 229 | AVVideoMaxKeyFrameIntervalKey: @(_config.gop), 230 | AVVideoExpectedSourceFrameRateKey: @30, 231 | AVVideoAllowFrameReorderingKey: @(NO), 232 | AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, 233 | }]; 234 | 235 | return @{ 236 | AVVideoCodecKey: AVVideoCodecTypeH264, 237 | AVVideoCompressionPropertiesKey: compressDict, 238 | AVVideoWidthKey: @(_config.size.width), 239 | AVVideoHeightKey: @(_config.size.height), 240 | }; 241 | 242 | } 243 | 244 | - (NSDictionary *)fetchAudioSetting 245 | { 246 | AudioChannelLayout acl; 247 | bzero(&acl, sizeof(acl)); 248 | acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; 249 | NSUInteger sampleRate = [AVAudioSession sharedInstance].sampleRate; 250 | if (sampleRate < 44100) { 251 | sampleRate = 44100; 252 | } else if (sampleRate > 48000) { 253 | sampleRate = 48000; 254 | } 255 | return @{ 256 | AVFormatIDKey: @(kAudioFormatMPEG4AAC), 257 | AVNumberOfChannelsKey: @(1), 258 | AVSampleRateKey: @(sampleRate), 259 | AVChannelLayoutKey: [NSData dataWithBytes:&acl length:sizeof(acl)], 260 | AVEncoderBitRateKey: @(64000), 261 | }; 262 | } 263 | 264 | 265 | 266 | @end 267 | -------------------------------------------------------------------------------- /Sources/SYSingleCamera.h: -------------------------------------------------------------------------------- 1 | // 2 | // SYSingleCamera.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/28. 6 | // 7 | 8 | #include "SYBaseCamera.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface SYSingleCamera : SYBaseCamera 13 | 14 | 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /Sources/SYSingleCamera.m: -------------------------------------------------------------------------------- 1 | // 2 | // SYSingleCamera.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/28. 6 | // 7 | 8 | #import "SYSingleCamera.h" 9 | #import "SYLog.h" 10 | 11 | static NSString *TAG = @"SYSingleCamera"; 12 | 13 | @interface SYSingleCamera () 14 | { 15 | AVCaptureDevice *_inputCamera; 16 | AVCaptureDeviceInput *_videoInput; 17 | AVCaptureVideoDataOutput *_videoOutput; 18 | AVCapturePhotoOutput *_photoOutput; 19 | } 20 | 21 | @end 22 | 23 | @implementation SYSingleCamera 24 | 25 | - (void)dealloc 26 | { 27 | [_videoOutput setSampleBufferDelegate:nil queue:nil]; 28 | } 29 | 30 | - (void)setupCaptureSession 31 | { 32 | self.session = [[AVCaptureSession alloc] init]; 33 | if (self.delegateMap.getVideoPreviewForPosition) { 34 | SYPreviewView *previewView = [self.delegate getVideoPreviewForPosition:self.cameraPosition]; 35 | previewView.session = self.session; 36 | } 37 | 38 | } 39 | 40 | - (BOOL)setupCameraDevice 41 | { 42 | SYLog(TAG, "configureCameraDevice"); 43 | _inputCamera = [self fetchCameraDeviceWithPosition:self.cameraPosition]; 44 | return _inputCamera != nil; 45 | } 46 | 47 | - (void)setupVideoDeviceInput 48 | { 49 | SYLog(TAG, "setupVideoDeviceInput"); 50 | if (_videoInput != nil &&_inputCamera != nil && _videoInput.device == _inputCamera) { 51 | SYLog(TAG, "setupVideoDeviceInput _videoInput.device == _inputCamera"); 52 | return; 53 | } 54 | 55 | NSError *error = nil; 56 | // 添加 input 57 | AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_inputCamera error:&error]; 58 | 59 | if (error) { 60 | SYLog(TAG, "setupVideoDeviceInput initWithDevice failure,error = %@", error.description); 61 | if (self.delegateMap.cameraSessionSetupResult) { 62 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 63 | } 64 | return; 65 | } 66 | 67 | if (_videoInput) { 68 | [self.session removeInput:_videoInput]; 69 | } 70 | if ([self.session canAddInput:videoInput]) { 71 | [self.session addInput:videoInput]; 72 | _videoInput = videoInput; 73 | } else { 74 | if (_videoInput) { 75 | [self.session addInput:_videoInput]; 76 | } else { 77 | if (self.delegateMap.cameraSessionSetupResult) { 78 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 79 | } 80 | } 81 | SYLog(TAG, "configureVideoDeviceInput addInput failure"); 82 | } 83 | } 84 | 85 | - (void)setupVideoOutput 86 | { 87 | SYLog(TAG, "setupVideoOutput"); 88 | if (_videoOutput == nil) { 89 | _videoOutput = [[AVCaptureVideoDataOutput alloc] init]; 90 | [_videoOutput setAlwaysDiscardsLateVideoFrames:NO]; 91 | [_videoOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; 92 | [_videoOutput setSampleBufferDelegate:self queue:self.cameraProcessQueue]; 93 | if ([self.session canAddOutput:_videoOutput]) { 94 | [self.session addOutput:_videoOutput]; 95 | } else { 96 | SYLog(TAG, "setupVideoOutput addOutput failure"); 97 | if (self.delegateMap.cameraSessionSetupResult) { 98 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 99 | } 100 | } 101 | } 102 | 103 | for (AVCaptureConnection *connect in _videoOutput.connections) { 104 | if ([connect isVideoMirroringSupported]) { 105 | [connect setVideoMirrored: self.cameraPosition == AVCaptureDevicePositionFront ? YES : NO]; 106 | } 107 | if ([connect isVideoOrientationSupported]) { 108 | [connect setVideoOrientation: AVCaptureVideoOrientationPortrait]; 109 | } 110 | } 111 | } 112 | 113 | - (void)setupPhotoOutput 114 | { 115 | SYLog(TAG, "setupPhotoOutput"); 116 | if (_photoOutput == nil) { 117 | _photoOutput = [AVCapturePhotoOutput new]; 118 | [_photoOutput setHighResolutionCaptureEnabled:YES]; 119 | if ([self.session canAddOutput:_photoOutput]) { 120 | [self.session addOutput:_photoOutput]; 121 | } else { 122 | SYLog(TAG, "setupPhotoOutput addOutput failure"); 123 | if (self.delegateMap.cameraSessionSetupResult) { 124 | [self.delegate cameraSessionSetupResult:SYSessionSetupConfigurationFailed]; 125 | } 126 | } 127 | } 128 | } 129 | 130 | - (void)focusWithPoint:(CGPoint)point mode:(AVCaptureFocusMode)mode 131 | { 132 | __weak typeof(self)weakSelf = self; 133 | dispatch_async(self.sessionQueue, ^{ 134 | __strong typeof(weakSelf)strongSelf = weakSelf; 135 | NSError *error; 136 | 137 | if (!strongSelf->_inputCamera) { 138 | SYLog(TAG, "focusWithPoint cameraDevice is nil"); 139 | return; 140 | } 141 | 142 | [strongSelf->_inputCamera lockForConfiguration:&error]; 143 | if (error != nil) { 144 | [strongSelf->_inputCamera unlockForConfiguration]; 145 | SYLog(TAG, "focusWithPoint failure error = %@", error.description); 146 | return; 147 | } 148 | if ([strongSelf->_inputCamera isFocusModeSupported:mode]) { 149 | [strongSelf->_inputCamera setFocusPointOfInterest:point]; 150 | [strongSelf->_inputCamera setFocusMode:mode]; 151 | } 152 | [strongSelf->_inputCamera unlockForConfiguration]; 153 | if (strongSelf.delegateMap.changedFocus) { 154 | [strongSelf.delegate cameraDidChangeFocus:point mode:mode]; 155 | } 156 | }); 157 | } 158 | 159 | - (void)exposureWithPoint:(CGPoint)point mode:(AVCaptureExposureMode)mode 160 | { 161 | __weak typeof(self)weakSelf = self; 162 | dispatch_async(self.sessionQueue, ^{ 163 | __strong typeof(weakSelf)strongSelf = weakSelf; 164 | if (!strongSelf->_inputCamera) { 165 | SYLog(TAG, "exposureWithPoint cameraDevice is nil"); 166 | return; 167 | } 168 | NSError *error; 169 | [strongSelf->_inputCamera lockForConfiguration:&error]; 170 | if (error != nil) { 171 | [strongSelf->_inputCamera unlockForConfiguration]; 172 | SYLog(TAG, "exposureWithPoint failure error = %@", error.description); 173 | return; 174 | } 175 | [strongSelf->_inputCamera setExposurePointOfInterest:point]; 176 | [strongSelf->_inputCamera setExposureMode:mode]; 177 | [strongSelf->_inputCamera unlockForConfiguration]; 178 | if (strongSelf.delegateMap.changedExposure) { 179 | [strongSelf.delegate cameraDidChangeExposure:point mode:mode]; 180 | } 181 | }); 182 | } 183 | 184 | - (void)setEv:(CGFloat)value 185 | { 186 | __weak typeof(self)weakSelf = self; 187 | dispatch_async(self.sessionQueue, ^{ 188 | __strong typeof(weakSelf)strongSelf = weakSelf; 189 | if (!strongSelf->_inputCamera) { 190 | SYLog(TAG, "setEv cameraDevice is nil"); 191 | return; 192 | } 193 | NSError *error; 194 | [strongSelf->_inputCamera lockForConfiguration:&error]; 195 | if (error != nil) { 196 | [strongSelf->_inputCamera unlockForConfiguration]; 197 | SYLog(TAG, "setEv failure error = %@", error.description); 198 | return; 199 | } 200 | CGFloat maxEV = 3.0; 201 | CGFloat minEV = -3.0; 202 | CGFloat current = (maxEV - minEV) * value + minEV; 203 | [super setEv:value]; 204 | [strongSelf->_inputCamera setExposureTargetBias:(float)current completionHandler:nil]; 205 | [strongSelf->_inputCamera unlockForConfiguration]; 206 | if (strongSelf.delegateMap.changedEV) { 207 | [strongSelf.delegate cameraDidChangeEV:value]; 208 | } 209 | }); 210 | } 211 | 212 | - (void)setZoom:(CGFloat)zoom withAnimated:(BOOL)animated 213 | { 214 | __weak typeof(self)weakSelf = self; 215 | dispatch_async(self.sessionQueue, ^{ 216 | __strong typeof(weakSelf)strongSelf = weakSelf; 217 | if (!strongSelf->_inputCamera) { 218 | SYLog(TAG, "setZoom cameraDevice is nil"); 219 | return; 220 | } 221 | CGFloat value = zoom; 222 | CGFloat minZoom = [strongSelf minZoom]; 223 | CGFloat maxZoom = [strongSelf maxZoom]; 224 | if (@available(iOS 13.0, *)) { 225 | if (strongSelf->_inputCamera.deviceType == AVCaptureDeviceTypeBuiltInTripleCamera || strongSelf->_inputCamera.deviceType == AVCaptureDeviceTypeBuiltInDualWideCamera) { 226 | value *= 2; 227 | minZoom *= 2; 228 | } 229 | } 230 | 231 | if (value < minZoom || value > maxZoom) { 232 | SYLog(TAG, "setZoom failure value = %f,minZoom = %f, maxZoom = %f", value, minZoom, maxZoom); 233 | return; 234 | } 235 | if (value == [strongSelf zoom]) { 236 | SYLog(TAG, "setZoom value equal to current"); 237 | return; 238 | } 239 | 240 | NSError *error; 241 | [strongSelf->_inputCamera lockForConfiguration:&error]; 242 | if (error != nil) { 243 | [strongSelf->_inputCamera unlockForConfiguration]; 244 | SYLog(TAG, "setZoom failure error = %@", error.description); 245 | return; 246 | } 247 | if (animated) { 248 | [strongSelf->_inputCamera rampToVideoZoomFactor:value withRate:50]; 249 | } else { 250 | [strongSelf->_inputCamera setVideoZoomFactor:value]; 251 | } 252 | [strongSelf->_inputCamera unlockForConfiguration]; 253 | if (strongSelf.delegateMap.changedZoom) { 254 | [strongSelf.delegate cameraDidChangeZoom:value]; 255 | } 256 | }); 257 | } 258 | 259 | - (CGFloat)zoom 260 | { 261 | if (!_inputCamera) { 262 | return 1; 263 | } 264 | if (@available(iOS 13.0, *)) { 265 | if (_inputCamera.deviceType == AVCaptureDeviceTypeBuiltInTripleCamera || _inputCamera.deviceType == AVCaptureDeviceTypeBuiltInDualWideCamera) { 266 | return _inputCamera.videoZoomFactor / 2.0; 267 | } 268 | } 269 | return _inputCamera.videoZoomFactor; 270 | } 271 | 272 | - (CGFloat)maxZoom 273 | { 274 | if (!_inputCamera) { 275 | return 1; 276 | } 277 | if (@available(iOS 13.0, *)) { 278 | if (_inputCamera.deviceType == AVCaptureDeviceTypeBuiltInTripleCamera || _inputCamera.deviceType == AVCaptureDeviceTypeBuiltInDualWideCamera) { 279 | return _inputCamera.maxAvailableVideoZoomFactor / 2.0; 280 | } 281 | } 282 | return _inputCamera.maxAvailableVideoZoomFactor; 283 | } 284 | 285 | - (CGFloat)minZoom { 286 | if (!_inputCamera) { 287 | return 1; 288 | } 289 | if (@available(iOS 13.0, *)) { 290 | if (_inputCamera.deviceType == AVCaptureDeviceTypeBuiltInTripleCamera || _inputCamera.deviceType == AVCaptureDeviceTypeBuiltInDualWideCamera) { 291 | return _inputCamera.minAvailableVideoZoomFactor / 2.0; 292 | } 293 | } 294 | return _inputCamera.minAvailableVideoZoomFactor; 295 | } 296 | 297 | - (void)takePhoto 298 | { 299 | __weak typeof(self)weakSelf = self; 300 | dispatch_async(self.captureQueue, ^{ 301 | __strong typeof(weakSelf)strongSelf = weakSelf; 302 | 303 | if (strongSelf.mode != SYPhotoMode) { 304 | SYLog(TAG, "takePhoto %lu is not SYPhotoMode", strongSelf.mode); 305 | return; 306 | } 307 | 308 | if (!strongSelf->_photoOutput) { 309 | SYLog(TAG, "takePhoto photoOutput is nil"); 310 | return; 311 | } 312 | 313 | NSDictionary *dict = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}; 314 | AVCapturePhotoSettings *setting = [AVCapturePhotoSettings photoSettingsWithFormat:dict]; 315 | 316 | // 设置高清晰 317 | [setting setHighResolutionPhotoEnabled:YES]; 318 | // 防抖 319 | [setting setAutoStillImageStabilizationEnabled:YES]; 320 | 321 | AVCaptureConnection *photoOutputConnection = [strongSelf->_photoOutput connectionWithMediaType:AVMediaTypeVideo]; 322 | if (photoOutputConnection) { 323 | photoOutputConnection.videoMirrored = strongSelf.cameraPosition == AVCaptureDevicePositionFront; 324 | } 325 | 326 | if ([strongSelf->_inputCamera hasFlash]) { 327 | [setting setFlashMode:strongSelf.flashMode]; 328 | } 329 | 330 | if (@available(iOS 13.0, *)) { 331 | [setting setPhotoQualityPrioritization:AVCapturePhotoQualityPrioritizationBalanced]; 332 | } 333 | 334 | [strongSelf->_photoOutput capturePhotoWithSettings:setting delegate:self]; 335 | }); 336 | } 337 | 338 | 339 | @end 340 | -------------------------------------------------------------------------------- /Sources/SwiftyCamera.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyCamera.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2023/4/8. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for SwiftyCamera. 11 | FOUNDATION_EXPORT double SwiftyCameraVersionNumber; 12 | 13 | //! Project version string for SwiftyCamera. 14 | FOUNDATION_EXPORT const unsigned char SwiftyCameraVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | #import "SYCameraConfig.h" 19 | #import "SYCameraManager.h" 20 | 21 | -------------------------------------------------------------------------------- /Sources/UIImage+SYImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+SYImage.h 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/24. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface UIImage (SYImage) 13 | 14 | - (UIImage *)fixImageWithRatio:(CGFloat)ratio isFront:(BOOL)isFront; 15 | 16 | + (UIImage * _Nullable)stitchDualImages:(NSArray *)images andRects:(NSArray *)rects; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /Sources/UIImage+SYImage.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+SYImage.m 3 | // SwiftyCamera 4 | // 5 | // Created by 马陈爽 on 2024/6/24. 6 | // 7 | 8 | #import "UIImage+SYImage.h" 9 | 10 | @implementation UIImage (SYImage) 11 | 12 | - (UIImage *)fixImageWithRatio:(CGFloat)ratio isFront:(BOOL)isFront { 13 | // CGContext 理解成依附在画布上的一个窗口,画布严格遵循笛卡尔坐标系,窗口内的数据可见,窗口外的数据会被裁剪 14 | // transform 是影响画布的绘制起点坐标轴的位置、走向、x轴y轴的方向,窗口的位置仍然不变,且坐标方向不变 15 | // 当进行绘制的时候,严格按照从(0,0)开始 16 | CGImageRef cgImage = self.CGImage; 17 | int width = (int)CGImageGetWidth(cgImage); 18 | int height = (int)CGImageGetHeight(cgImage); 19 | 20 | CGRect cropRect; 21 | 22 | if (ratio > 0) { 23 | cropRect = [self calculateCropRect:CGSizeMake(height, width) ratio:ratio]; 24 | } else { 25 | cropRect = CGRectMake(0, 0, width, height); 26 | } 27 | 28 | int contextW = CGRectGetWidth(cropRect); 29 | int contextH = CGRectGetHeight(cropRect); 30 | // x 轴向左,y 轴向上 31 | CGAffineTransform transform = CGAffineTransformIdentity; 32 | transform = CGAffineTransformTranslate(transform, 0, width); 33 | transform = CGAffineTransformRotate(transform, -M_PI_2); 34 | // x 轴向下,y 轴向右 35 | if (isFront) { 36 | transform = CGAffineTransformTranslate(transform, 0, height); 37 | transform = CGAffineTransformScale(transform, 1, -1); 38 | // x 轴向下,y 轴向左 39 | transform = CGAffineTransformTranslate(transform, (width-contextH)/2, (height-contextW)/2); 40 | } else { 41 | transform = CGAffineTransformTranslate(transform, (width-contextH)/2, -(height-contextW)/2); 42 | } 43 | 44 | CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage); 45 | CGContextRef context = CGBitmapContextCreate(NULL, contextW, contextH, 8, contextW * 4, colorSpace, CGImageGetBitmapInfo(cgImage)); 46 | CGContextConcatCTM(context, transform); 47 | CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); 48 | CGImageRef newCGImage = CGBitmapContextCreateImage(context); 49 | UIImage *newImage = [UIImage imageWithCGImage:newCGImage scale:1.0 orientation:UIImageOrientationUp]; 50 | 51 | CGImageRelease(newCGImage); 52 | CGContextRelease(context); 53 | return newImage; 54 | } 55 | 56 | + (UIImage *)stitchDualImages:(NSArray *)images andRects:(NSArray *)rects { 57 | size_t outputWidth; 58 | size_t outputHeigth; 59 | 60 | UIImage *firstImage = images[0]; 61 | UIImage *secondImage = images[1]; 62 | CGRect firstRect = [rects[0] CGRectValue]; 63 | CGRect secondRect = [rects[1] CGRectValue]; 64 | 65 | outputWidth = firstImage.size.width / firstRect.size.width; 66 | outputHeigth = firstImage.size.height / firstRect.size.height; 67 | 68 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 69 | CGContextRef context = CGBitmapContextCreate(NULL, 70 | outputWidth, 71 | outputHeigth, 72 | 8, 73 | outputWidth * 4, 74 | colorSpace, 75 | kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); 76 | if (!context) return nil; 77 | CGContextDrawImage(context, CGRectMake(outputWidth * firstRect.origin.x, outputHeigth * (1 - firstRect.origin.y - firstRect.size.height), outputWidth * firstRect.size.width, outputHeigth * firstRect.size.height), firstImage.CGImage); 78 | CGContextDrawImage(context, CGRectMake(outputWidth * secondRect.origin.x, outputHeigth * (1 - secondRect.origin.y - secondRect.size.height), outputWidth * secondRect.size.width, outputHeigth * secondRect.size.height), secondImage.CGImage); 79 | CGImageRef imgRef = CGBitmapContextCreateImage(context); 80 | UIImage *img = [UIImage imageWithCGImage:imgRef scale:1.0 orientation:UIImageOrientationUp]; 81 | CGImageRelease(imgRef); 82 | CGContextRelease(context); 83 | return img; 84 | } 85 | 86 | - (CGRect)calculateCropRect:(CGSize)size ratio:(CGFloat)ratio 87 | { 88 | CGFloat cropW = size.width; 89 | CGFloat cropH = cropW / ratio; 90 | if (cropH > size.height) { 91 | cropH = size.height; 92 | cropW = cropH * ratio; 93 | } 94 | CGFloat cropX = (size.width - cropW) / 2; 95 | CGFloat cropY = (size.height - cropH) / 2; 96 | CGRect cropRect = CGRectMake((int)cropX, (int)cropY, (int)cropW, (int)cropH); 97 | return cropRect; 98 | } 99 | 100 | @end 101 | -------------------------------------------------------------------------------- /SwiftyCamera.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint SwiftyCamera.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'SwiftyCamera' 11 | s.version = '1.0.0' 12 | s.summary = 'A short description of SwiftyCamera.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | TODO: Add long description of the pod here. 22 | DESC 23 | 24 | s.homepage = 'https://github.com/chenshuangma@foxmail.com/SwiftyCamera' 25 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'chenshuangma@foxmail.com' => 'https://github.com/machenshuang' } 28 | s.source = { :git => 'https://github.com/machenshuang/SwiftyCamera.git', :branch => "master" } 29 | # s.social_media_url = 'https://twitter.com/' 30 | 31 | s.ios.deployment_target = '11.0' 32 | s.source_files = 'Sources/**/*' 33 | s.public_header_files = "Sources/**/*.h" 34 | 35 | end 36 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------