June 8, 2017

To quote someone from stack overflow, you pretty much need a PHD to use AVAssetWriter. This is unfortunate because you can do some pretty useful things with it like compressing your videos to be super quick for network transfer. In this post, I will walk you through a compression function and try to create a picture about how everything works together. It’s not that scary if you take it a piece at a time.

We are going to create a function called func compressVideo(videoURL:URL, outputUrl:URL callback) which will simply compress a video file already located at a file system URL. This video file should be recorded with AVFoundation but for this post I will assume you have already done that.

To compress this file, we are going to use a class called AVAssetWriter which basically creates an audio or video file at a specified destination on the devices filesystem while giving us complete control over the asset’s settings such as size, file type, color grading, frame rate, compression, and more.

Before we get into it, let me give you an outline of the classes we will be using in this post:

AVAssetWriter - Writes an audio or video file at a specified destination on the devices filesystem while giving us complete control over the asset’s settings such as size, file type, color grading, frame rate, compression, and more.

AVAssetWriterInput - A writer class takes multiple inputs that can be of different AVMediaTypes . In this post, we will have one for the video and one for the audio.

AVAssetReader - This is how we get the video data from the actual file and feed it to the AVAssetReader.

AVAssetReaderTrackOutput - We create asset reader outputs for both audio and video so the asset reader knows what to read from the files.

AVAsset - This is how AV Foundation handles files. We will read the file from the url into an AVAsset so AV Foundation knows how to use it.

AVAssetTrack - An asset is broken down into tracks. We will only have two for our purposes: one for video and one for audio.

Let’s get started. First, we will create the function and some class properties outside the function to hold our AVAssetReader and AVAssetWriter . We will also create a property to hold the bitrate of the video which will determine the amount of compression the video has.

