iOS中將多張圖片合成為可匯出可播放的視訊檔案(Swift 3)
阿新 • • 發佈:2019-02-17
最近在做一個東西的時候,需要把一張或者多張圖片合成為一個視訊檔案,並加入到視訊軌道中進行播放或者匯出,而不單單是把圖片當做水印加到現有的視訊上,做的時候首先考慮到的當然是AVAssetWriter,但是在做的時候還是遇到了很多問題,首先我用的是swift 3,翻遍了StackOverFlow也沒有找到合適的,當然也有點偷懶的意思,想直接搜來完事兒。
第一次做的時候,因為裡面的很多介面及操作都是非同步的方式,但是當在測試的時候,發現我現有流程的程式碼中用需要和其他流程的程式碼同步,所以又改成了同步的方式。下面就直接貼程式碼了:
非同步的方式:
// // 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 } }
執行結果截圖: