How can I reduce the file size of a video created with UIImagePickerController?

Viewed 68272

I have an app that allows a user to record a video with UIImagePickerController and then upload it to YouTube. The problem is that the video file that UIImagePickerController creates is HUGE, even when the video is only 5 seconds long. For example, a 5 second long video is 16-20 megabytes. I want to keep the video in 540 or 720 quality, but I want to reduce the file size.

I've been experimenting with AVFoundation and AVAssetExportSession to try to get a smaller file size. I've tried the following code:

AVAsset *video = [AVAsset assetWithURL:videoURL];
AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:video presetName:AVAssetExportPresetPassthrough];
exportSession.shouldOptimizeForNetworkUse = YES;
exportSession.outputFileType = AVFileTypeMPEG4;
exportSession.outputURL = [pathToSavedVideosDirectory URLByAppendingPathComponent:@"vid1.mp4"];
[exportSession exportAsynchronouslyWithCompletionHandler:^{
    NSLog(@"done processing video!");
}];

But this hasn't reduced the file size at all. I know what I'm doing is possible because in Apple's Photos app, when you select "share on YouTube", will automatically process the video file so its small enough to upload. I want to do the same thing in my app.

How can I accomplish this?

13 Answers

Code for Swift 5 and Good Quality

Here's how to do it following code from this link. The problem with the link is it only works with .mov file output, if you want to output a .mp4 file it will crash. The code below lets you get a .mp4 output. It is tried, tested, and works. Example a 15 sec video that is originally 27mb gets reduced to 2mb. If you want better quality raise the bitrate. I have it set at 1250000.

c+p this code:

import AVFoundation

// add these properties
var assetWriter: AVAssetWriter!
var assetWriterVideoInput: AVAssetWriterInput!
var audioMicInput: AVAssetWriterInput!
var videoURL: URL!
var audioAppInput: AVAssetWriterInput!
var channelLayout = AudioChannelLayout()
var assetReader: AVAssetReader?
let bitrate: NSNumber = NSNumber(value: 1250000) // *** you can change this number to increase/decrease the quality. The more you increase, the better the video quality but the the compressed file size will also increase

// compression function, it returns a .mp4 but you can change it to .mov inside the do try block towards the middle. Change assetWriter = try AVAssetWriter ... AVFileType.mp4 to AVFileType.mov
func compressFile(_ urlToCompress: URL, completion:@escaping (URL)->Void) {
    
    var audioFinished = false
    var videoFinished = false
    
    let asset = AVAsset(url: urlToCompress)
    
    //create asset reader
    do {
        assetReader = try AVAssetReader(asset: asset)
    } catch {
        assetReader = nil
    }
    
    guard let reader = assetReader else {
        print("Could not iniitalize asset reader probably failed its try catch")
        // show user error message/alert
        return
    }
    
    guard let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first else { return }
    let videoReaderSettings: [String:Any] = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB]
    
    let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
    
    var assetReaderAudioOutput: AVAssetReaderTrackOutput?
    if let audioTrack = asset.tracks(withMediaType: AVMediaType.audio).first {
        
        let audioReaderSettings: [String : Any] = [
            AVFormatIDKey: kAudioFormatLinearPCM,
            AVSampleRateKey: 44100,
            AVNumberOfChannelsKey: 2
        ]
        
        assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioReaderSettings)
        
        if reader.canAdd(assetReaderAudioOutput!) {
            reader.add(assetReaderAudioOutput!)
        } else {
            print("Couldn't add audio output reader")
            // show user error message/alert
            return
        }
    }
    
    if reader.canAdd(assetReaderVideoOutput) {
        reader.add(assetReaderVideoOutput)
    } else {
        print("Couldn't add video output reader")
        // show user error message/alert
        return
    }
    
    let videoSettings:[String:Any] = [
        AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: self.bitrate],
        AVVideoCodecKey: AVVideoCodecType.h264,
        AVVideoHeightKey: videoTrack.naturalSize.height,
        AVVideoWidthKey: videoTrack.naturalSize.width,
        AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill
    ]
    
    let audioSettings: [String:Any] = [AVFormatIDKey : kAudioFormatMPEG4AAC,
                                       AVNumberOfChannelsKey : 2,
                                       AVSampleRateKey : 44100.0,
                                       AVEncoderBitRateKey: 128000
    ]
    
    let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioSettings)
    let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings)
    videoInput.transform = videoTrack.preferredTransform
    
    let videoInputQueue = DispatchQueue(label: "videoQueue")
    let audioInputQueue = DispatchQueue(label: "audioQueue")
    
    do {
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
        let date = Date()
        let tempDir = NSTemporaryDirectory()
        let outputPath = "\(tempDir)/\(formatter.string(from: date)).mp4"
        let outputURL = URL(fileURLWithPath: outputPath)
        
        assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: AVFileType.mp4)
        
    } catch {
        assetWriter = nil
    }
    guard let writer = assetWriter else {
        print("assetWriter was nil")
        // show user error message/alert
        return
    }
    
    writer.shouldOptimizeForNetworkUse = true
    writer.add(videoInput)
    writer.add(audioInput)
    
    writer.startWriting()
    reader.startReading()
    writer.startSession(atSourceTime: CMTime.zero)
    
    let closeWriter:()->Void = {
        if (audioFinished && videoFinished) {
            self.assetWriter?.finishWriting(completionHandler: { [weak self] in
                
                if let assetWriter = self?.assetWriter {
                    do {
                        let data = try Data(contentsOf: assetWriter.outputURL)
                        print("compressFile -file size after compression: \(Double(data.count / 1048576)) mb")
                    } catch let err as NSError {
                        print("compressFile Error: \(err.localizedDescription)")
                    }
                }
                
                if let safeSelf = self, let assetWriter = safeSelf.assetWriter {
                    completion(assetWriter.outputURL)
                }
            })
            
            self.assetReader?.cancelReading()
        }
    }
    
    audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
        while(audioInput.isReadyForMoreMediaData) {
            if let cmSampleBuffer = assetReaderAudioOutput?.copyNextSampleBuffer() {
                
                audioInput.append(cmSampleBuffer)
                
            } else {
                audioInput.markAsFinished()
                DispatchQueue.main.async {
                    audioFinished = true
                    closeWriter()
                }
                break;
            }
        }
    }
    
    videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
        // request data here
        while(videoInput.isReadyForMoreMediaData) {
            if let cmSampleBuffer = assetReaderVideoOutput.copyNextSampleBuffer() {
                
                videoInput.append(cmSampleBuffer)
                
            } else {
                videoInput.markAsFinished()
                DispatchQueue.main.async {
                    videoFinished = true
                    closeWriter()
                }
                break;
            }
        }
    }
}

Here is how to use it if you're compressing a URL. The compressedURL is returned inside the call back:

@IBAction func buttonTapped(sender: UIButton) {

    // show activity indicator

    let videoURL = URL(string: "...")

    compressFile(videoURL) { (compressedURL) in

       // remove activity indicator
       // do something with the compressedURL such as sending to Firebase or playing it in a player on the *main queue*
    }
}

FYI, I notice the audio slows things up quite a bit, you also try this on a background task to see if it runs any faster. If you added anything like an alert inside the compressFile function itself, you will have to show it on the mainQueue or the app will crash.

DispatchQueue.global(qos: .background).async { [weak self] in

    self?.compressFile(videoURL) { (compressedURL) in

        DispatchQueue.main.async { [weak self] in
            // also remove activity indicator on mainQueue in addition to whatever is inside the function itself that needs to be updated on the mainQueue
        }
    }
}

Here is how to do it if you're compressing a mix composition. You will need to use an AVMutableComposition, an AVAssetExportSession, and the compressFile(:completion:) function above:

@IBAction func buttonTapped(sender: UIButton) {

    // show activity indicator

    let mixComposition = AVMutableComposition()
    // code to create mix ...

    // create a local file
    let tempDir = NSTemporaryDirectory()
    let dirPath = "\(tempDir)/videos_\(UUID().uuidString).mp4"
    let outputFileURL = URL(fileURLWithPath: dirPath)

    removeUrlFromFileManager(outputFileURL) // check to see if the file already exists, if it does remove it, code is at the bottom of the answer

    createAssetExportSession(mixComposition, outputFileURL)
}