var assetWriter:AVAssetWriter? var assetReader:AVAssetReader? let bitrate:NSNumber = NSNumber(value:250000) func compressFile(urlToCompress: URL, outputURL: URL, completion:@escaping (URL)->Void){ //... code .... }

This function takes the url of the file to compress, a link to an empty URL to write the newly compressed video to, and a completion block that returns nothing but contains the finished video URL.

//...code... 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{ fatalError("Could not initalize asset reader probably failed its try catch") } //...code...

Here we are creating two bool variables to tell us if the audio and video processing is finished so we know when to finish writing to the file.

On the next line we use the url to our video file to create an AVAsset. This is just putting the video in a format that AVFoundation can work with.

After that, we are creating our AVAssetReader with some error checking.

//...code... let videoTrack = asset.tracks(withMediaType: AVMediaTypeVideo).first! let audioTrack = asset.tracks(withMediaType: AVMediaTypeAudio).first! //...code...

Now we need to access the tracks from the AVAsset we have just created.Since this is just simple video recording, we can just assume there is only one audio and video track.

//...code... let videoReaderSettings: [String:Any] = [kCVPixelBufferPixelFormatTypeKey as String!:kCVPixelFormatType_32ARGB ] // ADJUST BIT RATE OF VIDEO HERE let videoSettings:[String:Any] = [ AVVideoCompressionPropertiesKey: [AVVideoAverageBitRateKey:self.bitrate], AVVideoCodecKey: AVVideoCodecH264, AVVideoHeightKey: videoTrack.naturalSize.height, AVVideoWidthKey: videoTrack.naturalSize.width ] //...code...

This part is very important. Here is where we tell the AVAssetWriter and AVAssetReader what settings we want for our output video. The compression of the video is determined by the bitrate so we will use the class property we created before to add the amount of bit rate we want on the video. You should change this property for yourself based on how much compression vs quality you want. Here is the link to the settings you can apply to your asset writer: Video Settings.

//...code... let assetReaderVideoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoReaderSettings) let assetReaderAudioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil) if reader.canAdd(assetReaderVideoOutput){ reader.add(assetReaderVideoOutput) }else{ fatalError("Couldn't add video output reader") } if reader.canAdd(assetReaderAudioOutput){ reader.add(assetReaderAudioOutput) }else{ fatalError("Couldn't add audio output reader") } //...code...

Now we will create our AVAssetReaderTrackOutput for both audio and video. We give them each their respective tracks and apply the video settings we set up here.

Then with some simple error handling we make sure we are able to add the tracks.

//...code... let audioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: nil) let videoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings) videoInput.transform = videoTrack.preferredTransform //...code...

Here we are creating the AVAssetWriterInput classes that will be passed the buffer information from the asset reader (this is like the data from the video and audio) and setting the media types and video settings.

The last line is important here. We need to set the transform of the video input as the same orientation as the video track otherwise it will default to being horizontal and we want the footage to be upright like it was recorded on the device so we will give it the same information that the track has.

//...code... let videoInputQueue = DispatchQueue(label: "videoQueue") let audioInputQueue = DispatchQueue(label: "audioQueue") do{ assetWriter = try AVAssetWriter(outputURL: outputURL, fileType: AVFileTypeQuickTimeMovie) }catch{ assetWriter = nil } guard let writer = assetWriter else{ fatalError("assetWriter was nil") } //...code...

We need to do two writes of information; one for the audio and one for the video. These need to happen at the same time so we need to create two dispatch queues for each event to happen on. Next we are creating the asset writer.

Almost there…

//...code... writer.shouldOptimizeForNetworkUse = true writer.add(videoInput) writer.add(audioInput) writer.startWriting() reader.startReading() writer.startSession(atSourceTime: kCMTimeZero) //...code...

We are now ready to start writing. First we add the video inputs to the writer and set shouldOptimizeForNetworkUse to true. (Pretty self explanatory). Next we start writing on both the reader and writer then set the time that the writer should start at. This will just be the beginning so we use the built in constant kCMTimeZero .

//...code... let closeWriter:()->Void = { if (audioFinished && videoFinished){ self.assetWriter?.finishWriting(completionHandler: { completion((self.assetWriter?.outputURL)!) }) self.assetReader?.cancelReading() } } //...code...

This is a little out of order now but we need to create a closure which will finish off the function and call the completion block and stop writing to the file and save everything. This will be called later after all the audio and video writing has been completed.

Very helpful website for swift closures

//...code... audioInput.requestMediaDataWhenReady(on: audioInputQueue) { while(audioInput.isReadyForMoreMediaData){ let sample = assetReaderAudioOutput.copyNextSampleBuffer() if (sample != nil){ audioInput.append(sample!) }else{ audioInput.markAsFinished() DispatchQueue.main.async { audioFinished = true closeWriter() } break; } } } videoInput.requestMediaDataWhenReady(on: videoInputQueue) { //request data here while(videoInput.isReadyForMoreMediaData){ let sample = assetReaderVideoOutput.copyNextSampleBuffer() if (sample != nil){ videoInput.append(sample!) }else{ videoInput.markAsFinished() DispatchQueue.main.async { videoFinished = true closeWriter() } break; } } } //...just add one more curly brace to finish off the function...

We need to pass buffers to the videoInput from the videoReader. This method will be called over and over when the videoInput is ready for another buffer (segment of video), We call markAsFinished at when there are no buffers left and call our closeWriter() from the previous step.

Basically to compress these files we are adjusting the bitrate and we created a helpful property at the top with the bitrate of the video. Just make it lower for more compression and higher for less compression but better quality.

Here is the link to the full code. Please feel free to tweet or email me of you have any questions. Enjoy!

EDIT: A user found that they needed to create the output url like this:

I haven’t tested this myself by I will include it in case this solves anyone else’s issue

let formatter = DateFormatter() formatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'" let date = Date() let documentsPath = NSTemporaryDirectory() let outputPath = "\(documentsPath)/\(formatter.string(from: date)).mov" let newOutputUrl = URL(fileURLWithPath: outputPath)

121 Kudos