-
-
Save rcholic/fa29606a28f4acada869a18207fbd6d6 to your computer and use it in GitHub Desktop.
Build a movie from jpeg images in Swift using AVFoundation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BuildTimelapseViewController.swift | |
// | |
// Created by Adam Jensen on 5/9/15. | |
// | |
import JGProgressHUD | |
import JoePro | |
import UIKit | |
class BuildTimelapseViewController: UIViewController { | |
@IBOutlet weak var resolutionSegmentedControl: UISegmentedControl! | |
@IBOutlet weak var speedSlider: UISlider! | |
@IBOutlet weak var removeFisheyeSlider: UISwitch! | |
var album: String? | |
var camera: JoeProCamera? | |
var timeLapseBuilder: TimeLapseBuilder? | |
init(camera: JoeProCamera, album: String) { | |
self.camera = camera | |
self.album = album | |
super.init(nibName: "BuildTimelapseViewController", bundle: nil) | |
} | |
required init(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
} | |
@IBAction func buildButtonTapped(sender: AnyObject) { | |
if let camera = camera, | |
let album = album { | |
let progressHUD = JGProgressHUD(style: .Light) | |
progressHUD.textLabel.text = "Building your timelapse..." | |
progressHUD.indicatorView = JGProgressHUDRingIndicatorView(HUDStyle: .Light) | |
progressHUD.setProgress(0, animated: true) | |
progressHUD.showInView(view) | |
camera.listOfVideos(album) { (videos) -> Void in | |
self.timeLapseBuilder = TimeLapseBuilder(photoURLs: videos) | |
self.timeLapseBuilder!.build( | |
{ (progress: NSProgress) in | |
NSLog("Progress: \(progress.completedUnitCount) / \(progress.totalUnitCount)") | |
dispatch_async(dispatch_get_main_queue(), { | |
let progressPercentage = Float(progress.completedUnitCount) / Float(progress.totalUnitCount) | |
progressHUD.setProgress(progressPercentage, animated: true) | |
}) | |
}, | |
success: { url in | |
NSLog("Output written to \(url)") | |
dispatch_async(dispatch_get_main_queue(), { | |
progressHUD.dismiss() | |
}) | |
}, | |
failure: { error in | |
NSLog("failure: \(error)") | |
dispatch_async(dispatch_get_main_queue(), { | |
progressHUD.dismiss() | |
}) | |
} | |
) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// TimeLapseBuilder.swift | |
// | |
// Created by Adam Jensen on 5/10/15. | |
// Copyright (c) 2015 Adam Jensen. All rights reserved. | |
// | |
// NOTE: This is the original Swift 1.2 implementation. For an updated version | |
// written in Swift 2.0, see https://gist.github.com/acj/6ae90aa1ebb8cad6b47b | |
import AVFoundation | |
import UIKit | |
let kErrorDomain = "TimeLapseBuilder" | |
let kFailedToStartAssetWriterError = 0 | |
let kFailedToAppendPixelBufferError = 1 | |
class TimeLapseBuilder: NSObject { | |
let photoURLs: [String] | |
var videoWriter: AVAssetWriter? | |
init(photoURLs: [String]) { | |
self.photoURLs = photoURLs | |
} | |
func build(progress: (NSProgress -> Void), success: (NSURL -> Void), failure: (NSError -> Void)) { | |
let inputSize = CGSize(width: 4000, height: 3000) | |
let outputSize = CGSize(width: 1280, height: 720) | |
var error: NSError? | |
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! NSString | |
let videoOutputURL = NSURL(fileURLWithPath: documentsPath.stringByAppendingPathComponent("AssembledVideo.mov"))! | |
NSFileManager.defaultManager().removeItemAtURL(videoOutputURL, error: nil) | |
videoWriter = AVAssetWriter(URL: videoOutputURL, fileType: AVFileTypeQuickTimeMovie, error: &error) | |
if let videoWriter = videoWriter { | |
let videoSettings: [NSObject : AnyObject] = [ | |
AVVideoCodecKey : AVVideoCodecH264, | |
AVVideoWidthKey : outputSize.width, | |
AVVideoHeightKey : outputSize.height, | |
// AVVideoCompressionPropertiesKey : [ | |
// AVVideoAverageBitRateKey : NSInteger(1000000), | |
// AVVideoMaxKeyFrameIntervalKey : NSInteger(16), | |
// AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel | |
// ] | |
] | |
let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings) | |
let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor( | |
assetWriterInput: videoWriterInput, | |
sourcePixelBufferAttributes: [ | |
kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_32ARGB, | |
kCVPixelBufferWidthKey : inputSize.width, | |
kCVPixelBufferHeightKey : inputSize.height, | |
] | |
) | |
assert(videoWriter.canAddInput(videoWriterInput)) | |
videoWriter.addInput(videoWriterInput) | |
if videoWriter.startWriting() { | |
videoWriter.startSessionAtSourceTime(kCMTimeZero) | |
assert(pixelBufferAdaptor.pixelBufferPool != nil) | |
let media_queue = dispatch_queue_create("mediaInputQueue", nil) | |
videoWriterInput.requestMediaDataWhenReadyOnQueue(media_queue, usingBlock: { () -> Void in | |
let fps: Int32 = 30 | |
let frameDuration = CMTimeMake(1, fps) | |
let currentProgress = NSProgress(totalUnitCount: Int64(self.photoURLs.count)) | |
var frameCount: Int64 = 0 | |
var remainingPhotoURLs = [String](self.photoURLs) | |
while (videoWriterInput.readyForMoreMediaData && !remainingPhotoURLs.isEmpty) { | |
let nextPhotoURL = remainingPhotoURLs.removeAtIndex(0) | |
let lastFrameTime = CMTimeMake(frameCount, fps) | |
let presentationTime = frameCount == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration) | |
if !self.appendPixelBufferForImageAtURL(nextPhotoURL, pixelBufferAdaptor: pixelBufferAdaptor, presentationTime: presentationTime) { | |
error = NSError( | |
domain: kErrorDomain, | |
code: kFailedToAppendPixelBufferError, | |
userInfo: [ | |
"description": "AVAssetWriterInputPixelBufferAdapter failed to append pixel buffer", | |
"rawError": videoWriter.error ?? "(none)" | |
] | |
) | |
break | |
} | |
frameCount++ | |
currentProgress.completedUnitCount = frameCount | |
progress(currentProgress) | |
} | |
videoWriterInput.markAsFinished() | |
videoWriter.finishWritingWithCompletionHandler { () -> Void in | |
if error == nil { | |
success(videoOutputURL) | |
} | |
} | |
}) | |
} else { | |
error = NSError( | |
domain: kErrorDomain, | |
code: kFailedToStartAssetWriterError, | |
userInfo: ["description": "AVAssetWriter failed to start writing"] | |
) | |
} | |
} | |
if let error = error { | |
failure(error) | |
} | |
} | |
func appendPixelBufferForImageAtURL(url: String, pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool { | |
var appendSucceeded = true | |
autoreleasepool { | |
if let url = NSURL(string: url), | |
let imageData = NSData(contentsOfURL: url), | |
let image = UIImage(data: imageData) { | |
var pixelBuffer: Unmanaged<CVPixelBuffer>? | |
let status: CVReturn = CVPixelBufferPoolCreatePixelBuffer( | |
kCFAllocatorDefault, | |
pixelBufferAdaptor.pixelBufferPool, | |
&pixelBuffer | |
) | |
if let pixelBuffer = pixelBuffer where status == 0 { | |
let managedPixelBuffer = pixelBuffer.takeRetainedValue() | |
fillPixelBufferFromImage(image, pixelBuffer: managedPixelBuffer) | |
appendSucceeded = pixelBufferAdaptor.appendPixelBuffer( | |
managedPixelBuffer, | |
withPresentationTime: presentationTime | |
) | |
} else { | |
NSLog("error: Failed to allocate pixel buffer from pool") | |
} | |
} | |
} | |
return appendSucceeded | |
} | |
func fillPixelBufferFromImage(image: UIImage, pixelBuffer: CVPixelBufferRef) { | |
let imageData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage)) | |
let lockStatus = CVPixelBufferLockBaseAddress(pixelBuffer, 0) | |
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) | |
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedFirst.rawValue) | |
let rgbColorSpace = CGColorSpaceCreateDeviceRGB() | |
let context = CGBitmapContextCreate( | |
pixelData, | |
Int(image.size.width), | |
Int(image.size.height), | |
8, | |
Int(4 * image.size.width), | |
rgbColorSpace, | |
bitmapInfo | |
) | |
CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage) | |
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// TimeLapseBuilder.swift | |
// Vapor | |
// | |
// Created by Adam Jensen on 5/10/15. | |
// Copyright (c) 2015 Adam Jensen. All rights reserved. | |
// | |
// NOTE: This implementation is written in Swift 2.0. | |
import AVFoundation | |
import UIKit | |
let kErrorDomain = "TimeLapseBuilder" | |
let kFailedToStartAssetWriterError = 0 | |
let kFailedToAppendPixelBufferError = 1 | |
class TimeLapseBuilder: NSObject { | |
let photoURLs: [String] | |
var videoWriter: AVAssetWriter? | |
init(photoURLs: [String]) { | |
self.photoURLs = photoURLs | |
} | |
func build(progress: (NSProgress -> Void), success: (NSURL -> Void), failure: (NSError -> Void)) { | |
let inputSize = CGSize(width: 4000, height: 3000) | |
let outputSize = CGSize(width: 1280, height: 720) | |
var error: NSError? | |
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as NSString | |
let videoOutputURL = NSURL(fileURLWithPath: documentsPath.stringByAppendingPathComponent("AssembledVideo.mov")) | |
do { | |
try NSFileManager.defaultManager().removeItemAtURL(videoOutputURL) | |
} catch {} | |
do { | |
try videoWriter = AVAssetWriter(URL: videoOutputURL, fileType: AVFileTypeQuickTimeMovie) | |
} catch let writerError as NSError { | |
error = writerError | |
videoWriter = nil | |
} | |
if let videoWriter = videoWriter { | |
let videoSettings: [String : AnyObject] = [ | |
AVVideoCodecKey : AVVideoCodecH264, | |
AVVideoWidthKey : outputSize.width, | |
AVVideoHeightKey : outputSize.height, | |
// AVVideoCompressionPropertiesKey : [ | |
// AVVideoAverageBitRateKey : NSInteger(1000000), | |
// AVVideoMaxKeyFrameIntervalKey : NSInteger(16), | |
// AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel | |
// ] | |
] | |
let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings) | |
let sourceBufferAttributes = [String : AnyObject](dictionaryLiteral: | |
(kCVPixelBufferPixelFormatTypeKey as String, Int(kCVPixelFormatType_32ARGB)), | |
(kCVPixelBufferWidthKey as String, Float(inputSize.width)), | |
(kCVPixelBufferHeightKey as String, Float(inputSize.height)) | |
) | |
let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor( | |
assetWriterInput: videoWriterInput, | |
sourcePixelBufferAttributes: sourceBufferAttributes | |
) | |
assert(videoWriter.canAddInput(videoWriterInput)) | |
videoWriter.addInput(videoWriterInput) | |
if videoWriter.startWriting() { | |
videoWriter.startSessionAtSourceTime(kCMTimeZero) | |
assert(pixelBufferAdaptor.pixelBufferPool != nil) | |
let media_queue = dispatch_queue_create("mediaInputQueue", nil) | |
videoWriterInput.requestMediaDataWhenReadyOnQueue(media_queue, usingBlock: { () -> Void in | |
let fps: Int32 = 30 | |
let frameDuration = CMTimeMake(1, fps) | |
let currentProgress = NSProgress(totalUnitCount: Int64(self.photoURLs.count)) | |
var frameCount: Int64 = 0 | |
var remainingPhotoURLs = [String](self.photoURLs) | |
while (videoWriterInput.readyForMoreMediaData && !remainingPhotoURLs.isEmpty) { | |
let nextPhotoURL = remainingPhotoURLs.removeAtIndex(0) | |
let lastFrameTime = CMTimeMake(frameCount, fps) | |
let presentationTime = frameCount == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration) | |
if !self.appendPixelBufferForImageAtURL(nextPhotoURL, pixelBufferAdaptor: pixelBufferAdaptor, presentationTime: presentationTime) { | |
error = NSError( | |
domain: kErrorDomain, | |
code: kFailedToAppendPixelBufferError, | |
userInfo: [ | |
"description": "AVAssetWriterInputPixelBufferAdapter failed to append pixel buffer", | |
"rawError": videoWriter.error ?? "(none)" | |
] | |
) | |
break | |
} | |
frameCount++ | |
currentProgress.completedUnitCount = frameCount | |
progress(currentProgress) | |
} | |
videoWriterInput.markAsFinished() | |
videoWriter.finishWritingWithCompletionHandler { () -> Void in | |
if error == nil { | |
success(videoOutputURL) | |
} | |
self.videoWriter = nil | |
} | |
}) | |
} else { | |
error = NSError( | |
domain: kErrorDomain, | |
code: kFailedToStartAssetWriterError, | |
userInfo: ["description": "AVAssetWriter failed to start writing"] | |
) | |
} | |
} | |
if let error = error { | |
failure(error) | |
} | |
} | |
func appendPixelBufferForImageAtURL(url: String, pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool { | |
var appendSucceeded = false | |
autoreleasepool { | |
if let url = NSURL(string: url), | |
let imageData = NSData(contentsOfURL: url), | |
let image = UIImage(data: imageData), | |
let pixelBufferPool = pixelBufferAdaptor.pixelBufferPool { | |
let pixelBufferPointer = UnsafeMutablePointer<CVPixelBuffer?>.alloc(1) | |
let status: CVReturn = CVPixelBufferPoolCreatePixelBuffer( | |
kCFAllocatorDefault, | |
pixelBufferPool, | |
pixelBufferPointer | |
) | |
if let pixelBuffer = pixelBufferPointer.memory where status == 0 { | |
fillPixelBufferFromImage(image, pixelBuffer: pixelBuffer) | |
appendSucceeded = pixelBufferAdaptor.appendPixelBuffer( | |
pixelBuffer, | |
withPresentationTime: presentationTime | |
) | |
pixelBufferPointer.destroy() | |
} else { | |
NSLog("error: Failed to allocate pixel buffer from pool") | |
} | |
pixelBufferPointer.dealloc(1) | |
} | |
} | |
return appendSucceeded | |
} | |
func fillPixelBufferFromImage(image: UIImage, pixelBuffer: CVPixelBufferRef) { | |
CVPixelBufferLockBaseAddress(pixelBuffer, 0) | |
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) | |
let rgbColorSpace = CGColorSpaceCreateDeviceRGB() | |
let context = CGBitmapContextCreate( | |
pixelData, | |
Int(image.size.width), | |
Int(image.size.height), | |
8, | |
CVPixelBufferGetBytesPerRow(pixelBuffer), | |
rgbColorSpace, | |
CGImageAlphaInfo.PremultipliedFirst.rawValue | |
) | |
CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage) | |
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment