├── VIExportSession
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Source
│ ├── ExportConfiguration.swift
│ └── VIExportSession.swift
├── Info.plist
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── AppDelegate.swift
└── ViewController.swift
├── VIExportSession.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── vito.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── VIExportSession.podspec
├── LICENSE
├── .gitignore
└── README.md
/VIExportSession/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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.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/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 |
--------------------------------------------------------------------------------