├── .gitignore ├── LICENSE ├── README.md ├── VIExportSession.podspec ├── VIExportSession.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── vito.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── VIExportSession ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Info.plist ├── Source ├── ExportConfiguration.swift └── VIExportSession.swift └── ViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | .build/ 46 | 47 | # CocoaPods 48 | # 49 | # We recommend against adding the Pods directory to your .gitignore. However 50 | # you should judge for yourself, the pros and cons are mentioned at: 51 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 52 | # 53 | # Pods/ 54 | # 55 | # Add this line if you want to avoid checking in source code from the Xcode workspace 56 | # *.xcworkspace 57 | 58 | # Carthage 59 | # 60 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 61 | # Carthage/Checkouts 62 | 63 | Carthage/Build 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | 78 | # End of https://www.gitignore.io/api/swift 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2018 Vito Zhang 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIExportSession 2 | 3 | A `AVAssetExportSession` drop-in replacement with customizable audio&video settings. 4 | 5 | You can get more control on video encode and decode, see the detail on `ExportConfiguration.swift` 6 | 7 | ```Swift 8 | class ExportConfiguration { 9 | var outputURL = URL.temporaryExportURL() 10 | var fileType: AVFileType = .mp4 11 | var shouldOptimizeForNetworkUse = false 12 | var metadata: [AVMetadataItem] = [] 13 | } 14 | 15 | class VideoConfiguration { 16 | // Video settings see AVVideoSettings.h 17 | var videoInputSetting: [String: Any]? 18 | var videoOutputSetting: [String: Any]? 19 | var videoComposition: AVVideoComposition? 20 | } 21 | 22 | class AudioConfiguration { 23 | // Audio settings see AVAudioSettings.h 24 | var audioInputSetting: [String: Any]? 25 | var audioOutputSetting: [String: Any]? 26 | var audioMix: AVAudioMix? 27 | var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm? 28 | } 29 | ``` 30 | 31 | ## Example 32 | 33 | ``` 34 | exportSession.videoConfiguration.videoOutputSetting = { 35 | let frameRate = 30 36 | let bitrate = min(2000000, videoTrack.estimatedDataRate) 37 | let trackDimensions = videoTrack.naturalSize 38 | let compressionSettings: [String: Any] = [ 39 | AVVideoAverageNonDroppableFrameRateKey: frameRate, 40 | AVVideoAverageBitRateKey: bitrate, 41 | AVVideoMaxKeyFrameIntervalKey: 30, 42 | AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel 43 | ] 44 | var videoSettings: [String : Any] = [ 45 | AVVideoWidthKey: trackDimensions.width, 46 | AVVideoHeightKey: trackDimensions.height, 47 | AVVideoCompressionPropertiesKey: compressionSettings 48 | ] 49 | if #available(iOS 11.0, *) { 50 | videoSettings[AVVideoCodecKey] = AVVideoCodecType.h264 51 | } else { 52 | videoSettings[AVVideoCodecKey] = AVVideoCodecH264 53 | } 54 | return videoSettings 55 | }() 56 | 57 | exportSession.audioConfiguration.audioOutputSetting = { 58 | var stereoChannelLayout = AudioChannelLayout() 59 | memset(&stereoChannelLayout, 0, MemoryLayout.size) 60 | stereoChannelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo 61 | 62 | let channelLayoutAsData = Data(bytes: &stereoChannelLayout, count: MemoryLayout.size) 63 | let compressionAudioSettings: [String: Any] = [ 64 | AVFormatIDKey: kAudioFormatMPEG4AAC, 65 | AVEncoderBitRateKey: 128000, 66 | AVSampleRateKey: 44100, 67 | AVChannelLayoutKey: channelLayoutAsData, 68 | AVNumberOfChannelsKey: 2 69 | ] 70 | return compressionAudioSettings 71 | }() 72 | ``` 73 | 74 | ## Installation 75 | 76 | `VIExportSession` only support Swift 4 77 | 78 | **Cocoapods** 79 | 80 | ``` 81 | platform :ios, '8.0' 82 | use_frameworks! 83 | 84 | target 'MyApp' do 85 | # your other pod 86 | # ... 87 | pod 'VIExportSession' 88 | end 89 | ``` 90 | 91 | **Manually** 92 | 93 | You can simplely drag `VIExportSession.swift` to you project 94 | 95 | ## LICENSE 96 | 97 | Under MIT 98 | -------------------------------------------------------------------------------- /VIExportSession.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = 'VIExportSession' 4 | s.version = '0.1' 5 | s.summary = 'VIExportSession is a AVAssetExportSession drop-in replacement with customizable audio&video settings.' 6 | 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | 9 | s.homepage = 'https://github.com/VideoFlint/VIExportSession' 10 | 11 | s.author = { 'Vito' => 'vvitozhang@gmail.com' } 12 | 13 | s.platform = :ios, '9.0' 14 | s.swift_version = "4.0" 15 | 16 | s.source = { :git => 'https://github.com/VideoFlint/VIExportSession.git', :tag => s.version.to_s } 17 | s.source_files = 'VIExportSession/Source/*.swift' 18 | 19 | s.requires_arc = true 20 | s.frameworks = 'AVFoundation' 21 | 22 | end 23 | 24 | -------------------------------------------------------------------------------- /VIExportSession.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5F464552210C1D000013ADC3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F464551210C1D000013ADC3 /* AppDelegate.swift */; }; 11 | 5F464554210C1D000013ADC3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F464553210C1D000013ADC3 /* ViewController.swift */; }; 12 | 5F464557210C1D000013ADC3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5F464555210C1D000013ADC3 /* Main.storyboard */; }; 13 | 5F464559210C1D020013ADC3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F464558210C1D020013ADC3 /* Assets.xcassets */; }; 14 | 5F46455C210C1D020013ADC3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5F46455A210C1D020013ADC3 /* LaunchScreen.storyboard */; }; 15 | 5F464566210C1D2A0013ADC3 /* ExportConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F464564210C1D2A0013ADC3 /* ExportConfiguration.swift */; }; 16 | 5F464567210C1D2A0013ADC3 /* VIExportSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F464565210C1D2A0013ADC3 /* VIExportSession.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 5F46454E210C1D000013ADC3 /* VIExportSession.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VIExportSession.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 5F464551210C1D000013ADC3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 5F464553210C1D000013ADC3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | 5F464556210C1D000013ADC3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 5F464558210C1D020013ADC3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 5F46455B210C1D020013ADC3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 5F46455D210C1D020013ADC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | 5F464564210C1D2A0013ADC3 /* ExportConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExportConfiguration.swift; sourceTree = ""; }; 28 | 5F464565210C1D2A0013ADC3 /* VIExportSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VIExportSession.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 5F46454B210C1D000013ADC3 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 5F464545210C1D000013ADC3 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 5F464550210C1D000013ADC3 /* VIExportSession */, 46 | 5F46454F210C1D000013ADC3 /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 5F46454F210C1D000013ADC3 /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 5F46454E210C1D000013ADC3 /* VIExportSession.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 5F464550210C1D000013ADC3 /* VIExportSession */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 5F464563210C1D2A0013ADC3 /* Source */, 62 | 5F464551210C1D000013ADC3 /* AppDelegate.swift */, 63 | 5F464553210C1D000013ADC3 /* ViewController.swift */, 64 | 5F464555210C1D000013ADC3 /* Main.storyboard */, 65 | 5F464558210C1D020013ADC3 /* Assets.xcassets */, 66 | 5F46455A210C1D020013ADC3 /* LaunchScreen.storyboard */, 67 | 5F46455D210C1D020013ADC3 /* Info.plist */, 68 | ); 69 | path = VIExportSession; 70 | sourceTree = ""; 71 | }; 72 | 5F464563210C1D2A0013ADC3 /* Source */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 5F464564210C1D2A0013ADC3 /* ExportConfiguration.swift */, 76 | 5F464565210C1D2A0013ADC3 /* VIExportSession.swift */, 77 | ); 78 | path = Source; 79 | sourceTree = ""; 80 | }; 81 | /* End PBXGroup section */ 82 | 83 | /* Begin PBXNativeTarget section */ 84 | 5F46454D210C1D000013ADC3 /* VIExportSession */ = { 85 | isa = PBXNativeTarget; 86 | buildConfigurationList = 5F464560210C1D020013ADC3 /* Build configuration list for PBXNativeTarget "VIExportSession" */; 87 | buildPhases = ( 88 | 5F46454A210C1D000013ADC3 /* Sources */, 89 | 5F46454B210C1D000013ADC3 /* Frameworks */, 90 | 5F46454C210C1D000013ADC3 /* Resources */, 91 | ); 92 | buildRules = ( 93 | ); 94 | dependencies = ( 95 | ); 96 | name = VIExportSession; 97 | productName = VIExportSession; 98 | productReference = 5F46454E210C1D000013ADC3 /* VIExportSession.app */; 99 | productType = "com.apple.product-type.application"; 100 | }; 101 | /* End PBXNativeTarget section */ 102 | 103 | /* Begin PBXProject section */ 104 | 5F464546210C1D000013ADC3 /* Project object */ = { 105 | isa = PBXProject; 106 | attributes = { 107 | LastSwiftUpdateCheck = 0940; 108 | LastUpgradeCheck = 0940; 109 | ORGANIZATIONNAME = Vito; 110 | TargetAttributes = { 111 | 5F46454D210C1D000013ADC3 = { 112 | CreatedOnToolsVersion = 9.4.1; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 5F464549210C1D000013ADC3 /* Build configuration list for PBXProject "VIExportSession" */; 117 | compatibilityVersion = "Xcode 9.3"; 118 | developmentRegion = en; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | Base, 123 | ); 124 | mainGroup = 5F464545210C1D000013ADC3; 125 | productRefGroup = 5F46454F210C1D000013ADC3 /* Products */; 126 | projectDirPath = ""; 127 | projectRoot = ""; 128 | targets = ( 129 | 5F46454D210C1D000013ADC3 /* VIExportSession */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXResourcesBuildPhase section */ 135 | 5F46454C210C1D000013ADC3 /* Resources */ = { 136 | isa = PBXResourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | 5F46455C210C1D020013ADC3 /* LaunchScreen.storyboard in Resources */, 140 | 5F464559210C1D020013ADC3 /* Assets.xcassets in Resources */, 141 | 5F464557210C1D000013ADC3 /* Main.storyboard in Resources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXResourcesBuildPhase section */ 146 | 147 | /* Begin PBXSourcesBuildPhase section */ 148 | 5F46454A210C1D000013ADC3 /* Sources */ = { 149 | isa = PBXSourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | 5F464554210C1D000013ADC3 /* ViewController.swift in Sources */, 153 | 5F464567210C1D2A0013ADC3 /* VIExportSession.swift in Sources */, 154 | 5F464566210C1D2A0013ADC3 /* ExportConfiguration.swift in Sources */, 155 | 5F464552210C1D000013ADC3 /* AppDelegate.swift in Sources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXSourcesBuildPhase section */ 160 | 161 | /* Begin PBXVariantGroup section */ 162 | 5F464555210C1D000013ADC3 /* Main.storyboard */ = { 163 | isa = PBXVariantGroup; 164 | children = ( 165 | 5F464556210C1D000013ADC3 /* Base */, 166 | ); 167 | name = Main.storyboard; 168 | sourceTree = ""; 169 | }; 170 | 5F46455A210C1D020013ADC3 /* LaunchScreen.storyboard */ = { 171 | isa = PBXVariantGroup; 172 | children = ( 173 | 5F46455B210C1D020013ADC3 /* Base */, 174 | ); 175 | name = LaunchScreen.storyboard; 176 | sourceTree = ""; 177 | }; 178 | /* End PBXVariantGroup section */ 179 | 180 | /* Begin XCBuildConfiguration section */ 181 | 5F46455E210C1D020013ADC3 /* Debug */ = { 182 | isa = XCBuildConfiguration; 183 | buildSettings = { 184 | ALWAYS_SEARCH_USER_PATHS = NO; 185 | CLANG_ANALYZER_NONNULL = YES; 186 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 188 | CLANG_CXX_LIBRARY = "libc++"; 189 | CLANG_ENABLE_MODULES = YES; 190 | CLANG_ENABLE_OBJC_ARC = YES; 191 | CLANG_ENABLE_OBJC_WEAK = YES; 192 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 193 | CLANG_WARN_BOOL_CONVERSION = YES; 194 | CLANG_WARN_COMMA = YES; 195 | CLANG_WARN_CONSTANT_CONVERSION = YES; 196 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 197 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 198 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 199 | CLANG_WARN_EMPTY_BODY = YES; 200 | CLANG_WARN_ENUM_CONVERSION = YES; 201 | CLANG_WARN_INFINITE_RECURSION = YES; 202 | CLANG_WARN_INT_CONVERSION = YES; 203 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 204 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 205 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 206 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 207 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 208 | CLANG_WARN_STRICT_PROTOTYPES = YES; 209 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 210 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 211 | CLANG_WARN_UNREACHABLE_CODE = YES; 212 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 213 | CODE_SIGN_IDENTITY = "iPhone Developer"; 214 | COPY_PHASE_STRIP = NO; 215 | DEBUG_INFORMATION_FORMAT = dwarf; 216 | ENABLE_STRICT_OBJC_MSGSEND = YES; 217 | ENABLE_TESTABILITY = YES; 218 | GCC_C_LANGUAGE_STANDARD = gnu11; 219 | GCC_DYNAMIC_NO_PIC = NO; 220 | GCC_NO_COMMON_BLOCKS = YES; 221 | GCC_OPTIMIZATION_LEVEL = 0; 222 | GCC_PREPROCESSOR_DEFINITIONS = ( 223 | "DEBUG=1", 224 | "$(inherited)", 225 | ); 226 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 227 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 228 | GCC_WARN_UNDECLARED_SELECTOR = YES; 229 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 230 | GCC_WARN_UNUSED_FUNCTION = YES; 231 | GCC_WARN_UNUSED_VARIABLE = YES; 232 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 233 | MTL_ENABLE_DEBUG_INFO = YES; 234 | ONLY_ACTIVE_ARCH = YES; 235 | SDKROOT = iphoneos; 236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 238 | }; 239 | name = Debug; 240 | }; 241 | 5F46455F210C1D020013ADC3 /* Release */ = { 242 | isa = XCBuildConfiguration; 243 | buildSettings = { 244 | ALWAYS_SEARCH_USER_PATHS = NO; 245 | CLANG_ANALYZER_NONNULL = YES; 246 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 248 | CLANG_CXX_LIBRARY = "libc++"; 249 | CLANG_ENABLE_MODULES = YES; 250 | CLANG_ENABLE_OBJC_ARC = YES; 251 | CLANG_ENABLE_OBJC_WEAK = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 259 | CLANG_WARN_EMPTY_BODY = YES; 260 | CLANG_WARN_ENUM_CONVERSION = YES; 261 | CLANG_WARN_INFINITE_RECURSION = YES; 262 | CLANG_WARN_INT_CONVERSION = YES; 263 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 265 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 268 | CLANG_WARN_STRICT_PROTOTYPES = YES; 269 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 270 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 271 | CLANG_WARN_UNREACHABLE_CODE = YES; 272 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 273 | CODE_SIGN_IDENTITY = "iPhone Developer"; 274 | COPY_PHASE_STRIP = NO; 275 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 276 | ENABLE_NS_ASSERTIONS = NO; 277 | ENABLE_STRICT_OBJC_MSGSEND = YES; 278 | GCC_C_LANGUAGE_STANDARD = gnu11; 279 | GCC_NO_COMMON_BLOCKS = YES; 280 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 281 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 282 | GCC_WARN_UNDECLARED_SELECTOR = YES; 283 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 284 | GCC_WARN_UNUSED_FUNCTION = YES; 285 | GCC_WARN_UNUSED_VARIABLE = YES; 286 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 287 | MTL_ENABLE_DEBUG_INFO = NO; 288 | SDKROOT = iphoneos; 289 | SWIFT_COMPILATION_MODE = wholemodule; 290 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 291 | VALIDATE_PRODUCT = YES; 292 | }; 293 | name = Release; 294 | }; 295 | 5F464561210C1D020013ADC3 /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | CODE_SIGN_STYLE = Automatic; 300 | DEVELOPMENT_TEAM = ZZX7396L9W; 301 | INFOPLIST_FILE = VIExportSession/Info.plist; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/Frameworks", 305 | ); 306 | PRODUCT_BUNDLE_IDENTIFIER = com.vito.VIExportSession; 307 | PRODUCT_NAME = "$(TARGET_NAME)"; 308 | SWIFT_VERSION = 4.0; 309 | TARGETED_DEVICE_FAMILY = "1,2"; 310 | }; 311 | name = Debug; 312 | }; 313 | 5F464562210C1D020013ADC3 /* Release */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | CODE_SIGN_STYLE = Automatic; 318 | DEVELOPMENT_TEAM = ZZX7396L9W; 319 | INFOPLIST_FILE = VIExportSession/Info.plist; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/Frameworks", 323 | ); 324 | PRODUCT_BUNDLE_IDENTIFIER = com.vito.VIExportSession; 325 | PRODUCT_NAME = "$(TARGET_NAME)"; 326 | SWIFT_VERSION = 4.0; 327 | TARGETED_DEVICE_FAMILY = "1,2"; 328 | }; 329 | name = Release; 330 | }; 331 | /* End XCBuildConfiguration section */ 332 | 333 | /* Begin XCConfigurationList section */ 334 | 5F464549210C1D000013ADC3 /* Build configuration list for PBXProject "VIExportSession" */ = { 335 | isa = XCConfigurationList; 336 | buildConfigurations = ( 337 | 5F46455E210C1D020013ADC3 /* Debug */, 338 | 5F46455F210C1D020013ADC3 /* Release */, 339 | ); 340 | defaultConfigurationIsVisible = 0; 341 | defaultConfigurationName = Release; 342 | }; 343 | 5F464560210C1D020013ADC3 /* Build configuration list for PBXNativeTarget "VIExportSession" */ = { 344 | isa = XCConfigurationList; 345 | buildConfigurations = ( 346 | 5F464561210C1D020013ADC3 /* Debug */, 347 | 5F464562210C1D020013ADC3 /* Release */, 348 | ); 349 | defaultConfigurationIsVisible = 0; 350 | defaultConfigurationName = Release; 351 | }; 352 | /* End XCConfigurationList section */ 353 | }; 354 | rootObject = 5F464546210C1D000013ADC3 /* Project object */; 355 | } 356 | -------------------------------------------------------------------------------- /VIExportSession.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VIExportSession.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VIExportSession.xcodeproj/xcuserdata/vito.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | VIExportSession.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | VIExportSession.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /VIExportSession/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // VIExportSession 4 | // 5 | // Created by Vito on 2018/7/28. 6 | // Copyright © 2018 Vito. 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 invalidate graphics rendering callbacks. 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 active 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 | -------------------------------------------------------------------------------- /VIExportSession/Assets.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" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /VIExportSession/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /VIExportSession/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /VIExportSession/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 | 52 | 65 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /VIExportSession/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | NSPhotoLibraryUsageDescription 45 | Save media file to Photos 46 | 47 | 48 | -------------------------------------------------------------------------------- /VIExportSession/Source/ExportConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExportConfiguration.swift 3 | // VIExportSession 4 | // 5 | // Created by Vito on 06/02/2018. 6 | // Copyright © 2018 Vito. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | public class ExportConfiguration { 12 | public var outputURL = URL.temporaryExportURL() 13 | public var fileType: AVFileType = .mp4 14 | public var shouldOptimizeForNetworkUse = false 15 | public var metadata: [AVMetadataItem] = [] 16 | } 17 | 18 | public class VideoConfiguration { 19 | // Video settings see AVVideoSettings.h 20 | public var videoInputSetting: [String: Any]? 21 | public var videoOutputSetting: [String: Any]? 22 | public var videoComposition: AVVideoComposition? 23 | } 24 | 25 | public class AudioConfiguration { 26 | // Audio settings see AVAudioSettings.h 27 | public var audioInputSetting: [String: Any]? 28 | public var audioOutputSetting: [String: Any]? 29 | public var audioMix: AVAudioMix? 30 | public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm? 31 | } 32 | 33 | // MARK: - Helper 34 | 35 | fileprivate extension URL { 36 | static func temporaryExportURL() -> URL { 37 | let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! 38 | let filename = ProcessInfo.processInfo.globallyUniqueString + ".mp4" 39 | return documentDirectory.appendingPathComponent(filename) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /VIExportSession/Source/VIExportSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VIExportSession.swift 3 | // VIExportSession 4 | // 5 | // Created by Vito on 30/01/2018. 6 | // Copyright © 2018 Vito. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | public class VIExportSession { 12 | 13 | public private(set) var asset: AVAsset 14 | public var exportConfiguration = ExportConfiguration() 15 | public var videoConfiguration = VideoConfiguration() 16 | public var audioConfiguration = AudioConfiguration() 17 | 18 | fileprivate var reader: AVAssetReader! 19 | fileprivate var videoOutput: AVAssetReaderVideoCompositionOutput? 20 | fileprivate var audioOutput: AVAssetReaderAudioMixOutput? 21 | fileprivate var writer: AVAssetWriter! 22 | fileprivate var videoInput: AVAssetWriterInput? 23 | fileprivate var audioInput: AVAssetWriterInput? 24 | fileprivate var inputQueue = DispatchQueue(label: "VideoEncoderQueue") 25 | 26 | // MARK: - Exporting properties 27 | public var progress: Float = 0 { 28 | didSet { 29 | progressHandler?(progress) 30 | } 31 | } 32 | public var videoProgress: Float = 0 { 33 | didSet { 34 | if audioInput != nil { 35 | progress = 0.95 * videoProgress + 0.05 * audioProgress 36 | } else { 37 | progress = videoProgress 38 | } 39 | } 40 | } 41 | public var audioProgress: Float = 0 { 42 | didSet { 43 | if videoInput != nil { 44 | progress = 0.95 * videoProgress + 0.05 * audioProgress 45 | } else { 46 | progress = audioProgress 47 | } 48 | } 49 | } 50 | 51 | public var progressHandler: ((Float) -> Void)? 52 | public var completionHandler: ((Error?) -> Void)? 53 | 54 | fileprivate var videoCompleted = false 55 | fileprivate var audioCompleted = false 56 | 57 | public init(asset: AVAsset) { 58 | self.asset = asset 59 | } 60 | 61 | // MARK: - Main 62 | 63 | public func cancelExport() { 64 | if let writer = writer, let reader = reader { 65 | inputQueue.async { 66 | writer.cancelWriting() 67 | reader.cancelReading() 68 | } 69 | } 70 | } 71 | 72 | public func export() { 73 | cancelExport() 74 | reset() 75 | do { 76 | reader = try AVAssetReader(asset: asset) 77 | writer = try AVAssetWriter(url: exportConfiguration.outputURL, fileType: exportConfiguration.fileType) 78 | 79 | writer.shouldOptimizeForNetworkUse = exportConfiguration.shouldOptimizeForNetworkUse 80 | writer.metadata = exportConfiguration.metadata 81 | 82 | // Video output 83 | let videoTracks = asset.tracks(withMediaType: .video) 84 | if videoTracks.count > 0 { 85 | if videoConfiguration.videoOutputSetting == nil { 86 | videoConfiguration.videoOutputSetting = buildDefaultVideoOutputSetting(videoTrack: videoTracks.first!) 87 | } 88 | 89 | let videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: videoConfiguration.videoInputSetting) 90 | videoOutput.alwaysCopiesSampleData = false 91 | videoOutput.videoComposition = videoConfiguration.videoComposition 92 | if videoOutput.videoComposition == nil { 93 | videoOutput.videoComposition = buildDefaultVideoComposition(with: asset) 94 | } 95 | 96 | guard reader.canAdd(videoOutput) else { 97 | throw NSError(domain: "com.exportsession", code: 0, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("Can't add video output", comment: "")]) 98 | } 99 | reader.add(videoOutput) 100 | self.videoOutput = videoOutput 101 | 102 | // Video input 103 | let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoConfiguration.videoOutputSetting) 104 | videoInput.expectsMediaDataInRealTime = false 105 | guard writer.canAdd(videoInput) else { 106 | throw NSError(domain: "com.exportsession", code: 0, userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("Can't add video input", comment: "")]) 107 | } 108 | writer.add(videoInput) 109 | self.videoInput = videoInput 110 | } 111 | 112 | // Audio output 113 | let audioTracks = asset.tracks(withMediaType: .audio) 114 | if audioTracks.count > 0 { 115 | if audioConfiguration.audioOutputSetting == nil { 116 | audioConfiguration.audioOutputSetting = buildDefaultAudioOutputSetting() 117 | } 118 | 119 | let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: audioConfiguration.audioInputSetting) 120 | audioOutput.alwaysCopiesSampleData = false 121 | audioOutput.audioMix = audioConfiguration.audioMix 122 | if let audioTimePitchAlgorithm = audioConfiguration.audioTimePitchAlgorithm { 123 | audioOutput.audioTimePitchAlgorithm = audioTimePitchAlgorithm 124 | } 125 | if reader.canAdd(audioOutput) { 126 | reader.add(audioOutput) 127 | self.audioOutput = audioOutput 128 | } 129 | 130 | if self.audioOutput != nil { 131 | // Audio input 132 | let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioConfiguration.audioOutputSetting) 133 | audioInput.expectsMediaDataInRealTime = false 134 | if writer.canAdd(audioInput) { 135 | writer.add(audioInput) 136 | self.audioInput = audioInput 137 | } 138 | } 139 | } 140 | 141 | writer.startWriting() 142 | reader.startReading() 143 | writer.startSession(atSourceTime: kCMTimeZero) 144 | 145 | encodeVideoData() 146 | encodeAudioData() 147 | } catch { 148 | self.completionHandler?(error) 149 | } 150 | } 151 | 152 | fileprivate func encodeVideoData() { 153 | if let videoInput = videoInput { 154 | videoInput.requestMediaDataWhenReady(on: inputQueue, using: { [weak self] in 155 | guard let strongSelf = self else { return } 156 | guard let videoOutput = strongSelf.videoOutput, let videoInput = strongSelf.videoInput else { return } 157 | strongSelf.encodeReadySamplesFrom(output: videoOutput, to: videoInput, completion: { 158 | strongSelf.videoCompleted = true 159 | strongSelf.tryFinish() 160 | }) 161 | }) 162 | } else { 163 | videoCompleted = true 164 | tryFinish() 165 | } 166 | } 167 | 168 | fileprivate func encodeAudioData() { 169 | if let audioInput = audioInput { 170 | audioInput.requestMediaDataWhenReady(on: inputQueue, using: { [weak self] in 171 | guard let strongSelf = self else { return } 172 | guard let audioOutput = strongSelf.audioOutput, let audioInput = strongSelf.audioInput else { return } 173 | strongSelf.encodeReadySamplesFrom(output: audioOutput, to: audioInput, completion: { 174 | strongSelf.audioCompleted = true 175 | strongSelf.tryFinish() 176 | }) 177 | }) 178 | } else { 179 | audioCompleted = true 180 | tryFinish() 181 | } 182 | } 183 | 184 | private var lastVideoSamplePresentationTime = kCMTimeZero 185 | private var lastAudioSamplePresentationTime = kCMTimeZero 186 | fileprivate func encodeReadySamplesFrom(output: AVAssetReaderOutput, to input: AVAssetWriterInput, completion: @escaping () -> Void) { 187 | while input.isReadyForMoreMediaData { 188 | let complete = autoreleasepool(invoking: { [weak self] () -> Bool in 189 | guard let strongSelf = self else { return true } 190 | if let sampleBuffer = output.copyNextSampleBuffer() { 191 | guard strongSelf.reader.status == .reading && strongSelf.writer.status == .writing else { 192 | return true 193 | } 194 | 195 | guard input.append(sampleBuffer) else { 196 | return true 197 | } 198 | 199 | if let videoOutput = strongSelf.videoOutput, videoOutput == output { 200 | lastVideoSamplePresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) 201 | if strongSelf.asset.duration.seconds > 0 { 202 | strongSelf.videoProgress = Float(lastVideoSamplePresentationTime.seconds / strongSelf.asset.duration.seconds) 203 | } else { 204 | strongSelf.videoProgress = 1 205 | } 206 | } else if let audioOutput = strongSelf.audioOutput, audioOutput == output { 207 | lastAudioSamplePresentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) 208 | if strongSelf.asset.duration.seconds > 0 { 209 | strongSelf.audioProgress = Float(lastAudioSamplePresentationTime.seconds / strongSelf.asset.duration.seconds) 210 | } else { 211 | strongSelf.audioProgress = 1 212 | } 213 | } 214 | } else { 215 | input.markAsFinished() 216 | return true 217 | } 218 | return false 219 | }) 220 | if complete { 221 | completion() 222 | break 223 | } 224 | } 225 | } 226 | 227 | fileprivate func tryFinish() { 228 | objc_sync_enter(self) 229 | defer { objc_sync_exit(self) } 230 | if audioCompleted && videoCompleted { 231 | if reader.status == .cancelled || writer.status == .cancelled { 232 | finish() 233 | } else if writer.status == .failed { 234 | finish() 235 | } else if reader.status == .failed { 236 | writer.cancelWriting() 237 | finish() 238 | } else { 239 | writer.finishWriting { [weak self] in 240 | guard let strongSelf = self else { return } 241 | strongSelf.finish() 242 | } 243 | } 244 | } 245 | } 246 | 247 | fileprivate func finish() { 248 | if writer.status == .failed || reader.status == .failed { 249 | try? FileManager.default.removeItem(at: exportConfiguration.outputURL) 250 | } 251 | let error = writer.error ?? reader.error 252 | completionHandler?(error) 253 | 254 | reset() 255 | } 256 | 257 | fileprivate func reset() { 258 | videoCompleted = false 259 | videoCompleted = false 260 | 261 | videoProgress = 0 262 | audioProgress = 0 263 | progress = 0 264 | 265 | reader = nil 266 | videoOutput = nil 267 | audioInput = nil 268 | writer = nil 269 | videoInput = nil 270 | audioInput = nil 271 | } 272 | 273 | } 274 | 275 | extension VIExportSession { 276 | fileprivate func buildDefaultVideoComposition(with asset: AVAsset) -> AVVideoComposition { 277 | let videoComposition = AVMutableVideoComposition() 278 | 279 | if let videoTrack = asset.tracks(withMediaType: .video).first { 280 | var trackFrameRate: Float = 30 281 | if let videoCompressionProperties = videoConfiguration.videoOutputSetting?[AVVideoCompressionPropertiesKey] as? [String: Any], 282 | let frameRate = videoCompressionProperties[AVVideoAverageNonDroppableFrameRateKey] as? NSNumber { 283 | trackFrameRate = frameRate.floatValue 284 | } else { 285 | trackFrameRate = videoTrack.nominalFrameRate 286 | } 287 | videoComposition.frameDuration = CMTime(value: 1, timescale: CMTimeScale(trackFrameRate)) 288 | 289 | var naturalSize = videoTrack.naturalSize 290 | var transform = videoTrack.preferredTransform 291 | let angle = atan2(transform.b, transform.a) 292 | let videoAngleInDegree = angle * 180 / CGFloat.pi 293 | if videoAngleInDegree == 90 || videoAngleInDegree == -90 { 294 | let width = naturalSize.width 295 | naturalSize.width = naturalSize.height 296 | naturalSize.height = width 297 | } 298 | 299 | videoComposition.renderSize = naturalSize 300 | 301 | var targetSize = naturalSize 302 | if let width = videoConfiguration.videoOutputSetting?[AVVideoWidthKey] as? NSNumber { 303 | targetSize.width = CGFloat(width.floatValue) 304 | } 305 | if let height = videoConfiguration.videoOutputSetting?[AVVideoHeightKey] as? NSNumber { 306 | targetSize.height = CGFloat(height.floatValue) 307 | } 308 | // Center 309 | if naturalSize.width > 0 && naturalSize.height > 0 { 310 | let xratio = targetSize.width / naturalSize.width 311 | let yratio = targetSize.height / naturalSize.height 312 | let ratio = min(xratio, yratio) 313 | let postWidth = naturalSize.width * ratio 314 | let postHeight = naturalSize.height * ratio 315 | let transx = (targetSize.width - postWidth) * 0.5 316 | let transy = (targetSize.height - postHeight) * 0.5 317 | var matrix = CGAffineTransform(translationX: transx / xratio, y: transy / yratio) 318 | matrix = matrix.scaledBy(x: ratio / xratio, y: ratio / yratio) 319 | transform = transform.concatenating(matrix) 320 | } 321 | 322 | let passThroughInstruction = AVMutableVideoCompositionInstruction() 323 | passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration) 324 | let passThroughLayer = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) 325 | passThroughLayer.setTransform(transform, at: kCMTimeZero) 326 | passThroughInstruction.layerInstructions = [passThroughLayer] 327 | videoComposition.instructions = [passThroughInstruction] 328 | } 329 | 330 | return videoComposition 331 | } 332 | 333 | fileprivate func buildDefaultVideoOutputSetting(videoTrack: AVAssetTrack) -> [String: Any] { 334 | let trackDimensions = { () -> CGSize in 335 | var trackDimensions = videoTrack.naturalSize 336 | let videoAngleInDegree = atan2(videoTrack.preferredTransform.b, videoTrack.preferredTransform.a) * 180.0 / CGFloat(Double.pi) 337 | if abs(videoAngleInDegree) == 90 { 338 | let width = trackDimensions.width 339 | trackDimensions.width = trackDimensions.height 340 | trackDimensions.height = width 341 | } 342 | 343 | return trackDimensions 344 | }() 345 | 346 | var videoSettings: [String : Any] = [ 347 | AVVideoWidthKey: trackDimensions.width, 348 | AVVideoHeightKey: trackDimensions.height, 349 | ] 350 | if #available(iOS 11.0, *) { 351 | videoSettings[AVVideoCodecKey] = AVVideoCodecType.h264 352 | } else { 353 | videoSettings[AVVideoCodecKey] = AVVideoCodecH264 354 | } 355 | return videoSettings 356 | } 357 | 358 | fileprivate func buildDefaultAudioOutputSetting() -> [String: Any] { 359 | var stereoChannelLayout = AudioChannelLayout() 360 | memset(&stereoChannelLayout, 0, MemoryLayout.size) 361 | stereoChannelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo 362 | 363 | let channelLayoutAsData = Data(bytes: &stereoChannelLayout, count: MemoryLayout.size) 364 | let compressionAudioSettings: [String: Any] = [ 365 | AVFormatIDKey: kAudioFormatMPEG4AAC, 366 | AVEncoderBitRateKey: 128000, 367 | AVSampleRateKey: 44100, 368 | AVChannelLayoutKey: channelLayoutAsData, 369 | AVNumberOfChannelsKey: 2 370 | ] 371 | return compressionAudioSettings 372 | } 373 | } 374 | 375 | 376 | -------------------------------------------------------------------------------- /VIExportSession/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // VIExportSession 4 | // 5 | // Created by Vito on 30/01/2018. 6 | // Copyright © 2018 Vito. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | import Photos 12 | import MobileCoreServices 13 | 14 | class ViewController: UIViewController { 15 | 16 | @IBOutlet weak var progressView: UIProgressView! 17 | @IBOutlet weak var statusLabel: UILabel! 18 | @IBOutlet weak var exportButton: UIButton! 19 | @IBOutlet weak var cancelButton: UIButton! 20 | 21 | @IBOutlet weak var coverImageView: UIImageView! 22 | @IBOutlet weak var fileInfoLabel: UILabel! 23 | 24 | private var exportSession: VIExportSession! 25 | 26 | fileprivate var pickedAsset: AVAsset? 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | 31 | } 32 | 33 | @IBAction func addAction(_ sender: Any) { 34 | let imagePicker = UIImagePickerController() 35 | imagePicker.allowsEditing = true 36 | imagePicker.sourceType = .photoLibrary 37 | imagePicker.mediaTypes = [kUTTypeMovie as String] 38 | imagePicker.delegate = self 39 | present(imagePicker, animated: true, completion: nil) 40 | } 41 | 42 | @IBAction func exportAction(_ sender: UIButton) { 43 | guard let asset = pickedAsset else { 44 | return 45 | } 46 | exportSession = VIExportSession.init(asset: asset) 47 | 48 | if let track = asset.tracks(withMediaType: .video).first { 49 | configureExportConfiguration(videoTrack: track) 50 | } 51 | 52 | exportButton.isEnabled = false 53 | exportSession.progressHandler = { [weak self] (progress) in 54 | guard let strongSelf = self else { return } 55 | DispatchQueue.main.async { 56 | strongSelf.progressView.progress = progress 57 | strongSelf.statusLabel.text = "Exporting \(Int(progress * 100))%" 58 | } 59 | } 60 | exportSession.completionHandler = { [weak self] (error) in 61 | guard let strongSelf = self else { return } 62 | DispatchQueue.main.async { 63 | if let error = error { 64 | strongSelf.statusLabel.text = error.localizedDescription 65 | } else { 66 | strongSelf.statusLabel.text = "Finished" 67 | strongSelf.saveFileToPhotos(fileURL: strongSelf.exportSession.exportConfiguration.outputURL) 68 | } 69 | strongSelf.exportButton.isEnabled = true 70 | } 71 | } 72 | exportSession.export() 73 | } 74 | 75 | @IBAction func cancelAction(_ sender: UIButton) { 76 | exportSession.cancelExport() 77 | } 78 | 79 | private func saveFileToPhotos(fileURL: URL) { 80 | PHPhotoLibrary.shared().performChanges({ 81 | PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: fileURL) 82 | }) { [weak self] (saved, error) in 83 | guard let strongSelf = self else { return } 84 | if saved { 85 | let alertController = UIAlertController(title: "😀 Your video was successfully saved", message: nil, preferredStyle: .alert) 86 | let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil) 87 | alertController.addAction(defaultAction) 88 | strongSelf.present(alertController, animated: true, completion: nil) 89 | } else { 90 | let errorMessage = error?.localizedDescription ?? "" 91 | let alertController = UIAlertController(title: "😢 Video can't save to Photos.app, error: \(errorMessage)", message: nil, preferredStyle: .alert) 92 | let defaultAction = UIAlertAction(title: "OK", style: .default, handler: nil) 93 | alertController.addAction(defaultAction) 94 | strongSelf.present(alertController, animated: true, completion: nil) 95 | } 96 | } 97 | } 98 | 99 | // MARK: - Helper 100 | 101 | func configureExportConfiguration(videoTrack: AVAssetTrack) { 102 | exportSession.videoConfiguration.videoOutputSetting = { 103 | let frameRate = 30 104 | let bitrate = min(2000000, videoTrack.estimatedDataRate) 105 | let trackDimensions = videoTrack.naturalSize 106 | let compressionSettings: [String: Any] = [ 107 | AVVideoAverageNonDroppableFrameRateKey: frameRate, 108 | AVVideoAverageBitRateKey: bitrate, 109 | AVVideoMaxKeyFrameIntervalKey: 30, 110 | AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel 111 | ] 112 | var videoSettings: [String : Any] = [ 113 | AVVideoWidthKey: trackDimensions.width, 114 | AVVideoHeightKey: trackDimensions.height, 115 | AVVideoCompressionPropertiesKey: compressionSettings 116 | ] 117 | if #available(iOS 11.0, *) { 118 | videoSettings[AVVideoCodecKey] = AVVideoCodecType.h264 119 | } else { 120 | videoSettings[AVVideoCodecKey] = AVVideoCodecH264 121 | } 122 | return videoSettings 123 | }() 124 | 125 | exportSession.audioConfiguration.audioOutputSetting = { 126 | var stereoChannelLayout = AudioChannelLayout() 127 | memset(&stereoChannelLayout, 0, MemoryLayout.size) 128 | stereoChannelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo 129 | 130 | let channelLayoutAsData = Data(bytes: &stereoChannelLayout, count: MemoryLayout.size) 131 | let compressionAudioSettings: [String: Any] = [ 132 | AVFormatIDKey: kAudioFormatMPEG4AAC, 133 | AVEncoderBitRateKey: 128000, 134 | AVSampleRateKey: 44100, 135 | AVChannelLayoutKey: channelLayoutAsData, 136 | AVNumberOfChannelsKey: 2 137 | ] 138 | return compressionAudioSettings 139 | }() 140 | } 141 | 142 | fileprivate func updatePickedAsset(_ asset: AVAsset) { 143 | pickedAsset = asset 144 | 145 | let imageGenerator = AVAssetImageGenerator(asset: asset) 146 | imageGenerator.appliesPreferredTrackTransform = true 147 | let width = UIScreen.main.bounds.width 148 | imageGenerator.maximumSize = CGSize(width: width, height: width) 149 | imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: kCMTimeZero)]) { [weak self] (time, image, actualTime, result, error) in 150 | guard let strongSelf = self else { return } 151 | if let image = image { 152 | DispatchQueue.main.async { 153 | strongSelf.coverImageView.backgroundColor = UIColor.clear 154 | strongSelf.coverImageView.image = UIImage(cgImage: image) 155 | } 156 | } else { 157 | print("load thumb image failed") 158 | DispatchQueue.main.async { 159 | strongSelf.coverImageView.backgroundColor = UIColor.red.withAlphaComponent(0.7) 160 | strongSelf.coverImageView.image = nil 161 | } 162 | } 163 | } 164 | 165 | var infoText = "duration: \(String(format: "%.2f", asset.duration.seconds))" 166 | 167 | let size = asset.tracks(withMediaType: .video).first!.naturalSize 168 | infoText.append("\nresolution: \(size)") 169 | 170 | let framerate = asset.tracks(withMediaType: .video).first!.nominalFrameRate 171 | infoText.append("\nframerate: \(String(format: "%.2f", framerate))") 172 | 173 | let bitrate = asset.tracks(withMediaType: .video).first!.estimatedDataRate 174 | infoText.append("\nbitrate: \(String(format: "%.2f", bitrate / 1000))kb") 175 | 176 | let transform = asset.tracks(withMediaType: .video).first!.preferredTransform 177 | let angleDegress = atan2(transform.b, transform.a) * 180 / CGFloat.pi 178 | infoText.append("\nangle degress: \(String(format: "%.0f", angleDegress))") 179 | 180 | fileInfoLabel.text = infoText 181 | } 182 | 183 | } 184 | 185 | extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 186 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 187 | picker.dismiss(animated: true, completion: nil) 188 | } 189 | 190 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 191 | picker.dismiss(animated: true, completion: nil) 192 | if let videoURL = info[UIImagePickerControllerMediaURL] as? URL { 193 | let asset = AVURLAsset(url: videoURL) 194 | updatePickedAsset(asset) 195 | } 196 | } 197 | } 198 | 199 | --------------------------------------------------------------------------------