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)")
}
}
}
}