How can I apply filter for each frame of a video in AVCaptureSession?

1k views Asked by At

I am writing an app which needs to apply filter to a video captured using AVCaptureSession. The filtered output is written to an output file. I am current using CIFilter and CIImage for filter each video frame. Here is the code:

func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
    ...
    let pixelBuffer = CMSampleBufferGetImageBuffer(samples)!
    let options = [kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]
    let cameraImage = CIImage(cvImageBuffer: pixelBuffer, options: options)
    let filter = CIFilter(name: "CIGaussianBlur")!
    filter.setValue((70.0), forKey: kCIInputRadiusKey)
    filter.setValue(cameraImage, forKey: kCIInputImageKey)
    let result = filter.outputImage!
    var pixBuffer:CVPixelBuffer? = nil;
    let fmt = CVPixelBufferGetPixelFormatType(pixelBuffer)
    CVPixelBufferCreate(kCFAllocatorSystemDefault,
                        CVPixelBufferGetWidth(pixelBuffer),
                        CVPixelBufferGetHeight(pixelBuffer),
                        fmt,
                        CVBufferGetAttachments(pixelBuffer, .shouldPropagate),
                        &pixBuffer);

    CVBufferPropagateAttachments(pixelBuffer, pixBuffer!)
    let eaglContext = EAGLContext(api: EAGLRenderingAPI.openGLES3)!
    eaglContext.isMultiThreaded = true
    let contextOptions = [kCIContextWorkingColorSpace : NSNull(), kCIContextOutputColorSpace: NSNull()]
    let context = CIContext(eaglContext: eaglContext, options: contextOptions)
    CVPixelBufferLockBaseAddress( pixBuffer!, CVPixelBufferLockFlags(rawValue: 0))
    context.render(result, to: pixBuffer!)
    CVPixelBufferUnlockBaseAddress( pixBuffer!, CVPixelBufferLockFlags(rawValue: 0))
    var timeInfo = CMSampleTimingInfo(duration: sampleBuffer.duration,
                                      presentationTimeStamp: sampleBuffer.presentationTimeStamp,
                                      decodeTimeStamp: sampleBuffer.decodeTimeStamp)
    var sampleBuf:CMSampleBuffer? = nil;
    CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault,
                                             pixBuffer!,
                                             samples.formatDescription!,
                                             &timeInfo,
                                             &sampleBuf)

    // write to video file
    let ret = assetWriterInput.append(sampleBuf!)
    ...
}

The ret from the AVAssetWriterInput.append is always false. What am I doing wrong here? Also, the approach I am using is very inefficient. A few temp copies are created along the way. Is it possible to it in-place?

1

There are 1 answers

0
Varrry On

I used almost the same code with the the same problem. As I found out there was something wrong with pixel buffer created for rendering. append(sampleBuffer:) was always returning false and assetWriter.error was

Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSUnderlyingError=0x17024ba30 {Error Domain=NSOSStatusErrorDomain Code=-12780 "(null)"}, NSLocalizedFailureReason=An unknown error occurred (-12780), NSLocalizedDescription=The operation could not be completed}

They say this is a bug (as described here), already posted: https://bugreport.apple.com/web/?problemID=34574848.

But unexpectedly I found that problem goes away when using original pixel buffer for rendering. See code below:

let sourcePixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)!
let sourceImage = CIImage(cvImageBuffer: sourcePixelBuffer)
let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: [kCIInputImageKey: sourceImage])!
let filteredImage = filter.outputImage!

var pixelBuffer: CVPixelBuffer? = nil
let width = CVPixelBufferGetWidth(sourcePixelBuffer)
let height = CVPixelBufferGetHeight(sourcePixelBuffer)
let pixelFormat = CVPixelBufferGetPixelFormatType(sourcePixelBuffer)
let attributes = CVBufferGetAttachments(sourcePixelBuffer, .shouldPropagate)!
CVPixelBufferCreate(nil, width, height, pixelFormat, attributes, &pixelBuffer)
CVBufferPropagateAttachments(sourcePixelBuffer, pixelBuffer!)

var filteredPixelBuffer = pixelBuffer!      // this never works
filteredPixelBuffer = sourcePixelBuffer     // 0_0

let context = CIContext(options: [kCIContextOutputColorSpace: CGColorSpace(name: CGColorSpace.sRGB)!])
context.render(filteredImage, to: filteredPixelBuffer)  // modifying original image buffer here!

let presentationTimestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
var timing = CMSampleTimingInfo(duration: kCMTimeInvalid, presentationTimeStamp: presentationTimestamp, decodeTimeStamp: kCMTimeInvalid)

var processedSampleBuffer: CMSampleBuffer? = nil
var formatDescription: CMFormatDescription? = nil
CMVideoFormatDescriptionCreateForImageBuffer(nil, filteredPixelBuffer, &formatDescription)
CMSampleBufferCreateReadyWithImageBuffer(nil, filteredPixelBuffer, formatDescription!, &timing, &processedSampleBuffer)

print(assetInput!.append(processedSampleBuffer!))

Sure, we all know you are not allowed to modify sample buffer, but somehow this approach gives normal processed video. Trick is dirty and I can't say if it will be fine in cases when you have preview layer or some concurrent processing routines.