├── .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
--------------------------------------------------------------------------------