├── .gitignore ├── Sources ├── Example │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── SceneDelegate.swift │ └── ViewController.swift ├── Tests │ ├── LinuxMain.swift │ └── LightCompressorTests │ │ ├── XCTestManifests.swift │ │ └── LightCompressorTests.swift └── LightCompressor │ └── LightCompressor.swift ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── abedelazizshehadeh.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ ├── hasanabuzayed.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── abedelazizshehadeh.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Example.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── hasanabuzayed.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── hasanabuzayed.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── Package.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ -------------------------------------------------------------------------------- /Sources/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import LightCompressorTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += LightCompressorTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Tests/LightCompressorTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(LightCompressorTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcuserdata/abedelazizshehadeh.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbedElazizShe/LightCompressor_iOS/HEAD/.swiftpm/xcode/package.xcworkspace/xcuserdata/abedelazizshehadeh.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Example.xcodeproj/project.xcworkspace/xcuserdata/hasanabuzayed.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AbedElazizShe/LightCompressor_iOS/HEAD/Example.xcodeproj/project.xcworkspace/xcuserdata/hasanabuzayed.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example.xcodeproj/xcuserdata/hasanabuzayed.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Example.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/hasanabuzayed.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LightCompressor.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/Tests/LightCompressorTests/LightCompressorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import LightCompressor 3 | 4 | final class LightCompressorTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(LightCompressor().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/abedelazizshehadeh.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | LightCompressor.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | LightCompressor 16 | 17 | primary 18 | 19 | 20 | LightCompressorTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "LightCompressor", 8 | platforms: [ 9 | .iOS(.v14), .tvOS(.v14) 10 | ], 11 | products: [ 12 | .library( 13 | name: "LightCompressor", 14 | targets: ["LightCompressor"]), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "LightCompressor", 19 | path: "Sources/LightCompressor" 20 | ), 21 | .testTarget( 22 | name: "LightCompressorTests", 23 | dependencies: ["LightCompressor"], 24 | path: "Sources/Tests" 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | NSPhotoLibraryUsageDescription 25 | ${PRODUCT_MODULE_NAME} library Usage 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Abed Elaziz Shehadeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Hasan Abuzayed on 6/29/22. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sources/Example/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 | -------------------------------------------------------------------------------- /Sources/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Example/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Example 4 | // 5 | // Created by Hasan Abuzayed on 6/29/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LightCompressor 2 | 3 | A powerful and easy-to-use video compression swift package for iOS. It generates a compressed MP4 video with a modified width, height, and bitrate (the number of bits per seconds that determines the video and audio files’ size and quality). It is based on [LightCompressor](https://github.com/AbedElazizShe/LightCompressor) for Android. 4 | 5 | The general idea of how the library works is that, extreme high bitrate is reduced while maintaining a good video quality resulting in a smaller size. 6 | 7 | I would like to mention that the set attributes for size and quality worked just great in my projects and met the expectations. It may or may not meet yours. I’d appreciate your feedback so I can enhance the compression process. 8 | 9 | ## How it works 10 | When the video file is called to be compressed, the library checks if the user wants to set a min bitrate to avoid compressing low resolution videos. This becomes handy if you don’t want the video to be compressed every time it is to be processed to avoid having very bad quality after multiple rounds of compression. The minimum bitrate set is 2mbps. 11 | 12 | You can pass one of 5 video qualities; `.very_high`, `.high`, `.medium`, `.low` or `.very_low` and the package will handle generating the right bitrate and size values for the output video. 13 | 14 | 15 | Usage 16 | -------- 17 | To import this swift package to your XCode project follow the following; 18 | - Go to File -> Swift Packages then choose to **Add package dependency**. 19 | - Add `https://github.com/AbedElazizShe/LightCompressor_iOS.git`, to the text field shown on the popup window and click next. 20 | - Specifiy the minimum release version and confirm. The project will be imported and you can start using it. 21 | - In case on a new release update, you can choose File -> Swift packages and then click on `Update to latest package version`. 22 | 23 | In order to use the compressor, just call [compressVideo()] and pass the following: 24 | 25 | - A list of videos to be compressed where each video contains: 26 | - source: which is the source path of the input video. **required** 27 | - destination: which is the path where the output video should be saved. **required** 28 | - configuration: a set of configurations to control video compression - see configuration below. **optional** 29 | 30 | - Callbacks: the method has a callback for 5 functions; 31 | 1) onStart - called when compression started. 32 | 2) onSuccess - called when compression completed with no errors/exceptions. 33 | 3) onFailure - called when an exception occurred or video bitrate and size are below the minimum required for compression. 34 | 4) onProgress - called with progress new value. 35 | 5) onCancelled - called when the job is cancelled. 36 | 37 | ### Configuration 38 | 39 | - VideoQuality: VERY_HIGH (original-bitrate * 0.6) , HIGH (original-bitrate * 0.4), MEDIUM (original-bitrate * 0.3), LOW (original-bitrate * 0.2), OR VERY_LOW (original-bitrate * 0.1) - .medium by default. 40 | 41 | - isMinBitrateCheckEnabled: this means, don't compress if bitrate is less than 2mbps - true by default. 42 | 43 | - videoBitrateInMbps: any custom bitrate value in Mbps - nil by default. 44 | 45 | - disableAudio: true/false to generate a video without audio - false by default. 46 | 47 | - keepOriginalResolution: true/false to tell the library not to change the resolution - false by default. 48 | 49 | - videoSize: it contains; videoWidth: custom video width, and videoHeight: custom video height - nil by default 50 | 51 | 52 | #### Example 53 | 54 | ```swift 55 | import LightCompressor 56 | ... 57 | 58 | let videoCompressor = LightCompressor() 59 | 60 | compression = videoCompressor.compressVideo(videos: [ 61 | .init( 62 | source: videoToCompress, 63 | destination: destinationPath, 64 | configuration: .init( 65 | quality: VideoQuality.very_high, 66 | videoBitrateInMbps: 5, 67 | disableAudio: false, 68 | keepOriginalResolution: 69 | false, videoSize: 70 | CGSize(width: 360, height: 480) 71 | ) 72 | ) 73 | ], 74 | progressQueue: .main, 75 | progressHandler: { progress in 76 | DispatchQueue.main.async { [unowned self] in 77 | // Handle progress- "\(String(format: "%.0f", progress.fractionCompleted * 100))%" 78 | }}, 79 | 80 | completion: {[weak self] result in 81 | guard let `self` = self else { return } 82 | 83 | switch result { 84 | 85 | case .onSuccess(let index, let path): 86 | // Handle onSuccess 87 | 88 | case .onStart: 89 | // Handle onStart 90 | 91 | case .onFailure(let index, let error): 92 | // Handle onFailure 93 | 94 | case .onCancelled: 95 | // Handle onCancelled 96 | } 97 | }) 98 | 99 | // to cancel call 100 | compression.cancel = true 101 | 102 | ``` 103 | 104 | ## Compatibility 105 | The minimum iOS version supported is 14. 106 | 107 | ## Getting help 108 | For questions, suggestions, or anything else, email elaziz.shehadeh(at)gmail.com. 109 | -------------------------------------------------------------------------------- /Sources/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // lightcompressorsample 4 | // 5 | // Created by AbedElaziz shehadeh on 28/08/2020. 6 | // Copyright © 2020 AbedElaziz shehadeh. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MobileCoreServices 11 | import AVKit 12 | import Photos 13 | import LightCompressor 14 | 15 | class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { 16 | 17 | @IBOutlet weak var videoView: UIImageView! 18 | @IBOutlet weak var originalSize: UILabel! 19 | @IBOutlet weak var sizeAfterCompression: UILabel! 20 | @IBOutlet weak var duration: UILabel! 21 | @IBOutlet weak var progressView: UIStackView! 22 | @IBOutlet weak var progressBar: UIProgressView! 23 | @IBOutlet weak var progressLabel: UILabel! 24 | 25 | private var imagePickerController: UIImagePickerController? 26 | private var compression: Compression? 27 | 28 | private var compressedPath: URL? 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | // create tap gesture recognizer 33 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(imageTapped(gesture:))) 34 | 35 | // add it to the image view; 36 | videoView.addGestureRecognizer(tapGesture) 37 | // make sure imageView can be interacted with by user 38 | videoView.isUserInteractionEnabled = true 39 | 40 | } 41 | 42 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 43 | self.imagePickerController?.dismiss(animated: true, completion: nil) 44 | 45 | // Get source video 46 | let videoToCompress = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL")] as! URL 47 | 48 | let thumbnail = createThumbnailOfVideoFromFileURL(videoURL: videoToCompress.absoluteString) 49 | videoView.image = UIImage(cgImage: thumbnail!) 50 | 51 | DispatchQueue.main.async { [unowned self] in 52 | self.originalSize.isHidden = false 53 | self.originalSize.text = "Original size: \(videoToCompress.fileSizeInMB())" 54 | } 55 | 56 | // Declare destination path and remove anything exists in it 57 | let destinationPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("compressed.mp4") 58 | try? FileManager.default.removeItem(at: destinationPath) 59 | 60 | let startingPoint = Date() 61 | let videoCompressor = LightCompressor() 62 | 63 | compression = videoCompressor.compressVideo(videos: [.init(source: videoToCompress, destination: destinationPath, configuration: .init(quality: VideoQuality.very_high, videoBitrateInMbps: 5, disableAudio: false, keepOriginalResolution: false, videoSize: CGSize(width: 360, height: 480) ))], 64 | progressQueue: .main, 65 | progressHandler: { progress in 66 | DispatchQueue.main.async { [unowned self] in 67 | self.progressBar.progress = Float(progress.fractionCompleted) 68 | self.progressLabel.text = "\(String(format: "%.0f", progress.fractionCompleted * 100))%" 69 | }}, 70 | 71 | completion: {[weak self] result in 72 | guard let `self` = self else { return } 73 | 74 | switch result { 75 | 76 | case .onSuccess(let index, let path): 77 | self.compressedPath = path 78 | DispatchQueue.main.async { [unowned self] in 79 | self.sizeAfterCompression.isHidden = false 80 | self.duration.isHidden = false 81 | self.progressBar.isHidden = true 82 | self.progressLabel.isHidden = true 83 | 84 | self.sizeAfterCompression.text = "Size after compression: \(path.fileSizeInMB())" 85 | self.duration.text = "Duration: \(String(format: "%.2f", startingPoint.timeIntervalSinceNow * -1)) seconds" 86 | 87 | PHPhotoLibrary.shared().performChanges({ 88 | PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: path) 89 | }) 90 | } 91 | 92 | case .onStart: 93 | self.progressBar.isHidden = false 94 | self.progressLabel.isHidden = false 95 | self.sizeAfterCompression.isHidden = true 96 | self.duration.isHidden = true 97 | //self.originalSize.visiblity(gone: false) 98 | 99 | case .onFailure(let index, let error): 100 | self.progressBar.isHidden = true 101 | self.progressLabel.isHidden = false 102 | self.progressLabel.text = (error as! CompressionError).title 103 | 104 | 105 | case .onCancelled: 106 | print("---------------------------") 107 | print("Cancelled") 108 | print("---------------------------") 109 | } 110 | }) 111 | 112 | } 113 | 114 | @IBAction func pickVideoPressed(_ sender: UIButton) { 115 | 116 | DispatchQueue.main.async { [unowned self] in 117 | self.imagePickerController = UIImagePickerController() 118 | self.imagePickerController?.delegate = self 119 | self.imagePickerController?.sourceType = .photoLibrary 120 | self.imagePickerController?.mediaTypes = ["public.movie"] 121 | self.imagePickerController?.videoQuality = UIImagePickerController.QualityType.typeHigh 122 | self.imagePickerController?.videoExportPreset = AVAssetExportPresetPassthrough 123 | self.present(self.imagePickerController!, animated: true, completion: nil) 124 | } 125 | } 126 | 127 | @IBAction func cancelPressed(_ sender: UIButton) { 128 | compression?.cancel = true 129 | } 130 | 131 | @objc func imageTapped(gesture: UIGestureRecognizer) { 132 | // if the tapped view is a UIImageView then set it to imageview 133 | if (gesture.view as? UIImageView) != nil { 134 | 135 | DispatchQueue.main.async { [unowned self] in 136 | let player = AVPlayer(url: self.compressedPath! as URL) 137 | let controller = AVPlayerViewController() 138 | controller.player = player 139 | self.present(controller, animated: true) { 140 | player.play() 141 | } 142 | } 143 | 144 | } 145 | } 146 | 147 | func createThumbnailOfVideoFromFileURL(videoURL: String) -> CGImage? { 148 | let asset = AVAsset(url: URL(string: videoURL)!) 149 | let assetImgGenerate = AVAssetImageGenerator(asset: asset) 150 | assetImgGenerate.appliesPreferredTrackTransform = true 151 | let time = CMTimeMakeWithSeconds(Float64(1), preferredTimescale: 100) 152 | do { 153 | let img = try assetImgGenerate.copyCGImage(at: time, actualTime: nil) 154 | return img 155 | } catch { 156 | return nil 157 | } 158 | } 159 | } 160 | 161 | 162 | extension URL { 163 | func fileSizeInMB() -> String { 164 | let p = self.path 165 | 166 | let attr = try? FileManager.default.attributesOfItem(atPath: p) 167 | 168 | if let attr = attr { 169 | let fileSize = Float(attr[FileAttributeKey.size] as! UInt64) / (1024.0 * 1024.0) 170 | 171 | return String(format: "%.2f MB", fileSize) 172 | } else { 173 | return "Failed to get size" 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sources/Example/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 | 33 | 39 | 45 | 46 | 47 | 48 | 49 | 50 | 67 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 99 | 100 | 101 | 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 | -------------------------------------------------------------------------------- /Sources/LightCompressor/LightCompressor.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import UIKit 3 | 4 | public enum VideoQuality { 5 | case very_high 6 | case high 7 | case medium 8 | case low 9 | case very_low 10 | } 11 | 12 | // Compression Result 13 | public enum CompressionResult { 14 | case onStart 15 | case onSuccess(Int, URL) 16 | case onFailure(Int, CompressionError) 17 | case onCancelled 18 | } 19 | 20 | // Compression Interruption Wrapper 21 | public class Compression { 22 | public init() {} 23 | 24 | public var cancel = false 25 | } 26 | 27 | // Compression Error Messages 28 | public struct CompressionError: LocalizedError { 29 | public let title: String 30 | 31 | init(title: String = "Compression Error") { 32 | self.title = title 33 | } 34 | } 35 | 36 | @available(iOS 14.0, *) 37 | public struct LightCompressor { 38 | public struct Video { 39 | public struct Configuration { 40 | public let quality: VideoQuality 41 | public let isMinBitrateCheckEnabled: Bool 42 | public let videoBitrateInMbps: Int? 43 | public let disableAudio: Bool 44 | public let keepOriginalResolution: Bool 45 | public let videoSize: CGSize? 46 | 47 | public init( 48 | quality: VideoQuality = .medium, 49 | isMinBitrateCheckEnabled: Bool = true, 50 | videoBitrateInMbps: Int? = nil, 51 | disableAudio: Bool = false, 52 | keepOriginalResolution: Bool = false, 53 | videoSize: CGSize? = nil 54 | ) { 55 | self.quality = quality 56 | self.isMinBitrateCheckEnabled = isMinBitrateCheckEnabled 57 | self.videoBitrateInMbps = videoBitrateInMbps 58 | self.disableAudio = disableAudio 59 | self.keepOriginalResolution = keepOriginalResolution 60 | self.videoSize = videoSize 61 | } 62 | } 63 | 64 | public let source: URL 65 | public let destination: URL 66 | public let configuration: Configuration 67 | public init( 68 | source: URL, 69 | destination: URL, 70 | configuration: Configuration = Configuration() 71 | ) { 72 | self.source = source 73 | self.destination = destination 74 | self.configuration = configuration 75 | } 76 | } 77 | 78 | public init() {} 79 | 80 | private let MIN_BITRATE = Float(2000000) 81 | private let MIN_HEIGHT = 640.0 82 | private let MIN_WIDTH = 360.0 83 | 84 | /** 85 | * This function compresses a given list of [video] files and writes the compressed video file at 86 | * [destination] 87 | * 88 | * @param [videos] the list of videos to be compressed. Each video object should have [source], [destination], and an optional [configuration] where: 89 | * - [source] is the source path of the video 90 | * - [destination] the path where the output compressed video file should be saved 91 | * - [configuration] is the custom configuration to control compression parameters for the video to be compressed. The configurations include: 92 | * - [quality] to allow choosing a video quality that can be [.very_low], [.low], [.medium], [.high], and [very_high]. This defaults to [.medium] 93 | * - [isMinBitrateCheckEnabled] to determine if the checking for a minimum bitrate threshold before compression is enabled or not. This default to `true` 94 | * - [videoBitrateInMbps] which is a custom bitrate for the video 95 | * - [keepOriginalResolution] to keep the original video height and width when compressing. This defaults to `false` 96 | * - [VideoSize] which is a custom height and width for the video 97 | * @param [progressHandler] a compression progress listener that listens to compression progress status 98 | * @param [completion] to return completion status that can be [onStart], [onSuccess], [onFailure], 99 | * and if the compression was [onCancelled] 100 | */ 101 | 102 | public func compressVideo(videos: [Video], 103 | progressQueue: DispatchQueue = .main, 104 | progressHandler: ((Progress) -> ())?, 105 | completion: @escaping (CompressionResult) -> ()) -> Compression { 106 | let compressionOperation = Compression() 107 | 108 | guard !videos.isEmpty else { 109 | return compressionOperation 110 | } 111 | 112 | var frameCount = 0 113 | 114 | for (index, video) in videos.enumerated() { 115 | let source = video.source 116 | let destination = video.destination 117 | let configuration = video.configuration 118 | 119 | // Compression started 120 | completion(.onStart) 121 | 122 | let videoAsset = AVURLAsset(url: source) 123 | guard let videoTrack = videoAsset.tracks(withMediaType: AVMediaType.video).first else { 124 | let error = CompressionError(title: "Cannot find video track") 125 | completion(.onFailure(index, error)) 126 | continue 127 | } 128 | 129 | let bitrate = videoTrack.estimatedDataRate 130 | // Check for a min video bitrate before compression 131 | if configuration.isMinBitrateCheckEnabled && bitrate <= MIN_BITRATE { 132 | let error = CompressionError(title: "The provided bitrate is smaller than what is needed for compression try to set isMinBitRateEnabled to false") 133 | completion(.onFailure(index, error)) 134 | continue 135 | } 136 | 137 | // Generate a bitrate based on desired quality 138 | let newBitrate = configuration.videoBitrateInMbps == nil ? 139 | getBitrate(bitrate: bitrate, quality: configuration.quality) : 140 | configuration.videoBitrateInMbps! * 1000000 141 | 142 | // Handle new width and height values 143 | let videoSize = videoTrack.naturalSize 144 | let size: (width: Int, height: Int) = configuration.videoSize == nil ? 145 | generateWidthAndHeight(width: videoSize.width, height: videoSize.height, keepOriginalResolution: configuration.keepOriginalResolution) 146 | : (Int(configuration.videoSize!.width), Int(configuration.videoSize!.height)) 147 | 148 | let newWidth = size.width 149 | let newHeight = size.height 150 | 151 | // Total Frames 152 | let durationInSeconds = videoAsset.duration.seconds 153 | let frameRate = videoTrack.nominalFrameRate 154 | let totalFrames = ceil(durationInSeconds * Double(frameRate)) 155 | 156 | // Progress 157 | let totalUnits = Int64(totalFrames) 158 | let progress = Progress(totalUnitCount: totalUnits) 159 | 160 | // Setup video writer input 161 | let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: getVideoWriterSettings(bitrate: newBitrate, width: newWidth, height: newHeight)) 162 | videoWriterInput.expectsMediaDataInRealTime = true 163 | videoWriterInput.transform = videoTrack.preferredTransform 164 | 165 | let videoWriter = try? AVAssetWriter(outputURL: destination, fileType: AVFileType.mov) 166 | videoWriter?.add(videoWriterInput) 167 | 168 | // Setup video reader output 169 | let videoReaderSettings:[String : AnyObject] = [ 170 | kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) as AnyObject 171 | ] 172 | let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings) 173 | 174 | var videoReader: AVAssetReader? 175 | do { 176 | videoReader = try AVAssetReader(asset: videoAsset) 177 | } catch { 178 | let compressionError = CompressionError(title: error.localizedDescription) 179 | completion(.onFailure(index, compressionError)) 180 | continue 181 | } 182 | 183 | videoReader?.add(videoReaderOutput) 184 | //setup audio writer 185 | let audioWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil) 186 | audioWriterInput.expectsMediaDataInRealTime = false 187 | videoWriter?.add(audioWriterInput) 188 | //setup audio reader 189 | let audioTrack = videoAsset.tracks(withMediaType: AVMediaType.audio).first 190 | var audioReader: AVAssetReader? 191 | var audioReaderOutput: AVAssetReaderTrackOutput? 192 | if(audioTrack != nil) { 193 | audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack!, outputSettings: nil) 194 | audioReader = try? AVAssetReader(asset: videoAsset) 195 | audioReader?.add(audioReaderOutput!) 196 | } 197 | videoWriter?.startWriting() 198 | 199 | //start writing from video reader 200 | videoReader?.startReading() 201 | videoWriter?.startSession(atSourceTime: CMTime.zero) 202 | let processingQueue = DispatchQueue(label: "processingQueue1", qos: .background) 203 | 204 | var isFirstBuffer = true 205 | videoWriterInput.requestMediaDataWhenReady(on: processingQueue, using: {() -> Void in 206 | while videoWriterInput.isReadyForMoreMediaData { 207 | 208 | // Observe any cancellation 209 | if compressionOperation.cancel { 210 | videoReader?.cancelReading() 211 | videoWriter?.cancelWriting() 212 | completion(.onCancelled) 213 | return 214 | } 215 | 216 | // Update progress based on number of processed frames 217 | frameCount += 1 218 | if let handler = progressHandler { 219 | progress.completedUnitCount = Int64(frameCount) 220 | progressQueue.async { handler(progress) } 221 | } 222 | 223 | let sampleBuffer: CMSampleBuffer? = videoReaderOutput.copyNextSampleBuffer() 224 | 225 | if videoReader?.status == .reading && sampleBuffer != nil { 226 | videoWriterInput.append(sampleBuffer!) 227 | } else { 228 | videoWriterInput.markAsFinished() 229 | if videoReader?.status == .completed { 230 | if audioReader != nil && !configuration.disableAudio { 231 | if !(audioReader!.status == .reading) || !(audioReader!.status == .completed) { 232 | //start writing from audio reader 233 | audioReader?.startReading() 234 | videoWriter?.startSession(atSourceTime: CMTime.zero) 235 | let processingQueue = DispatchQueue(label: "processingQueue2", qos: .background) 236 | 237 | audioWriterInput.requestMediaDataWhenReady(on: processingQueue, using: { 238 | while audioWriterInput.isReadyForMoreMediaData { 239 | let sampleBuffer: CMSampleBuffer? = audioReaderOutput?.copyNextSampleBuffer() 240 | if audioReader?.status == .reading && sampleBuffer != nil { 241 | if isFirstBuffer { 242 | let dict = CMTimeCopyAsDictionary(CMTimeMake(value: 1024, timescale: 44100), allocator: kCFAllocatorDefault); 243 | CMSetAttachment(sampleBuffer as CMAttachmentBearer, key: kCMSampleBufferAttachmentKey_TrimDurationAtStart, value: dict, attachmentMode: kCMAttachmentMode_ShouldNotPropagate); 244 | isFirstBuffer = false 245 | } 246 | audioWriterInput.append(sampleBuffer!) 247 | } else { 248 | audioWriterInput.markAsFinished() 249 | 250 | videoWriter?.finishWriting { 251 | completion(.onSuccess(index, destination)) 252 | } 253 | } 254 | } 255 | }) 256 | } 257 | } else { 258 | videoWriter?.finishWriting { 259 | completion(.onSuccess(index, destination)) 260 | } 261 | } 262 | } 263 | } 264 | } 265 | }) 266 | } 267 | return compressionOperation 268 | } 269 | 270 | private func getBitrate(bitrate: Float, quality: VideoQuality) -> Int { 271 | switch quality { 272 | case .very_high: 273 | return Int(bitrate * 0.6) 274 | case .high: 275 | return Int(bitrate * 0.4) 276 | case .medium: 277 | return Int(bitrate * 0.3) 278 | case .low: 279 | return Int(bitrate * 0.2) 280 | case .very_low: 281 | return Int(bitrate * 0.1) 282 | } 283 | } 284 | 285 | private func generateWidthAndHeight( 286 | width: CGFloat, 287 | height: CGFloat, 288 | keepOriginalResolution: Bool 289 | ) -> (width: Int, height: Int) { 290 | 291 | if (keepOriginalResolution) { 292 | return (Int(width), Int(height)) 293 | } 294 | 295 | var newWidth: Int 296 | var newHeight: Int 297 | 298 | if width >= 1920 || height >= 1920 { 299 | 300 | newWidth = Int(width * 0.5 / 16) * 16 301 | newHeight = Int(height * 0.5 / 16 ) * 16 302 | 303 | } else if width >= 1280 || height >= 1280 { 304 | newWidth = Int(width * 0.75 / 16) * 16 305 | newHeight = Int(height * 0.75 / 16) * 16 306 | } else if width >= 960 || height >= 960 { 307 | if(width > height){ 308 | newWidth = Int(MIN_HEIGHT * 0.95 / 16) * 16 309 | newHeight = Int(MIN_WIDTH * 0.95 / 16) * 16 310 | } else { 311 | newWidth = Int(MIN_WIDTH * 0.95 / 16) * 16 312 | newHeight = Int(MIN_HEIGHT * 0.95 / 16) * 16 313 | } 314 | } else { 315 | newWidth = Int(width * 0.9 / 16) * 16 316 | newHeight = Int(height * 0.9 / 16) * 16 317 | } 318 | 319 | return (newWidth, newHeight) 320 | } 321 | 322 | private func getVideoWriterSettings(bitrate: Int, width: Int, height: Int) -> [String : AnyObject] { 323 | 324 | let videoWriterCompressionSettings = [ 325 | AVVideoAverageBitRateKey : bitrate 326 | ] 327 | 328 | let videoWriterSettings: [String : AnyObject] = [ 329 | AVVideoCodecKey : AVVideoCodecType.h264 as AnyObject, 330 | AVVideoCompressionPropertiesKey : videoWriterCompressionSettings as AnyObject, 331 | AVVideoWidthKey : width as AnyObject, 332 | AVVideoHeightKey : height as AnyObject 333 | ] 334 | 335 | return videoWriterSettings 336 | } 337 | 338 | } 339 | -------------------------------------------------------------------------------- /Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 341B8CB2286D3FE0005A6252 /* LightCompressor in Frameworks */ = {isa = PBXBuildFile; productRef = 341B8CB1286D3FE0005A6252 /* LightCompressor */; }; 11 | 34C5E5B4286D3D0800DC3F2E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C5E5B3286D3D0800DC3F2E /* AppDelegate.swift */; }; 12 | 34C5E5B6286D3D0800DC3F2E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C5E5B5286D3D0800DC3F2E /* SceneDelegate.swift */; }; 13 | 34C5E5B8286D3D0800DC3F2E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C5E5B7286D3D0800DC3F2E /* ViewController.swift */; }; 14 | 34C5E5BB286D3D0800DC3F2E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 34C5E5B9286D3D0800DC3F2E /* Main.storyboard */; }; 15 | 34C5E5BD286D3D0900DC3F2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 34C5E5BC286D3D0900DC3F2E /* Assets.xcassets */; }; 16 | 34C5E5C0286D3D0900DC3F2E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 34C5E5BE286D3D0900DC3F2E /* LaunchScreen.storyboard */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 34C5E5B0286D3D0800DC3F2E /* Example */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = Example; path = ../../../../../../../../AndroidStudioProjects/LightCompressor_iOS/Sources/Example; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 34C5E5B3286D3D0800DC3F2E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 34C5E5B5286D3D0800DC3F2E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 23 | 34C5E5B7286D3D0800DC3F2E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 24 | 34C5E5BA286D3D0800DC3F2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 25 | 34C5E5BC286D3D0900DC3F2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 34C5E5BF286D3D0900DC3F2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 27 | 34C5E5C1286D3D0900DC3F2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | 34C5E5C8286D3D8D00DC3F2E /* */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ""; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 34C5E5AD286D3D0800DC3F2E /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | 341B8CB2286D3FE0005A6252 /* LightCompressor in Frameworks */, 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 341B8CB0286D3FE0005A6252 /* Frameworks */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | ); 47 | name = Frameworks; 48 | sourceTree = ""; 49 | }; 50 | 34C5E5A7286D3D0800DC3F2E = { 51 | isa = PBXGroup; 52 | children = ( 53 | 34C5E5C7286D3D8D00DC3F2E /* Packages */, 54 | 34C5E5B2286D3D0800DC3F2E /* Example */, 55 | 34C5E5B1286D3D0800DC3F2E /* Products */, 56 | 341B8CB0286D3FE0005A6252 /* Frameworks */, 57 | ); 58 | sourceTree = ""; 59 | }; 60 | 34C5E5B1286D3D0800DC3F2E /* Products */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 34C5E5B0286D3D0800DC3F2E /* Example */, 64 | ); 65 | name = Products; 66 | sourceTree = ""; 67 | }; 68 | 34C5E5B2286D3D0800DC3F2E /* Example */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 34C5E5B3286D3D0800DC3F2E /* AppDelegate.swift */, 72 | 34C5E5B5286D3D0800DC3F2E /* SceneDelegate.swift */, 73 | 34C5E5B7286D3D0800DC3F2E /* ViewController.swift */, 74 | 34C5E5B9286D3D0800DC3F2E /* Main.storyboard */, 75 | 34C5E5BC286D3D0900DC3F2E /* Assets.xcassets */, 76 | 34C5E5BE286D3D0900DC3F2E /* LaunchScreen.storyboard */, 77 | 34C5E5C1286D3D0900DC3F2E /* Info.plist */, 78 | ); 79 | name = Example; 80 | path = Sources/Example; 81 | sourceTree = ""; 82 | }; 83 | 34C5E5C7286D3D8D00DC3F2E /* Packages */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 34C5E5C8286D3D8D00DC3F2E /* */, 87 | ); 88 | name = Packages; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 34C5E5AF286D3D0800DC3F2E /* Example */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 34C5E5C4286D3D0900DC3F2E /* Build configuration list for PBXNativeTarget "Example" */; 97 | buildPhases = ( 98 | 34C5E5AC286D3D0800DC3F2E /* Sources */, 99 | 34C5E5AD286D3D0800DC3F2E /* Frameworks */, 100 | 34C5E5AE286D3D0800DC3F2E /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = Example; 107 | packageProductDependencies = ( 108 | 341B8CB1286D3FE0005A6252 /* LightCompressor */, 109 | ); 110 | productName = Example; 111 | productReference = 34C5E5B0286D3D0800DC3F2E /* Example */; 112 | productType = "com.apple.product-type.application"; 113 | }; 114 | /* End PBXNativeTarget section */ 115 | 116 | /* Begin PBXProject section */ 117 | 34C5E5A8286D3D0800DC3F2E /* Project object */ = { 118 | isa = PBXProject; 119 | attributes = { 120 | BuildIndependentTargetsInParallel = 1; 121 | LastSwiftUpdateCheck = 1340; 122 | LastUpgradeCheck = 1340; 123 | TargetAttributes = { 124 | 34C5E5AF286D3D0800DC3F2E = { 125 | CreatedOnToolsVersion = 13.4; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 34C5E5AB286D3D0800DC3F2E /* Build configuration list for PBXProject "Example" */; 130 | compatibilityVersion = "Xcode 13.0"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = 34C5E5A7286D3D0800DC3F2E; 138 | productRefGroup = 34C5E5B1286D3D0800DC3F2E /* Products */; 139 | projectDirPath = ""; 140 | projectRoot = ""; 141 | targets = ( 142 | 34C5E5AF286D3D0800DC3F2E /* Example */, 143 | ); 144 | }; 145 | /* End PBXProject section */ 146 | 147 | /* Begin PBXResourcesBuildPhase section */ 148 | 34C5E5AE286D3D0800DC3F2E /* Resources */ = { 149 | isa = PBXResourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | 34C5E5C0286D3D0900DC3F2E /* LaunchScreen.storyboard in Resources */, 153 | 34C5E5BD286D3D0900DC3F2E /* Assets.xcassets in Resources */, 154 | 34C5E5BB286D3D0800DC3F2E /* Main.storyboard in Resources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXResourcesBuildPhase section */ 159 | 160 | /* Begin PBXSourcesBuildPhase section */ 161 | 34C5E5AC286D3D0800DC3F2E /* Sources */ = { 162 | isa = PBXSourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 34C5E5B8286D3D0800DC3F2E /* ViewController.swift in Sources */, 166 | 34C5E5B4286D3D0800DC3F2E /* AppDelegate.swift in Sources */, 167 | 34C5E5B6286D3D0800DC3F2E /* SceneDelegate.swift in Sources */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXSourcesBuildPhase section */ 172 | 173 | /* Begin PBXVariantGroup section */ 174 | 34C5E5B9286D3D0800DC3F2E /* Main.storyboard */ = { 175 | isa = PBXVariantGroup; 176 | children = ( 177 | 34C5E5BA286D3D0800DC3F2E /* Base */, 178 | ); 179 | name = Main.storyboard; 180 | sourceTree = ""; 181 | }; 182 | 34C5E5BE286D3D0900DC3F2E /* LaunchScreen.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | 34C5E5BF286D3D0900DC3F2E /* Base */, 186 | ); 187 | name = LaunchScreen.storyboard; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXVariantGroup section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | 34C5E5C2286D3D0900DC3F2E /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 200 | CLANG_ENABLE_MODULES = YES; 201 | CLANG_ENABLE_OBJC_ARC = YES; 202 | CLANG_ENABLE_OBJC_WEAK = YES; 203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 | CLANG_WARN_BOOL_CONVERSION = YES; 205 | CLANG_WARN_COMMA = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 209 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 210 | CLANG_WARN_EMPTY_BODY = YES; 211 | CLANG_WARN_ENUM_CONVERSION = YES; 212 | CLANG_WARN_INFINITE_RECURSION = YES; 213 | CLANG_WARN_INT_CONVERSION = YES; 214 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 215 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 220 | CLANG_WARN_STRICT_PROTOTYPES = YES; 221 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 223 | CLANG_WARN_UNREACHABLE_CODE = YES; 224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = dwarf; 227 | ENABLE_STRICT_OBJC_MSGSEND = YES; 228 | ENABLE_TESTABILITY = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu11; 230 | GCC_DYNAMIC_NO_PIC = NO; 231 | GCC_NO_COMMON_BLOCKS = YES; 232 | GCC_OPTIMIZATION_LEVEL = 0; 233 | GCC_PREPROCESSOR_DEFINITIONS = ( 234 | "DEBUG=1", 235 | "$(inherited)", 236 | ); 237 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 238 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 239 | GCC_WARN_UNDECLARED_SELECTOR = YES; 240 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 241 | GCC_WARN_UNUSED_FUNCTION = YES; 242 | GCC_WARN_UNUSED_VARIABLE = YES; 243 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 244 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 245 | MTL_FAST_MATH = YES; 246 | ONLY_ACTIVE_ARCH = YES; 247 | SDKROOT = iphoneos; 248 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 249 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 250 | }; 251 | name = Debug; 252 | }; 253 | 34C5E5C3286D3D0900DC3F2E /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | CLANG_ANALYZER_NONNULL = YES; 258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 260 | CLANG_ENABLE_MODULES = YES; 261 | CLANG_ENABLE_OBJC_ARC = YES; 262 | CLANG_ENABLE_OBJC_WEAK = YES; 263 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 264 | CLANG_WARN_BOOL_CONVERSION = YES; 265 | CLANG_WARN_COMMA = YES; 266 | CLANG_WARN_CONSTANT_CONVERSION = YES; 267 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 268 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 269 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 270 | CLANG_WARN_EMPTY_BODY = YES; 271 | CLANG_WARN_ENUM_CONVERSION = YES; 272 | CLANG_WARN_INFINITE_RECURSION = YES; 273 | CLANG_WARN_INT_CONVERSION = YES; 274 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 276 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 278 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 279 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 280 | CLANG_WARN_STRICT_PROTOTYPES = YES; 281 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 282 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 283 | CLANG_WARN_UNREACHABLE_CODE = YES; 284 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 285 | COPY_PHASE_STRIP = NO; 286 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 287 | ENABLE_NS_ASSERTIONS = NO; 288 | ENABLE_STRICT_OBJC_MSGSEND = YES; 289 | GCC_C_LANGUAGE_STANDARD = gnu11; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 292 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 293 | GCC_WARN_UNDECLARED_SELECTOR = YES; 294 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 295 | GCC_WARN_UNUSED_FUNCTION = YES; 296 | GCC_WARN_UNUSED_VARIABLE = YES; 297 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 298 | MTL_ENABLE_DEBUG_INFO = NO; 299 | MTL_FAST_MATH = YES; 300 | SDKROOT = iphoneos; 301 | SWIFT_COMPILATION_MODE = wholemodule; 302 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 303 | VALIDATE_PRODUCT = YES; 304 | }; 305 | name = Release; 306 | }; 307 | 34C5E5C5286D3D0900DC3F2E /* Debug */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 311 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 312 | CODE_SIGN_STYLE = Automatic; 313 | CURRENT_PROJECT_VERSION = 1; 314 | GENERATE_INFOPLIST_FILE = YES; 315 | INFOPLIST_FILE = Sources/Example/Info.plist; 316 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 317 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 318 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 319 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 320 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 321 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 322 | LD_RUNPATH_SEARCH_PATHS = ( 323 | "$(inherited)", 324 | "@executable_path/Frameworks", 325 | ); 326 | MARKETING_VERSION = 1.0; 327 | PRODUCT_BUNDLE_IDENTIFIER = com.abedelazizshe.lightcompressor.Example; 328 | PRODUCT_NAME = "$(TARGET_NAME)"; 329 | SWIFT_EMIT_LOC_STRINGS = YES; 330 | SWIFT_VERSION = 5.0; 331 | TARGETED_DEVICE_FAMILY = "1,2"; 332 | }; 333 | name = Debug; 334 | }; 335 | 34C5E5C6286D3D0900DC3F2E /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 340 | CODE_SIGN_STYLE = Automatic; 341 | CURRENT_PROJECT_VERSION = 1; 342 | GENERATE_INFOPLIST_FILE = YES; 343 | INFOPLIST_FILE = Sources/Example/Info.plist; 344 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 345 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 346 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 347 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 348 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 349 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 350 | LD_RUNPATH_SEARCH_PATHS = ( 351 | "$(inherited)", 352 | "@executable_path/Frameworks", 353 | ); 354 | MARKETING_VERSION = 1.0; 355 | PRODUCT_BUNDLE_IDENTIFIER = com.abedelazizshe.lightcompressor.Example; 356 | PRODUCT_NAME = "$(TARGET_NAME)"; 357 | SWIFT_EMIT_LOC_STRINGS = YES; 358 | SWIFT_VERSION = 5.0; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Release; 362 | }; 363 | /* End XCBuildConfiguration section */ 364 | 365 | /* Begin XCConfigurationList section */ 366 | 34C5E5AB286D3D0800DC3F2E /* Build configuration list for PBXProject "Example" */ = { 367 | isa = XCConfigurationList; 368 | buildConfigurations = ( 369 | 34C5E5C2286D3D0900DC3F2E /* Debug */, 370 | 34C5E5C3286D3D0900DC3F2E /* Release */, 371 | ); 372 | defaultConfigurationIsVisible = 0; 373 | defaultConfigurationName = Release; 374 | }; 375 | 34C5E5C4286D3D0900DC3F2E /* Build configuration list for PBXNativeTarget "Example" */ = { 376 | isa = XCConfigurationList; 377 | buildConfigurations = ( 378 | 34C5E5C5286D3D0900DC3F2E /* Debug */, 379 | 34C5E5C6286D3D0900DC3F2E /* Release */, 380 | ); 381 | defaultConfigurationIsVisible = 0; 382 | defaultConfigurationName = Release; 383 | }; 384 | /* End XCConfigurationList section */ 385 | 386 | /* Begin XCSwiftPackageProductDependency section */ 387 | 341B8CB1286D3FE0005A6252 /* LightCompressor */ = { 388 | isa = XCSwiftPackageProductDependency; 389 | productName = LightCompressor; 390 | }; 391 | /* End XCSwiftPackageProductDependency section */ 392 | }; 393 | rootObject = 34C5E5A8286D3D0800DC3F2E /* Project object */; 394 | } 395 | --------------------------------------------------------------------------------