如何将UIImage数组导出为电影?
我有一个严重的问题:我有一个NSArray
与几个UIImage
对象。 我现在想要做的是从这些UIImages
创build电影。 但我不知道如何去做。
我希望有人可以帮助我或给我一个代码片段,做我喜欢的事情。
编辑:为了将来的参考 – 应用解决scheme后,如果video看起来失真,请确保您捕捉的图像/区域的宽度是16的倍数。经过几个小时的斗争在这里发现:
为什么我的UIImage电影会变形?
这里是完整的解决scheme(只要确保宽度是16的倍数)
http://codethink.no-ip.org/wordpress/archives/673
看看AVAssetWriter和AVFoundation框架的其余部分。 作者有一个AVAssetWriterInputtypes的input,它依次具有一个名为appendSampleBuffer的方法:它允许您将单个帧添加到videostream。 基本上你必须:
1)连线作者:
NSError *error = nil; AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL: [NSURL fileURLWithPath:somePath] fileType:AVFileTypeQuickTimeMovie error:&error]; NSParameterAssert(videoWriter); NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys: AVVideoCodecH264, AVVideoCodecKey, [NSNumber numberWithInt:640], AVVideoWidthKey, [NSNumber numberWithInt:480], AVVideoHeightKey, nil]; AVAssetWriterInput* writerInput = [[AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings] retain]; //retain should be removed if ARC NSParameterAssert(writerInput); NSParameterAssert([videoWriter canAddInput:writerInput]); [videoWriter addInput:writerInput];
2)开始会话:
[videoWriter startWriting]; [videoWriter startSessionAtSourceTime:…] //use kCMTimeZero if unsure
3)写一些样品:
// Or you can use AVAssetWriterInputPixelBufferAdaptor. // That lets you feed the writer input data from a CVPixelBuffer // that's quite easy to create from a CGImage. [writerInput appendSampleBuffer:sampleBuffer];
4)完成会话:
[writerInput markAsFinished]; [videoWriter endSessionAtSourceTime:…]; //optional can call finishWriting without specifiying endTime [videoWriter finishWriting]; //deprecated in ios6 /* [videoWriter finishWritingWithCompletionHandler:...]; //ios 6.0+ */
你仍然需要填补很多空白,但是我认为唯一真正很难的部分是从CGImage
获取一个像素缓冲区:
- (CVPixelBufferRef) newPixelBufferFromCGImage: (CGImageRef) image { NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey, [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil]; CVPixelBufferRef pxbuffer = NULL; CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameSize.width, frameSize.height, kCVPixelFormatType_32ARGB, (CFDictionaryRef) options, &pxbuffer); NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL); CVPixelBufferLockBaseAddress(pxbuffer, 0); void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer); NSParameterAssert(pxdata != NULL); CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(pxdata, frameSize.width, frameSize.height, 8, 4*frameSize.width, rgbColorSpace, kCGImageAlphaNoneSkipFirst); NSParameterAssert(context); CGContextConcatCTM(context, frameTransform); CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image); CGColorSpaceRelease(rgbColorSpace); CGContextRelease(context); CVPixelBufferUnlockBaseAddress(pxbuffer, 0); return pxbuffer; }
frameSize
是一个描述你的目标帧大小的frameTransform
, frameTransform
是一个CGAffineTransform
,当你把它们绘制成帧时,可以让你转换图像。
这是Objective-C中iOS8的最新工作代码。
我们必须对@ Zoul的答案做出各种调整,才能使它适用于最新版本的Xcode和iOS8。 这里是我们完整的工作代码,需要一个UIImages数组,将它们变成一个.mov文件,将其保存到临时目录,然后将其移动到相机胶卷。 我们汇集了来自多个不同职位的代码来实现这个工作。 我们已经强调了我们必须解决的陷阱,以使代码在我们的评论中发挥作用。
(1)创build一个UIImages的集合
[self saveMovieToLibrary] - (IBAction)saveMovieToLibrary { // You just need the height and width of the video here // For us, our input and output video was 640 height x 480 width // which is what we get from the iOS front camera ATHSingleton *singleton = [ATHSingleton singletons]; int height = singleton.screenHeight; int width = singleton.screenWidth; // You can save a .mov or a .mp4 file //NSString *fileNameOut = @"temp.mp4"; NSString *fileNameOut = @"temp.mov"; // We chose to save in the tmp/ directory on the device initially NSString *directoryOut = @"tmp/"; NSString *outFile = [NSString stringWithFormat:@"%@%@",directoryOut,fileNameOut]; NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:outFile]]; NSURL *videoTempURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), fileNameOut]]; // WARNING: AVAssetWriter does not overwrite files for us, so remove the destination file if it already exists NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager removeItemAtPath:[videoTempURL path] error:NULL]; // Create your own array of UIImages NSMutableArray *images = [NSMutableArray array]; for (int i=0; i<singleton.numberOfScreenshots; i++) { // This was our routine that returned a UIImage. Just use your own. UIImage *image =[self uiimageFromCopyOfPixelBuffersUsingIndex:i]; // We used a routine to write text onto every image // so we could validate the images were actually being written when testing. This was it below. image = [self writeToImage:image Text:[NSString stringWithFormat:@"%i",i ]]; [images addObject:image]; } // If you just want to manually add a few images - here is code you can uncomment // NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"Documents/movie.mp4"]]; // NSArray *images = [[NSArray alloc] initWithObjects: // [UIImage imageNamed:@"add_ar.png"], // [UIImage imageNamed:@"add_ja.png"], // [UIImage imageNamed:@"add_ru.png"], // [UIImage imageNamed:@"add_ru.png"], // [UIImage imageNamed:@"add_ar.png"], // [UIImage imageNamed:@"add_ja.png"], // [UIImage imageNamed:@"add_ru.png"], // [UIImage imageNamed:@"add_ar.png"], // [UIImage imageNamed:@"add_en.png"], nil]; [self writeImageAsMovie:images toPath:path size:CGSizeMake(height, width)]; }
这是创build您的AssetWriter并为其添加图像的主要方法。
(2)连接AVAssetWriter
-(void)writeImageAsMovie:(NSArray *)array toPath:(NSString*)path size:(CGSize)size { NSError *error = nil; // FIRST, start up an AVAssetWriter instance to write your video // Give it a destination path (for us: tmp/temp.mov) AVAssetWriter *videoWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:path] fileType:AVFileTypeQuickTimeMovie error:&error]; NSParameterAssert(videoWriter); NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys: AVVideoCodecH264, AVVideoCodecKey, [NSNumber numberWithInt:size.width], AVVideoWidthKey, [NSNumber numberWithInt:size.height], AVVideoHeightKey, nil]; AVAssetWriterInput* writerInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:writerInput sourcePixelBufferAttributes:nil]; NSParameterAssert(writerInput); NSParameterAssert([videoWriter canAddInput:writerInput]); [videoWriter addInput:writerInput];
(3)开始写作会议 (注:方法从上面继续)
//Start a SESSION of writing. // After you start a session, you will keep adding image frames // until you are complete - then you will tell it you are done. [videoWriter startWriting]; // This starts your video at time = 0 [videoWriter startSessionAtSourceTime:kCMTimeZero]; CVPixelBufferRef buffer = NULL; // This was just our utility class to get screen sizes etc. ATHSingleton *singleton = [ATHSingleton singletons]; int i = 0; while (1) { // Check if the writer is ready for more data, if not, just wait if(writerInput.readyForMoreMediaData){ CMTime frameTime = CMTimeMake(150, 600); // CMTime = Value and Timescale. // Timescale = the number of tics per second you want // Value is the number of tics // For us - each frame we add will be 1/4th of a second // Apple recommend 600 tics per second for video because it is a // multiple of the standard video rates 24, 30, 60 fps etc. CMTime lastTime=CMTimeMake(i*150, 600); CMTime presentTime=CMTimeAdd(lastTime, frameTime); if (i == 0) {presentTime = CMTimeMake(0, 600);} // This ensures the first frame starts at 0. if (i >= [array count]) { buffer = NULL; } else { // This command grabs the next UIImage and converts it to a CGImage buffer = [self pixelBufferFromCGImage:[[array objectAtIndex:i] CGImage]]; } if (buffer) { // Give the CGImage to the AVAssetWriter to add to your video [adaptor appendPixelBuffer:buffer withPresentationTime:presentTime]; i++; } else {
(4)完成会议 (注:方法从上面继续)
//Finish the session: // This is important to be done exactly in this order [writerInput markAsFinished]; // WARNING: finishWriting in the solution above is deprecated. // You now need to give a completion handler. [videoWriter finishWritingWithCompletionHandler:^{ NSLog(@"Finished writing...checking completion status..."); if (videoWriter.status != AVAssetWriterStatusFailed && videoWriter.status == AVAssetWriterStatusCompleted) { NSLog(@"Video writing succeeded."); // Move video to camera roll // NOTE: You cannot write directly to the camera roll. // You must first write to an iOS directory then move it! NSURL *videoTempURL = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@", path]]; [self saveToCameraRoll:videoTempURL]; } else { NSLog(@"Video writing failed: %@", videoWriter.error); } }]; // end videoWriter finishWriting Block CVPixelBufferPoolRelease(adaptor.pixelBufferPool); NSLog (@"Done"); break; } } } }
(5)将你的UIImage转换为CVPixelBufferRef
这个方法会给你一个AssetWriter需要的CV像素缓冲区引用。 这是从你的UIImage(上面)获得的CGImageRef。
- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image { // This again was just our utility class for the height & width of the // incoming video (640 height x 480 width) ATHSingleton *singleton = [ATHSingleton singletons]; int height = singleton.screenHeight; int width = singleton.screenWidth; NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey, [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil]; CVPixelBufferRef pxbuffer = NULL; CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer); NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL); CVPixelBufferLockBaseAddress(pxbuffer, 0); void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer); NSParameterAssert(pxdata != NULL); CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(pxdata, width, height, 8, 4*width, rgbColorSpace, kCGImageAlphaNoneSkipFirst); NSParameterAssert(context); CGContextConcatCTM(context, CGAffineTransformMakeRotation(0)); CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image); CGColorSpaceRelease(rgbColorSpace); CGContextRelease(context); CVPixelBufferUnlockBaseAddress(pxbuffer, 0); return pxbuffer; }
(6)将您的video移动到相机胶卷由于AVAssetWriter无法直接写入相机胶卷,因此会将video从“tmp / temp.mov”(或上面命名的任何文件名)移动到相机胶卷。
- (void) saveToCameraRoll:(NSURL *)srcURL { NSLog(@"srcURL: %@", srcURL); ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; ALAssetsLibraryWriteVideoCompletionBlock videoWriteCompletionBlock = ^(NSURL *newURL, NSError *error) { if (error) { NSLog( @"Error writing image with metadata to Photo Library: %@", error ); } else { NSLog( @"Wrote image with metadata to Photo Library %@", newURL.absoluteString); } }; if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:srcURL]) { [library writeVideoAtPathToSavedPhotosAlbum:srcURL completionBlock:videoWriteCompletionBlock]; } }
上面的Zoul的答案给出了你将要做的事情的一个很好的轮廓。 我们广泛地评论了这个代码,这样你就可以看到它是如何使用工作代码完成的。
注意:这是一个Swift 2.1解决scheme(iOS8 +,XCode 7.2) 。
上周我着手编写iOS代码来从图像生成video。 我有一点AVFoundation的经验,但从来没有听说过CVPixelBuffer。 我在这页上也看到了答案。 花了好几天的时间来解剖一切,然后把这些东西放在一起,以一种对我的大脑有意义的方式。 以下是我想出的。
注:如果您复制/粘贴下面的所有代码到一个单一的Swift文件,它应该编译。 你只需要调整loadImages()
和RenderSettings
值。
第1部分:设置
在这里,我将所有与导出相关的设置分组到一个RenderSettings
结构中。
import AVFoundation import UIKit import Photos struct RenderSettings { var width: CGFloat = 1280 var height: CGFloat = 720 var fps: Int32 = 2 // 2 frames per second var avCodecKey = AVVideoCodecH264 var videoFilename = "render" var videoFilenameExt = "mp4" var size: CGSize { return CGSize(width: width, height: height) } var outputURL: NSURL { // Use the CachesDirectory so the rendered video file sticks around as long as we need it to. // Using the CachesDirectory ensures the file won't be included in a backup of the app. let fileManager = NSFileManager.defaultManager() if let tmpDirURL = try? fileManager.URLForDirectory(.CachesDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) { return tmpDirURL.URLByAppendingPathComponent(videoFilename).URLByAppendingPathExtension(videoFilenameExt) } fatalError("URLForDirectory() failed") } }
第2部分:ImageAnimator
ImageAnimator
类知道您的图像,并使用VideoWriter
类来执行渲染。 这个想法是保持video内容代码与低级AVFoundation代码分开。 我还在这里添加了saveToLibrary()
作为一个类的函数,这个函数在链的最后被调用来保存video到Photo Library。
class ImageAnimator { // Apple suggests a timescale of 600 because it's a multiple of standard video rates 24, 25, 30, 60 fps etc. static let kTimescale: Int32 = 600 let settings: RenderSettings let videoWriter: VideoWriter var images: [UIImage]! var frameNum = 0 class func saveToLibrary(videoURL: NSURL) { PHPhotoLibrary.requestAuthorization { status in guard status == .Authorized else { return } PHPhotoLibrary.sharedPhotoLibrary().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(videoURL) }) { success, error in if !success { print("Could not save video to photo library:", error) } } } } class func removeFileAtURL(fileURL: NSURL) { do { try NSFileManager.defaultManager().removeItemAtPath(fileURL.path!) } catch _ as NSError { // Assume file doesn't exist. } } init(renderSettings: RenderSettings) { settings = renderSettings videoWriter = VideoWriter(renderSettings: settings) images = loadImages() } func render(completion: ()->Void) { // The VideoWriter will fail if a file exists at the URL, so clear it out first. ImageAnimator.removeFileAtURL(settings.outputURL) videoWriter.start() videoWriter.render(appendPixelBuffers) { ImageAnimator.saveToLibrary(self.settings.outputURL) completion() } } // Replace this logic with your own. func loadImages() -> [UIImage] { var images = [UIImage]() for index in 1...10 { let filename = "\(index).jpg" images.append(UIImage(named: filename)!) } return images } // This is the callback function for VideoWriter.render() func appendPixelBuffers(writer: VideoWriter) -> Bool { let frameDuration = CMTimeMake(Int64(ImageAnimator.kTimescale / settings.fps), ImageAnimator.kTimescale) while !images.isEmpty { if writer.isReadyForData == false { // Inform writer we have more buffers to write. return false } let image = images.removeFirst() let presentationTime = CMTimeMultiply(frameDuration, Int32(frameNum)) let success = videoWriter.addImage(image, withPresentationTime: presentationTime) if success == false { fatalError("addImage() failed") } frameNum++ } // Inform writer all buffers have been written. return true } }
第3部分:VideoWriter
VideoWriter
类可以完成所有AVFoundation的繁重工作。 它主要是AVAssetWriter
和AVAssetWriterInput
的封装。 它也包含了我不知道如何将图像转换为CVPixelBuffer
花式代码。
class VideoWriter { let renderSettings: RenderSettings var videoWriter: AVAssetWriter! var videoWriterInput: AVAssetWriterInput! var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor! var isReadyForData: Bool { return videoWriterInput?.readyForMoreMediaData ?? false } class func pixelBufferFromImage(image: UIImage, pixelBufferPool: CVPixelBufferPool, size: CGSize) -> CVPixelBuffer { var pixelBufferOut: CVPixelBuffer? let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pixelBufferOut) if status != kCVReturnSuccess { fatalError("CVPixelBufferPoolCreatePixelBuffer() failed") } let pixelBuffer = pixelBufferOut! CVPixelBufferLockBaseAddress(pixelBuffer, 0) let data = CVPixelBufferGetBaseAddress(pixelBuffer) let rgbColorSpace = CGColorSpaceCreateDeviceRGB() let context = CGBitmapContextCreate(data, Int(size.width), Int(size.height), 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, CGImageAlphaInfo.PremultipliedFirst.rawValue) CGContextClearRect(context, CGRectMake(0, 0, size.width, size.height)) let horizontalRatio = size.width / image.size.width let verticalRatio = size.height / image.size.height //aspectRatio = max(horizontalRatio, verticalRatio) // ScaleAspectFill let aspectRatio = min(horizontalRatio, verticalRatio) // ScaleAspectFit let newSize = CGSize(width: image.size.width * aspectRatio, height: image.size.height * aspectRatio) let x = newSize.width < size.width ? (size.width - newSize.width) / 2 : 0 let y = newSize.height < size.height ? (size.height - newSize.height) / 2 : 0 CGContextDrawImage(context, CGRectMake(x, y, newSize.width, newSize.height), image.CGImage) CVPixelBufferUnlockBaseAddress(pixelBuffer, 0) return pixelBuffer } init(renderSettings: RenderSettings) { self.renderSettings = renderSettings } func start() { let avOutputSettings: [String: AnyObject] = [ AVVideoCodecKey: renderSettings.avCodecKey, AVVideoWidthKey: NSNumber(float: Float(renderSettings.width)), AVVideoHeightKey: NSNumber(float: Float(renderSettings.height)) ] func createPixelBufferAdaptor() { let sourcePixelBufferAttributesDictionary = [ kCVPixelBufferPixelFormatTypeKey as String: NSNumber(unsignedInt: kCVPixelFormatType_32ARGB), kCVPixelBufferWidthKey as String: NSNumber(float: Float(renderSettings.width)), kCVPixelBufferHeightKey as String: NSNumber(float: Float(renderSettings.height)) ] pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary) } func createAssetWriter(outputURL: NSURL) -> AVAssetWriter { guard let assetWriter = try? AVAssetWriter(URL: outputURL, fileType: AVFileTypeMPEG4) else { fatalError("AVAssetWriter() failed") } guard assetWriter.canApplyOutputSettings(avOutputSettings, forMediaType: AVMediaTypeVideo) else { fatalError("canApplyOutputSettings() failed") } return assetWriter } videoWriter = createAssetWriter(renderSettings.outputURL) videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: avOutputSettings) if videoWriter.canAddInput(videoWriterInput) { videoWriter.addInput(videoWriterInput) } else { fatalError("canAddInput() returned false") } // The pixel buffer adaptor must be created before we start writing. createPixelBufferAdaptor() if videoWriter.startWriting() == false { fatalError("startWriting() failed") } videoWriter.startSessionAtSourceTime(kCMTimeZero) precondition(pixelBufferAdaptor.pixelBufferPool != nil, "nil pixelBufferPool") } func render(appendPixelBuffers: (VideoWriter)->Bool, completion: ()->Void) { precondition(videoWriter != nil, "Call start() to initialze the writer") let queue = dispatch_queue_create("mediaInputQueue", nil) videoWriterInput.requestMediaDataWhenReadyOnQueue(queue) { let isFinished = appendPixelBuffers(self) if isFinished { self.videoWriterInput.markAsFinished() self.videoWriter.finishWritingWithCompletionHandler() { dispatch_async(dispatch_get_main_queue()) { completion() } } } else { // Fall through. The closure will be called again when the writer is ready. } } } func addImage(image: UIImage, withPresentationTime presentationTime: CMTime) -> Bool { precondition(pixelBufferAdaptor != nil, "Call start() to initialze the writer") let pixelBuffer = VideoWriter.pixelBufferFromImage(image, pixelBufferPool: pixelBufferAdaptor.pixelBufferPool!, size: renderSettings.size) return pixelBufferAdaptor.appendPixelBuffer(pixelBuffer, withPresentationTime: presentationTime) } }
第4部分:做到这一点
一旦一切就绪,这些就是你的三条神奇线条:
let settings = RenderSettings() let imageAnimator = ImageAnimator(renderSettings: settings) imageAnimator.render() { print("yes") }
我把Zoul的主要想法和AVAssetWriterInputPixelBufferAdaptor方法结合起来,并从中取出了一些小框架的开头。
随时检查出来,并改善它! CEMovieMaker
这里是在iOS 8上testing的Swift 2.x版本。它将来自@Scott Raposa和@Praxiteles的答案以及来自@acj的代码一起提供给另一个问题。 来自@acj的代码在这里: https ://gist.github.com/acj/6ae90aa1ebb8cad6b47b。 @TimBull也提供了代码。
像@Scott Raposa一样,我从来没有听说过CVPixelBufferPoolCreatePixelBuffer
和其他几个函数,更不用说理解如何使用它们了。
你在下面看到的主要是通过反复试验和阅读苹果文件拼凑起来的。 请谨慎使用,如有错误,请提供build议。
用法:
import UIKit import AVFoundation import Photos writeImagesAsMovie(yourImages, videoPath: yourPath, videoSize: yourSize, videoFPS: 30)
码:
func writeImagesAsMovie(allImages: [UIImage], videoPath: String, videoSize: CGSize, videoFPS: Int32) { // Create AVAssetWriter to write video guard let assetWriter = createAssetWriter(videoPath, size: videoSize) else { print("Error converting images to video: AVAssetWriter not created") return } // If here, AVAssetWriter exists so create AVAssetWriterInputPixelBufferAdaptor let writerInput = assetWriter.inputs.filter{ $0.mediaType == AVMediaTypeVideo }.first! let sourceBufferAttributes : [String : AnyObject] = [ kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32ARGB), kCVPixelBufferWidthKey as String : videoSize.width, kCVPixelBufferHeightKey as String : videoSize.height, ] let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: sourceBufferAttributes) // Start writing session assetWriter.startWriting() assetWriter.startSessionAtSourceTime(kCMTimeZero) if (pixelBufferAdaptor.pixelBufferPool == nil) { print("Error converting images to video: pixelBufferPool nil after starting session") return } // -- Create queue for <requestMediaDataWhenReadyOnQueue> let mediaQueue = dispatch_queue_create("mediaInputQueue", nil) // -- Set video parameters let frameDuration = CMTimeMake(1, videoFPS) var frameCount = 0 // -- Add images to video let numImages = allImages.count writerInput.requestMediaDataWhenReadyOnQueue(mediaQueue, usingBlock: { () -> Void in // Append unadded images to video but only while input ready while (writerInput.readyForMoreMediaData && frameCount < numImages) { let lastFrameTime = CMTimeMake(Int64(frameCount), videoFPS) let presentationTime = frameCount == 0 ? lastFrameTime : CMTimeAdd(lastFrameTime, frameDuration) if !self.appendPixelBufferForImageAtURL(allImages[frameCount], pixelBufferAdaptor: pixelBufferAdaptor, presentationTime: presentationTime) { print("Error converting images to video: AVAssetWriterInputPixelBufferAdapter failed to append pixel buffer") return } frameCount += 1 } // No more images to add? End video. if (frameCount >= numImages) { writerInput.markAsFinished() assetWriter.finishWritingWithCompletionHandler { if (assetWriter.error != nil) { print("Error converting images to video: \(assetWriter.error)") } else { self.saveVideoToLibrary(NSURL(fileURLWithPath: videoPath)) print("Converted images to movie @ \(videoPath)") } } } }) } func createAssetWriter(path: String, size: CGSize) -> AVAssetWriter? { // Convert <path> to NSURL object let pathURL = NSURL(fileURLWithPath: path) // Return new asset writer or nil do { // Create asset writer let newWriter = try AVAssetWriter(URL: pathURL, fileType: AVFileTypeMPEG4) // Define settings for video input let videoSettings: [String : AnyObject] = [ AVVideoCodecKey : AVVideoCodecH264, AVVideoWidthKey : size.width, AVVideoHeightKey : size.height, ] // Add video input to writer let assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings) newWriter.addInput(assetWriterVideoInput) // Return writer print("Created asset writer for \(size.width)x\(size.height) video") return newWriter } catch { print("Error creating asset writer: \(error)") return nil } } func appendPixelBufferForImageAtURL(image: UIImage, pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool { var appendSucceeded = false autoreleasepool { if 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() // Create CGBitmapContext let context = CGBitmapContextCreate( pixelData, Int(image.size.width), Int(image.size.height), 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, CGImageAlphaInfo.PremultipliedFirst.rawValue ) // Draw image into context CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), image.CGImage) CVPixelBufferUnlockBaseAddress(pixelBuffer, 0) } func saveVideoToLibrary(videoURL: NSURL) { PHPhotoLibrary.requestAuthorization { status in // Return if unauthorized guard status == .Authorized else { print("Error saving video: unauthorized access") return } // If here, save video to library PHPhotoLibrary.sharedPhotoLibrary().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(videoURL) }) { success, error in if !success { print("Error saving video: \(error)") } } } }
Here's the swift3 version how to convert Images array to the Video
import Foundation import AVFoundation import UIKit typealias CXEMovieMakerCompletion = (URL) -> Void typealias CXEMovieMakerUIImageExtractor = (AnyObject) -> UIImage? public class ImagesToVideoUtils: NSObject { static let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) static let tempPath = paths[0] + "/exprotvideo.mp4" static let fileURL = URL(fileURLWithPath: tempPath) // static let tempPath = NSTemporaryDirectory() + "/exprotvideo.mp4" // static let fileURL = URL(fileURLWithPath: tempPath) var assetWriter:AVAssetWriter! var writeInput:AVAssetWriterInput! var bufferAdapter:AVAssetWriterInputPixelBufferAdaptor! var videoSettings:[String : Any]! var frameTime:CMTime! //var fileURL:URL! var completionBlock: CXEMovieMakerCompletion? var movieMakerUIImageExtractor:CXEMovieMakerUIImageExtractor? public class func videoSettings(codec:String, width:Int, height:Int) -> [String: Any]{ if(Int(width) % 16 != 0){ print("warning: video settings width must be divisible by 16") } let videoSettings:[String: Any] = [AVVideoCodecKey: AVVideoCodecJPEG, //AVVideoCodecH264, AVVideoWidthKey: width, AVVideoHeightKey: height] return videoSettings } public init(videoSettings: [String: Any]) { super.init() if(FileManager.default.fileExists(atPath: ImagesToVideoUtils.tempPath)){ guard (try? FileManager.default.removeItem(atPath: ImagesToVideoUtils.tempPath)) != nil else { print("remove path failed") return } } self.assetWriter = try! AVAssetWriter(url: ImagesToVideoUtils.fileURL, fileType: AVFileTypeQuickTimeMovie) self.videoSettings = videoSettings self.writeInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings) assert(self.assetWriter.canAdd(self.writeInput), "add failed") self.assetWriter.add(self.writeInput) let bufferAttributes:[String: Any] = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32ARGB)] self.bufferAdapter = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: self.writeInput, sourcePixelBufferAttributes: bufferAttributes) self.frameTime = CMTimeMake(1, 5) } func createMovieFrom(urls: [URL], withCompletion: @escaping CXEMovieMakerCompletion){ self.createMovieFromSource(images: urls as [AnyObject], extractor:{(inputObject:AnyObject) ->UIImage? in return UIImage(data: try! Data(contentsOf: inputObject as! URL))}, withCompletion: withCompletion) } func createMovieFrom(images: [UIImage], withCompletion: @escaping CXEMovieMakerCompletion){ self.createMovieFromSource(images: images, extractor: {(inputObject:AnyObject) -> UIImage? in return inputObject as? UIImage}, withCompletion: withCompletion) } func createMovieFromSource(images: [AnyObject], extractor: @escaping CXEMovieMakerUIImageExtractor, withCompletion: @escaping CXEMovieMakerCompletion){ self.completionBlock = withCompletion self.assetWriter.startWriting() self.assetWriter.startSession(atSourceTime: kCMTimeZero) let mediaInputQueue = DispatchQueue(label: "mediaInputQueue") var i = 0 let frameNumber = images.count self.writeInput.requestMediaDataWhenReady(on: mediaInputQueue){ while(true){ if(i >= frameNumber){ break } if (self.writeInput.isReadyForMoreMediaData){ var sampleBuffer:CVPixelBuffer? autoreleasepool{ let img = extractor(images[i]) if img == nil{ i += 1 print("Warning: counld not extract one of the frames") //continue } sampleBuffer = self.newPixelBufferFrom(cgImage: img!.cgImage!) } if (sampleBuffer != nil){ if(i == 0){ self.bufferAdapter.append(sampleBuffer!, withPresentationTime: kCMTimeZero) }else{ let value = i - 1 let lastTime = CMTimeMake(Int64(value), self.frameTime.timescale) let presentTime = CMTimeAdd(lastTime, self.frameTime) self.bufferAdapter.append(sampleBuffer!, withPresentationTime: presentTime) } i = i + 1 } } } self.writeInput.markAsFinished() self.assetWriter.finishWriting { DispatchQueue.main.sync { self.completionBlock!(ImagesToVideoUtils.fileURL) } } } } func newPixelBufferFrom(cgImage:CGImage) -> CVPixelBuffer?{ let options:[String: Any] = [kCVPixelBufferCGImageCompatibilityKey as String: true, kCVPixelBufferCGBitmapContextCompatibilityKey as String: true] var pxbuffer:CVPixelBuffer? let frameWidth = self.videoSettings[AVVideoWidthKey] as! Int let frameHeight = self.videoSettings[AVVideoHeightKey] as! Int let status = CVPixelBufferCreate(kCFAllocatorDefault, frameWidth, frameHeight, kCVPixelFormatType_32ARGB, options as CFDictionary?, &pxbuffer) assert(status == kCVReturnSuccess && pxbuffer != nil, "newPixelBuffer failed") CVPixelBufferLockBaseAddress(pxbuffer!, CVPixelBufferLockFlags(rawValue: 0)) let pxdata = CVPixelBufferGetBaseAddress(pxbuffer!) let rgbColorSpace = CGColorSpaceCreateDeviceRGB() let context = CGContext(data: pxdata, width: frameWidth, height: frameHeight, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pxbuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) assert(context != nil, "context is nil") context!.concatenate(CGAffineTransform.identity) context!.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)) CVPixelBufferUnlockBaseAddress(pxbuffer!, CVPixelBufferLockFlags(rawValue: 0)) return pxbuffer } }
I use it together with screen capturing, to basically create a video of screen capturing, here's the full story/complete example .
Just translated @Scott Raposa answer to swift3 (with some very little changes):
import AVFoundation import UIKit import Photos struct RenderSettings { var size : CGSize = .zero var fps: Int32 = 6 // frames per second var avCodecKey = AVVideoCodecH264 var videoFilename = "render" var videoFilenameExt = "mp4" var outputURL: URL { // Use the CachesDirectory so the rendered video file sticks around as long as we need it to. // Using the CachesDirectory ensures the file won't be included in a backup of the app. let fileManager = FileManager.default if let tmpDirURL = try? fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) { return tmpDirURL.appendingPathComponent(videoFilename).appendingPathExtension(videoFilenameExt) } fatalError("URLForDirectory() failed") } } class ImageAnimator { // Apple suggests a timescale of 600 because it's a multiple of standard video rates 24, 25, 30, 60 fps etc. static let kTimescale: Int32 = 600 let settings: RenderSettings let videoWriter: VideoWriter var images: [UIImage]! var frameNum = 0 class func saveToLibrary(videoURL: URL) { PHPhotoLibrary.requestAuthorization { status in guard status == .authorized else { return } PHPhotoLibrary.shared().performChanges({ PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL) }) { success, error in if !success { print("Could not save video to photo library:", error) } } } } class func removeFileAtURL(fileURL: URL) { do { try FileManager.default.removeItem(atPath: fileURL.path) } catch _ as NSError { // Assume file doesn't exist. } } init(renderSettings: RenderSettings) { settings = renderSettings videoWriter = VideoWriter(renderSettings: settings) // images = loadImages() } func render(completion: (()->Void)?) { // The VideoWriter will fail if a file exists at the URL, so clear it out first. ImageAnimator.removeFileAtURL(fileURL: settings.outputURL) videoWriter.start() videoWriter.render(appendPixelBuffers: appendPixelBuffers) { ImageAnimator.saveToLibrary(videoURL: self.settings.outputURL) completion?() } } // // Replace this logic with your own. // func loadImages() -> [UIImage] { // var images = [UIImage]() // for index in 1...10 { // let filename = "\(index).jpg" // images.append(UIImage(named: filename)!) // } // return images // } // This is the callback function for VideoWriter.render() func appendPixelBuffers(writer: VideoWriter) -> Bool { let frameDuration = CMTimeMake(Int64(ImageAnimator.kTimescale / settings.fps), ImageAnimator.kTimescale) while !images.isEmpty { if writer.isReadyForData == false { // Inform writer we have more buffers to write. return false } let image = images.removeFirst() let presentationTime = CMTimeMultiply(frameDuration, Int32(frameNum)) let success = videoWriter.addImage(image: image, withPresentationTime: presentationTime) if success == false { fatalError("addImage() failed") } frameNum += 1 } // Inform writer all buffers have been written. return true } } class VideoWriter { let renderSettings: RenderSettings var videoWriter: AVAssetWriter! var videoWriterInput: AVAssetWriterInput! var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor! var isReadyForData: Bool { return videoWriterInput?.isReadyForMoreMediaData ?? false } class func pixelBufferFromImage(image: UIImage, pixelBufferPool: CVPixelBufferPool, size: CGSize) -> CVPixelBuffer { var pixelBufferOut: CVPixelBuffer? let status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pixelBufferOut) if status != kCVReturnSuccess { fatalError("CVPixelBufferPoolCreatePixelBuffer() failed") } let pixelBuffer = pixelBufferOut! CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) let data = CVPixelBufferGetBaseAddress(pixelBuffer) let rgbColorSpace = CGColorSpaceCreateDeviceRGB() let context = CGContext(data: data, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) context!.clear(CGRect(x:0,y: 0,width: size.width,height: size.height)) let horizontalRatio = size.width / image.size.width let verticalRatio = size.height / image.size.height //aspectRatio = max(horizontalRatio, verticalRatio) // ScaleAspectFill let aspectRatio = min(horizontalRatio, verticalRatio) // ScaleAspectFit let newSize = CGSize(width: image.size.width * aspectRatio, height: image.size.height * aspectRatio) let x = newSize.width < size.width ? (size.width - newSize.width) / 2 : 0 let y = newSize.height < size.height ? (size.height - newSize.height) / 2 : 0 context?.draw(image.cgImage!, in: CGRect(x:x,y: y, width: newSize.width, height: newSize.height)) CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) return pixelBuffer } init(renderSettings: RenderSettings) { self.renderSettings = renderSettings } func start() { let avOutputSettings: [String: Any] = [ AVVideoCodecKey: renderSettings.avCodecKey, AVVideoWidthKey: NSNumber(value: Float(renderSettings.size.width)), AVVideoHeightKey: NSNumber(value: Float(renderSettings.size.height)) ] func createPixelBufferAdaptor() { let sourcePixelBufferAttributesDictionary = [ kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32ARGB), kCVPixelBufferWidthKey as String: NSNumber(value: Float(renderSettings.size.width)), kCVPixelBufferHeightKey as String: NSNumber(value: Float(renderSettings.size.height)) ] pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary) } func createAssetWriter(outputURL: URL) -> AVAssetWriter { guard let assetWriter = try? AVAssetWriter(outputURL: outputURL, fileType: AVFileTypeMPEG4) else { fatalError("AVAssetWriter() failed") } guard assetWriter.canApply(outputSettings: avOutputSettings, forMediaType: AVMediaTypeVideo) else { fatalError("canApplyOutputSettings() failed") } return assetWriter } videoWriter = createAssetWriter(outputURL: renderSettings.outputURL) videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: avOutputSettings) if videoWriter.canAdd(videoWriterInput) { videoWriter.add(videoWriterInput) } else { fatalError("canAddInput() returned false") } // The pixel buffer adaptor must be created before we start writing. createPixelBufferAdaptor() if videoWriter.startWriting() == false { fatalError("startWriting() failed") } videoWriter.startSession(atSourceTime: kCMTimeZero) precondition(pixelBufferAdaptor.pixelBufferPool != nil, "nil pixelBufferPool") } func render(appendPixelBuffers: ((VideoWriter)->Bool)?, completion: (()->Void)?) { precondition(videoWriter != nil, "Call start() to initialze the writer") let queue = DispatchQueue(label: "mediaInputQueue") videoWriterInput.requestMediaDataWhenReady(on: queue) { let isFinished = appendPixelBuffers?(self) ?? false if isFinished { self.videoWriterInput.markAsFinished() self.videoWriter.finishWriting() { DispatchQueue.main.async { completion?() } } } else { // Fall through. The closure will be called again when the writer is ready. } } } func addImage(image: UIImage, withPresentationTime presentationTime: CMTime) -> Bool { precondition(pixelBufferAdaptor != nil, "Call start() to initialze the writer") let pixelBuffer = VideoWriter.pixelBufferFromImage(image: image, pixelBufferPool: pixelBufferAdaptor.pixelBufferPool!, size: renderSettings.size) return pixelBufferAdaptor.append(pixelBuffer, withPresentationTime: presentationTime) } }
Well this is a bit hard to be implemented in pure Objective-C….If you are developing for jailbroken devices , a good idea is to use the command-line tool ffmpeg from inside your app. it's quite easy to create a movie from images with a command like:
ffmpeg -r 10 -b 1800 -i %03d.jpg test1800.mp4
Note that the images have to be named sequentially , and also be placed in the same directory. For more information take a look at: http://electron.mit.edu/~gsteele/ffmpeg/
Use AVAssetWriter to write images as movie. I already have answered here:- https://stackoverflow.com/a/19166876/1582217