// here is the AssetExportSession function with the compressFile(:completion:) inside the callback
func createAssetExportSession(_ mixComposition: AVMutableComposition, _ outputFileURL: URL) {
    
    // *** If your video/url doesn't have sound (not mute but literally no sound, my iPhone's mic was broken when I recorded the video), change this to use AVAssetExportPresetPassthrough instead of HighestQulity. When my video didn't have sound the exporter.status kept returning .failed *** You can check for sound using https://stackoverflow.com/a/64733623/4833705
    guard let exporter = AVAssetExportSession(asset: mixComposition, presetName: AVAssetExportPresetHighestQuality) else {
        // alert user there is a problem
        return
    }
    
    exporter.outputURL = outputFileURL
    exporter.outputFileType = AVFileType.mp4
    exporter.shouldOptimizeForNetworkUse = true
    exporter.exportAsynchronously {
        
        switch exporter.status {
        case .completed:
            print("completed")
            // view the AssetExportSession file size using HighestQuality which will be very high
            do {
                let data = try Data(contentsOf: outputFileURL)
                print("createAssetExportSession -file size: \(Double(data.count / 1048576)) mb")
            } catch let err as NSError {
                print("createAssetExportSession Error: \(err.localizedDescription)")
            }
        case .failed:
            print("failed:", exporter.error as Any)
            DispatchQueue.main.async { [weak self] in
                // remove activity indicator
                // alert user there is a problem
            }
            return
        case .cancelled:
            print("cancelled", exporter.error as Any)
            DispatchQueue.main.async { [weak self] in
                // remove activity indicator
                // alert user there is a problem
            }
            return
        default:
            print("complete")
        }
        
        guard let exporterOutputURL = exporter.outputURL else {
            // alert user there is a problem
            return
        }

        DispatchQueue.main.async { [weak self] in
            
            self?.compressFile(exporterOutputURL) { (compressedURL) in
               // remove activity indicator
               // do something with the compressedURL such as sending to Firebase or playing it in a player on the *main queue*
            }
        }
    }
}

Make sure to remove the compressedURL from file system after you are done with it, eg like before dismissing the vc

func dismissVC() {

    removeUrlFromFileManager(compressedURL)
    // dismiss vc ...
}

removeUrlFromFileManager(_ outputFileURL: URL?) {
    if let outputFileURL = outputFileURL {
        
        let path = outputFileURL.path
        if FileManager.default.fileExists(atPath: path) {
            do {
                try FileManager.default.removeItem(atPath: path)
                print("url SUCCESSFULLY removed: \(outputFileURL)")
            } catch {
                print("Could not remove file at url: \(outputFileURL)")
            }
        }
    }
}

Swift 4:

func convertVideoToLowQuailtyWithInputURL(inputURL: NSURL, outputURL: NSURL, completion: @escaping (Bool) -> Void) {

    let videoAsset = AVURLAsset(url: inputURL as URL, options: nil)
    let videoTrack = videoAsset.tracks(withMediaType: AVMediaType.video)[0]
    let videoSize = videoTrack.naturalSize
    let videoWriterCompressionSettings = [
        AVVideoAverageBitRateKey : Int(125000)
    ]

    let videoWriterSettings:[String : AnyObject] = [
        AVVideoCodecKey : AVVideoCodecH264 as AnyObject,
        AVVideoCompressionPropertiesKey : videoWriterCompressionSettings as AnyObject,
        AVVideoWidthKey : Int(videoSize.width) as AnyObject,
        AVVideoHeightKey : Int(videoSize.height) as AnyObject
    ]

    let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoWriterSettings)
    videoWriterInput.expectsMediaDataInRealTime = true
    videoWriterInput.transform = videoTrack.preferredTransform
    let videoWriter = try! AVAssetWriter(outputURL: outputURL as URL, fileType: AVFileType.mov)
    videoWriter.add(videoWriterInput)
    //setup video reader
    let videoReaderSettings:[String : AnyObject] = [
        kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) as AnyObject
    ]

    let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings)
    var videoReader: AVAssetReader!

    do{

        videoReader = try AVAssetReader(asset: videoAsset)
    }
    catch {

        print("video reader error: \(error)")
        completion(false)
    }
    videoReader.add(videoReaderOutput)
    //setup audio writer
    let audioWriterInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: nil)
    audioWriterInput.expectsMediaDataInRealTime = false
    videoWriter.add(audioWriterInput)
    //setup audio reader
    let audioTrack = videoAsset.tracks(withMediaType: AVMediaType.audio)[0]
    let audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
    let audioReader = try! AVAssetReader(asset: videoAsset)
    audioReader.add(audioReaderOutput)
    videoWriter.startWriting()

    //start writing from video reader
    videoReader.startReading()
    videoWriter.startSession(atSourceTime: kCMTimeZero)
    let processingQueue = DispatchQueue(label: "processingQueue1")
    videoWriterInput.requestMediaDataWhenReady(on: processingQueue, using: {() -> Void in
        while videoWriterInput.isReadyForMoreMediaData {
            let sampleBuffer:CMSampleBuffer? = videoReaderOutput.copyNextSampleBuffer();
            if videoReader.status == .reading && sampleBuffer != nil {
                videoWriterInput.append(sampleBuffer!)
            }
            else {
                videoWriterInput.markAsFinished()
                if videoReader.status == .completed {
                    //start writing from audio reader
                    audioReader.startReading()
                    videoWriter.startSession(atSourceTime: kCMTimeZero)
                    let processingQueue = DispatchQueue(label: "processingQueue2")
                    audioWriterInput.requestMediaDataWhenReady(on: processingQueue, using: {() -> Void in
                        while audioWriterInput.isReadyForMoreMediaData {
                            let sampleBuffer:CMSampleBuffer? = audioReaderOutput.copyNextSampleBuffer()
                            if audioReader.status == .reading && sampleBuffer != nil {
                                audioWriterInput.append(sampleBuffer!)
                            }
                            else {
                                audioWriterInput.markAsFinished()
                                if audioReader.status == .completed {
                                    videoWriter.finishWriting(completionHandler: {() -> Void in
                                        completion(true)
                                    })
                                }
                            }
                        }
                    })
                }
            }
        }
    })
}
Use exportSession.fileLengthLimit = 1024 * 1024 * 10 //10 MB

