最近在做一个东西的时候,需要把一张或者多张图片合成为一个视频文件,并加入到视频轨道中进行播放或者导出,而不单单是把图片当做水印加到现有的视频上,做的时候首先考虑到的当然是AVAssetWriter,但是在做的时候还是遇到了很多问题,首先我用的是swift 3,翻遍了StackOverFlow也没有找到合适的,当然也有点偷懒的意思,想直接搜来完事儿。
先上个链接,思路和方法姿势全都有了,http://stackoverflow.com/questions/3741323/how-do-i-export-uiimage-array-as-a-movie?noredirect=1&lq=1,具体的步骤及涉及到点还请点看来看下,当然如果是为了省事儿,下面我也会直接给出demo,还合适你拿去就好。
第一次做的时候,因为里面的很多接口及操作都是异步的方式,但是当在测试的时候,发现我现有流程的代码中用需要和其他流程的代码同步,所以又改成了同步的方式。下面就直接贴代码了:
异步的方式:
//
// DOVImagesToVideoAsync.swift
// ImagesToVideo
//
// Created by Wulei on 2016/12/24.
// Copyright © 2016年 wulei. All rights reserved.
//
import Foundation
import AVFoundation
import UIKit
typealias DOVMovieMakerCompletion = (URL) -> Void
fileprivate typealias DOVMovieMakerUIImageExtractor = (AnyObject) -> UIImage?
class DOVImageToVideoAsync: NSObject{
//MARK: Private Properties
private var assetWriter:AVAssetWriter!
private var writeInput:AVAssetWriterInput!
private var bufferAdapter:AVAssetWriterInputPixelBufferAdaptor!
private var videoSettings:[String : Any]!
private var frameTime:CMTime!
private var fileURL:URL!
private var completionBlock: DOVMovieMakerCompletion?
//MARK: Class Method
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: AVVideoCodecH264,
AVVideoWidthKey: width,
AVVideoHeightKey: height]
return videoSettings
}
//MARK: Public methods
init(videoSettings: [String: Any]) {
super.init()
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
var tempPath:String
repeat{
let random = arc4random()
tempPath = paths[0] + "/\(random).mp4"
}while(FileManager.default.fileExists(atPath: tempPath))
// let tempPath = paths[0] + "/exprotvideo.mp4"
// if(FileManager.default.fileExists(atPath: tempPath)){
// guard (try? FileManager.default.removeItem(atPath: tempPath)) != nil else {
// print("remove path failed")
// return
// }
// }
self.fileURL = URL(fileURLWithPath: tempPath)
self.assetWriter = try! AVAssetWriter(url: self.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, 1)
}
func createMovieFrom(urls: [URL], withCompletion: @escaping DOVMovieMakerCompletion){
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 DOVMovieMakerCompletion){
self.createMovieFromSource(images: images, extractor: {(inputObject:AnyObject) -> UIImage? in
return inputObject as? UIImage}, withCompletion: withCompletion)
}
//MARK: Private methods
private func createMovieFromSource(images: [AnyObject], extractor: @escaping DOVMovieMakerUIImageExtractor, 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!(self.fileURL)
}
}
}
}
private 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
}
}
//
// DOVImagesToVideoSync.swift
// ImagesToVideo
//
// Created by Wulei on 2016/12/24.
// Copyright © 2016年 wulei. All rights reserved.
//
import Foundation
import AVFoundation
import UIKit
fileprivate typealias DOVMovieMakerUIImageExtractor = (AnyObject) -> UIImage?
class DOVImageToVideoSync: NSObject{
//MARK: Private Properties
private var assetWriter:AVAssetWriter!
private var writeInput:AVAssetWriterInput!
private var bufferAdapter:AVAssetWriterInputPixelBufferAdaptor!
private var videoSettings:[String : Any]!
private var frameTime:CMTime!
private var fileURL:URL!
//MARK: Class Method
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: AVVideoCodecH264,
AVVideoWidthKey: width,
AVVideoHeightKey: height]
return videoSettings
}
//MARK: Public methods
init(videoSettings: [String: Any]) {
super.init()
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
var tempPath:String
repeat{
let random = arc4random()
tempPath = paths[0] + "/\(random).mp4"
}while(FileManager.default.fileExists(atPath: tempPath))
self.fileURL = URL(fileURLWithPath: tempPath)
self.assetWriter = try! AVAssetWriter(url: self.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(600, 600)
}
func createMovieFrom(url: URL, duration:Int) -> URL{
var urls = [URL]()
var index = duration
while(index > 0){
urls.append(url)
index -= 1
}
return self.createMovieFromSource(images: urls as [AnyObject], extractor:{(inputObject:AnyObject) ->UIImage? in
return UIImage(data: try! Data(contentsOf: inputObject as! URL))})
}
func createMovieFrom(image: UIImage, duration:Int) -> URL{
var images = [UIImage]()
var index = duration
while(index > 0){
images.append(image)
index -= 1
}
return self.createMovieFromSource(images: images, extractor: {(inputObject:AnyObject) -> UIImage? in
return inputObject as? UIImage})
}
//MARK: Private methods
private func createMovieFromSource(images: [AnyObject], extractor: @escaping DOVMovieMakerUIImageExtractor) -> URL{
self.assetWriter.startWriting()
// self.assetWriter.startSession(atSourceTime: kCMTimeZero)
let zeroTime = CMTimeMake(Int64(0),self.frameTime.timescale)
self.assetWriter.startSession(atSourceTime: zeroTime)
var i = 0
let frameNumber = images.count
while !self.writeInput.isReadyForMoreMediaData {}
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 {}
var isSuccess:Bool = false
while(!isSuccess){
switch self.assetWriter.status {
case .completed:
isSuccess = true
print("completed")
case .writing:
sleep(1)
print("writing")
case .failed:
isSuccess = true
print("failed")
case .cancelled:
isSuccess = true
print("cancelled")
default:
isSuccess = true
print("unknown")
}
}
return self.fileURL
}
private 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
}
}
Demo程序:https://github.com/Willib/ImagesToVideo