将 UIImages 数组转换为视频时出现黑框

Black frames while converting array of UIImages to Video

我正在尝试将一组 UIImages 转换为视频,但结果文件中有很多黑帧(例如,开头有 4 个黑帧,后面有 3 个好帧,然后是 3 个黑帧帧和 2 个好帧,这种模式一直重复到视频结束)。

我的代码基于this solution,但我认为问题的主要来源应该在这部分代码中:

func build(progress: (NSProgress -> Void), success: (NSURL -> Void), failure: (NSError -> Void)) {
    //videosizes and path to temp output file
    let inputSize = CGSize(width: 568, height: 320)
    let outputSize = CGSize(width: 568, height: 320)
    var error: NSError?
    let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! NSString
    let videoOutputURL = NSURL(fileURLWithPath: documentsPath.stringByAppendingPathComponent("TempVideo.mov"))!
    NSFileManager.defaultManager().removeItemAtURL(videoOutputURL, error: nil)

    videoWriter = AVAssetWriter(URL: videoOutputURL, fileType: AVFileTypeMPEG4, error: &error)

    if let videoWriter = videoWriter {
        let videoSettings: [NSObject : AnyObject] = [
            AVVideoCodecKey  : AVVideoCodecH264,
            AVVideoWidthKey  : outputSize.width,
            AVVideoHeightKey : outputSize.height,
        ]

        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

                for var i = 0; i < self.photoURLs.count - 1; i++ {

                    var currentFrame = self.photoURLs[i]
                    var lastFrameTime = CMTimeMake(Int64(i), fps)                       
                    var presentationTime = CMTimeAdd(lastFrameTime, frameDuration)

                    //this one is needed because sometimes videoWriter is not ready, and we have to wait for a while
                    while videoWriterInput.readyForMoreMediaData == false {
                        var maxDate = NSDate(timeIntervalSinceNow: 0.5)
                        var currentRunLoop = NSRunLoop()
                        currentRunLoop.runUntilDate(maxDate)

                    }

                    self.appendPixelBufferForImageAtURL(currentFrame, pixelBufferAdaptor: pixelBufferAdaptor, presentationTime: presentationTime)

                    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)
    }
}

显然我做错了什么,但是什么?我觉得应该是这里,因为有些图片转换没有问题,但是pixelbuffer还有两个函数:

func appendPixelBufferForImageAtURL(image: UIImage, pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool {
    var appendSucceeded = true

    autoreleasepool {


                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:UInt8 = UInt8(CVPixelBufferLockBaseAddress(pixelBuffer, 0))

    let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer)
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedFirst.rawValue)
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()

    let context = CGBitmapContextCreate(
        pixelData,
        Int(568),
        Int(320),
        8,
        Int(8 * 320),
        rgbColorSpace,
        bitmapInfo
    )



    var imageDataProvider = CGDataProviderCreateWithCFData(imageData)
    var imageRef = CGImageCreateWithJPEGDataProvider(imageDataProvider, nil, true, kCGRenderingIntentDefault)

    CGContextDrawImage(context, CGRectMake(0, 0, 568, 320), imageRef)

    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)
}

所以我能够通过使用我在此处找到的示例重写 fillPixelBufferFromImage 来解决此问题:CVPixelBufferPool Error ( kCVReturnInvalidArgument/-6661)

这是对我有用的 Swift 2 - Xcode 7 GM 解决方案:

 public func build(progress: (NSProgress -> Void), success: (NSURL -> Void), failure: (NSError -> Void)) {
    let inputSize = CGSize(width: 600, height: 600)
    let outputSize = CGSize(width: 600, height: 600)
    var error: NSError?

    let fileManager = NSFileManager.defaultManager()
    let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
    guard let documentDirectory: NSURL = urls.first else {
      fatalError("documentDir Error")
    }

    let videoOutputURL = documentDirectory.URLByAppendingPathComponent("AssembledVideo.mov")

    if NSFileManager.defaultManager().fileExistsAtPath(videoOutputURL.path!) {
      do {
        try NSFileManager.defaultManager().removeItemAtPath(videoOutputURL.path!)
      }catch{
        fatalError("Unable to delete file: \(error) : \(__FUNCTION__).")
      }
    }

    guard let videoWriter = try? AVAssetWriter(URL: videoOutputURL, fileType: AVFileTypeQuickTimeMovie) else{
      fatalError("AVAssetWriter error")
    }

    let outputSettings = [
      AVVideoCodecKey  : AVVideoCodecH264,
      AVVideoWidthKey  : NSNumber(float: Float(outputSize.width)),
      AVVideoHeightKey : NSNumber(float: Float(outputSize.height)),
    ]

    guard videoWriter.canApplyOutputSettings(outputSettings, forMediaType: AVMediaTypeVideo) else {
      fatalError("Negative : Can't apply the Output settings...")
    }

    let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: outputSettings)

    let sourcePixelBufferAttributesDictionary = [
      kCVPixelBufferPixelFormatTypeKey as String: NSNumber(unsignedInt: kCVPixelFormatType_32ARGB),
      kCVPixelBufferWidthKey as String: NSNumber(float: Float(inputSize.width)),
      kCVPixelBufferHeightKey as String: NSNumber(float: Float(inputSize.height)),
    ]

    let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(
      assetWriterInput: videoWriterInput,
      sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary
    )

    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 = 1
        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)
    }
  }

  public func appendPixelBufferForImageAtURL(urlString: String, pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool {
    var appendSucceeded = true

    autoreleasepool {
      if let image = UIImage(contentsOfFile: urlString) {

          var pixelBuffer: CVPixelBuffer? = nil
          let status: CVReturn = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferAdaptor.pixelBufferPool!, &pixelBuffer)

          if let pixelBuffer = pixelBuffer where status == 0 {
            let managedPixelBuffer = pixelBuffer

            fillPixelBufferFromImage(image.CGImage!, pixelBuffer: managedPixelBuffer)

            appendSucceeded = pixelBufferAdaptor.appendPixelBuffer(pixelBuffer, withPresentationTime: presentationTime)

          } else {
            NSLog("error: Failed to allocate pixel buffer from pool")
          }
       }
    }

    return appendSucceeded
  }


  func fillPixelBufferFromImage(image: CGImage, pixelBuffer: CVPixelBuffer){
    let frameSize = CGSizeMake(CGFloat(CGImageGetWidth(image)), CGFloat(CGImageGetHeight(image)))
    CVPixelBufferLockBaseAddress(pixelBuffer, 0)
    let data = CVPixelBufferGetBaseAddress(pixelBuffer)
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let context = CGBitmapContextCreate(data, Int(frameSize.width), Int(frameSize.height), 8, CVPixelBufferGetBytesPerRow(pixelBuffer), rgbColorSpace, CGImageAlphaInfo.PremultipliedFirst.rawValue)
    CGContextDrawImage(context, CGRectMake(0, 0, CGFloat(CGImageGetWidth(image)), CGFloat(CGImageGetHeight(image))), image)
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)
  }

此处的工作项目文件: https://github.com/justinlevi/imagesToVideo