├── .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 |
88 |
89 |
90 |
91 |
92 |
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 |
--------------------------------------------------------------------------------