|
// |
|
// TimeLapseBuilder.swift |
|
// Vapor |
|
// |
|
// Created by Adam Jensen on 5/10/15. |
|
// |
|
// 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) |
|
} |
|
} |
Adam,
Your code seems to be about the only swift example of how to create movies from images...I was able to modify it to simply create a movie from a series of jpegs...the codes runs swimmingly, and even writes out a movie file with no errors...However, the file is empty...
I was wondering if you could be so kind as to suggest where I might be going wrong or what I might do to troubleshoot? I've ensured my images are getting loaded, have run both on simulator and on iPad, I've changed the path where the movie gets written to, all to no avail. Any help would be appreciated.
Thanks in advance.
I load the images like so...
and I modified the "buildButtonTapped" IBAction to strip out references to a camera device like so...
The other change I've made is I have added a bit of code that copies the movie file to the SavedPhotosAlbum, but since the file has no data, nothing gets copied...