10MB is hard coded number. Use according to your required bitrate.

fileLengthLimit

session should not exceed. Depending on the content of the source asset, it is possible for the output to slightly exceed the file length limit. The length of the output file should be tested if you require that a strict limit be observed before making use of the output. See also maxDuration and timeRange.Indicates the file length that the output of the

developer.apple.com/documentation/avfoundation/avassetexportsession/1622333-filelengthlimit

Swift 5

More bitrate equals more quality and more space.

Improved version of https://stackoverflow.com/a/62862102/7668778

Works with videos from iCloud and also works with multiple videos at the same time. It's also possible to change the progress to a block parameter, instead of a delegate.

import Foundation
import AVKit

protocol VideoEditorDelegate: AnyObject {
    
    func propagate(event: VideoEditor.PropagateEvent)
    
}

final class VideoEditor {
    
    var assetReaders: [AVAssetReader] = []
    var assetWriters: [AVAssetWriter] = []
    
    /// Compress a video URL to H264 in mp4 format
    /// - Parameters:
    ///   - asset: video asset to compress
    ///   - uuID: unique id to identify progress of each video
    ///   - delegate: to handle progress
    ///   - completion: new compressed video url
    func compressVideo(_ asset: AVAsset,
                       uuID: String?,
                       delegate: VideoEditorDelegate?,
                       bitrate: NSNumber = NSNumber(value: 6000000),
                       completion: @escaping (Result<URL, Error>) -> Void) {
        DispatchQueue.global().async {
            //Patch for iCloud videos
            guard let videoData = asset.dataToUpload,
                  let asset = self.avAssetFrom(data: videoData) else {
                completion(.failure(Errors.nilAVAssetData))
                return
            }
            
            guard let reader = try? AVAssetReader(asset: asset) else {
                completion(.failure(Errors.nilAssetReader))
                return
            }
            self.assetReaders.append(reader) //To prevent reader being release while reading
            guard let videoTrack = asset.tracks(withMediaType: .video).first else {
                completion(.failure(Errors.nilVideoTrack))
                return
            }
            let videoReaderSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB]
            let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack,
                                                                  outputSettings: videoReaderSettings)
            
            guard let audioTrack = asset.tracks(withMediaType: .audio).first else {
                completion(.failure(Errors.failToAddAudio))
                return
            }
            
            let audioReaderSettings: [String : Any] = [
                AVFormatIDKey: kAudioFormatLinearPCM,
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2
            ]
            let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack,
                                                                  outputSettings: audioReaderSettings)
            guard reader.canAdd(assetReaderAudioOutput) else {
                completion(.failure(Errors.failToAddAudio))
                return
            }
            reader.add(assetReaderAudioOutput)
            
            guard reader.canAdd(assetReaderVideoOutput) else {
                completion(.failure(Errors.failToAddVideo))
                return
            }
            reader.add(assetReaderVideoOutput)
            
            let videoSettings: [String : Any] = [
                AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey: bitrate],
                AVVideoCodecKey: AVVideoCodecType.h264,
                AVVideoHeightKey: videoTrack.naturalSize.height,
                AVVideoWidthKey: videoTrack.naturalSize.width,
                AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill
            ]
            
            let audioSettings: [String:Any] = [AVFormatIDKey : kAudioFormatMPEG4AAC,
                                       AVNumberOfChannelsKey : 2,
                                             AVSampleRateKey : 44100.0,
                                          AVEncoderBitRateKey: 128000
            ]
            
            let audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio,
                                                outputSettings: audioSettings)
            let videoInput = AVAssetWriterInput(mediaType: AVMediaType.video,
                                                outputSettings: videoSettings)
            videoInput.transform = videoTrack.preferredTransform
            
            let videoInputQueue = DispatchQueue(label: "videoQueue")
            let audioInputQueue = DispatchQueue(label: "audioQueue")
            
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
            let date = Date()
            let tempDir = NSTemporaryDirectory()
            let outputPath = "\(tempDir)/\(formatter.string(from: date)).mp4"
            let outputURL = URL(fileURLWithPath: outputPath)
            
            guard let writer = try? AVAssetWriter(outputURL: outputURL,
                                                  fileType: AVFileType.mp4) else {
                completion(.failure(Errors.nilAssetWriter))
                return
            }
            self.assetWriters.append(writer) //To prevent writer being release while writing
            writer.shouldOptimizeForNetworkUse = true
            writer.add(videoInput)
            writer.add(audioInput)
            
            writer.startWriting()
            reader.startReading()
            writer.startSession(atSourceTime: CMTime.zero)
            
            let group = DispatchGroup()

            group.enter()
            audioInput.requestMediaDataWhenReady(on: audioInputQueue) {
                while(audioInput.isReadyForMoreMediaData) {
                    if let cmSampleBuffer = assetReaderAudioOutput.copyNextSampleBuffer() {
                        audioInput.append(cmSampleBuffer)
                    } else {
                        audioInput.markAsFinished()
                        group.leave()
                    }
                }
            }

            group.enter()
            let videoLenth = CMTimeGetSeconds(asset.duration)
            videoInput.requestMediaDataWhenReady(on: videoInputQueue) {
                while(videoInput.isReadyForMoreMediaData) {
                    if let cmSampleBuffer = assetReaderVideoOutput.copyNextSampleBuffer() {
                        videoInput.append(cmSampleBuffer)
                        //Show progress
                        if let uuID = uuID, let delegate = delegate {
                            let timeStamp = CMSampleBufferGetPresentationTimeStamp(cmSampleBuffer)
                            let timeInSecond = CMTimeGetSeconds(timeStamp)
                            let progress = Float(timeInSecond / videoLenth)
                            DispatchQueue.main.async {
                                delegate.propagate(event: .progress(progress,
                                                                     uuID: uuID))
                            }
                        }
                    } else {
                        videoInput.markAsFinished()
                        group.leave()
                    }
                }
            }

            let closeWriter: () -> Void = {
                Task {
                    await writer.finishWriting()
                    do {
                        let data = try Data(contentsOf: writer.outputURL)
                        //TODO ale: track file size
                        print("compressFile -file size after compression: \(Double(data.count / 1048576)) mb")
                    } catch {
                        completion(.failure(error))
                        return
                    }
                    completion(.success(writer.outputURL))
                    writer.cancelWriting()
                    self.assetWriters = self.assetWriters.filter { $0.outputURL != outputURL }
                    self.assetReaders = self.assetReaders.filter { $0.asset != asset }
                }
            }

            group.notify(queue: .global()) {
                closeWriter()
            }
        }
    }
    
    func avAssetFrom(data: Data) -> AVAsset? {
            let directory = NSTemporaryDirectory()
            let fileName = "\(NSUUID().uuidString).mov"
        guard let fullURL = NSURL.fileURL(withPathComponents: [directory, fileName]) else {
            return nil
        }
        do {
            try data.write(to: fullURL)
            let asset = AVAsset(url: fullURL)
            return asset
        } catch {
            return nil
        }
    }
    
}

// MARK: = Helpong Structures

extension VideoEditor {
    
    enum PropagateEvent {
        case progress(Float, uuID: String)
    }
    
    enum Errors: Error {
        case nilAssetReader
        case nilAssetWriter
        case failToAddAudio
        case failToAddVideo
        case nilVideoTrack
        case nilAVAssetData
    }
    
}
Related