0

I'm wondering if it's possible to achieve a better performance in converting the UIView into CVPixelBuffer.

My app converts a sequence of UIViews first into UIImages and then into CVPixelBuffers as shown below. In the end, I record all these images/frames into an AVAssetWriterInput and save the result as a movie file.

Thank you in advance!

Best, Aibek

func viewToImage(view: UIView) -> CGImage {
  let rect: CGRect = container.frame

  UIGraphicsBeginImageContextWithOptions(rect.size, true, 1)

  let context: CGContext = UIGraphicsGetCurrentContext()!
  view.layer.render(in: context)
  let img = UIGraphicsGetImageFromCurrentImageContext()

  UIGraphicsEndImageContext()

  return img!.cgImage
}
func imageToBuffer(image: CGImage) -> CVPixelBuffer? {
  let frameSize = CGSize(width: image.width, height: image.height)

  var pixelBuffer: CVPixelBuffer?
  let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(frameSize.width), Int(frameSize.height), kCVPixelFormatType_32BGRA, nil, &pixelBuffer)

  if status != kCVReturnSuccess {
    return nil
  }

  CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
  let data = CVPixelBufferGetBaseAddress(pixelBuffer!)
  let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
  let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
  let context = CGContext(data: data, width: Int(frameSize.width), height: Int(frameSize.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: bitmapInfo.rawValue)

  context?.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))

  CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))

  return pixelBuffer
}
aibek
  • 161
  • 8

2 Answers2

1

You'd better to see this. https://stackoverflow.com/a/61862728/13680955

In short, this sample converts UIView to MTLTexture in 12ms.

Sure you can use CVPixelBuffer directly, but I used MTLTexture to make video and no issue was on it.

If you are struggling with the performance, too slow or weird to use, try to do this.

With MTLTexture

import AVFoundation
import MetalKit

class VideoRecorder {
    let assetWriter: AVAssetWriter
    let assetWriterVideoInput: AVAssetWriterInput
    let assetWriterInputPixelBufferAdapter: AVAssetWriterInputPixelBufferAdaptor
    var recordingStartTime = TimeInterval(0)
    var recordingElapsedTime = TimeInterval(0)
    let url: URL = {
        let fileName = "exported_video.mp4"
        return FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
    }()

    init(outputSize: CGSize) throws {
        if FileManager.default.fileExists(atPath: url.path) {
            try FileManager.default.removeItem(at: url)
        }
        let fileType: AVFileType = .mov
        assetWriter = try AVAssetWriter(outputURL: url, fileType: fileType)
        let mediaType: AVMediaType = .video
        let outputSettings: [String: Any] = [
            AVVideoCodecKey: AVVideoCodecType.h264,
            AVVideoWidthKey: outputSize.width,
            AVVideoHeightKey: outputSize.height
        ]
        assetWriterVideoInput = AVAssetWriterInput(mediaType: mediaType, outputSettings: outputSettings)
        assetWriterVideoInput.expectsMediaDataInRealTime = false
        let sourcePixelBufferAttributes: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
            kCVPixelBufferWidthKey as String: outputSize.width,
            kCVPixelBufferHeightKey as String: outputSize.height
        ]
        assetWriterInputPixelBufferAdapter = AVAssetWriterInputPixelBufferAdaptor(
            assetWriterInput: assetWriterVideoInput, sourcePixelBufferAttributes: sourcePixelBufferAttributes)
        assetWriter.add(assetWriterVideoInput)
    }

    private static func currentTimestampString() -> String {
        let date = Date()
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return dateFormatter.string(from: date)
    }

    public func start() {
        print("videoRecorder.start")
        assetWriter.startWriting()
        assetWriter.startSession(atSourceTime: .zero)
        recordingStartTime = CACurrentMediaTime()
    }

    public func cancel() {
        #if DEBUG
        print("videoRecorder.cancel")
        #endif

        assetWriterVideoInput.markAsFinished()
        assetWriter.cancelWriting()
    }

    public func finish(_ callback: @escaping () -> Void) {
        print("videoRecorder.finish")
        assetWriterVideoInput.markAsFinished()
        assetWriter.finishWriting {
            self.recordingElapsedTime = CACurrentMediaTime() - self.recordingStartTime
            print("videoRecorder.finish elapsedTime: \(self.recordingElapsedTime)")
            callback()
        }
    }

    private var pixelBuffer: CVPixelBuffer?

    public func writeFrame(texture: MTLTexture, at presentationTime: CMTime) {
        print("videoRecorder.writeFrame: \(presentationTime)")
        if pixelBuffer == nil {
            guard let pixelBufferPool = assetWriterInputPixelBufferAdapter.pixelBufferPool else {
                print("Pixel buffer asset writer input did not have a pixel buffer pool available;")
                print("cannot retrieve frame")
                return
            }

            var maybePixelBuffer: CVPixelBuffer?
            let status  = CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &maybePixelBuffer)
            if status != kCVReturnSuccess {
                print("Could not get pixel buffer from asset writer input; dropping frame...")
                return
            }
            pixelBuffer = maybePixelBuffer
            print("videoRecorder.writeFrame: pixelBuffer was created: \(String(describing: pixelBuffer))")
        }

        guard let pixelBuffer = pixelBuffer else {
            print("videoRecorder.writeFrame: NO pixelBuffer")
            return
        }

        writeFrame(texture: texture, at: presentationTime, with: pixelBuffer)
    }

    private func writeFrame(texture: MTLTexture, at presentationTime: CMTime, with pixelBuffer: CVPixelBuffer) {
        while !assetWriterVideoInput.isReadyForMoreMediaData {
            //
            print("NOT ready for more media data at: \(presentationTime)")
        }
        CVPixelBufferLockBaseAddress(pixelBuffer, [])
        let pixelBufferBytes = CVPixelBufferGetBaseAddress(pixelBuffer)!

        // Use the bytes per row value from the pixel buffer since its stride may be rounded up to be 16-byte aligned
        let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
        let region = MTLRegionMake2D(0, 0, texture.width, texture.height)

        texture.getBytes(pixelBufferBytes, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0)

        assetWriterInputPixelBufferAdapter.append(pixelBuffer, withPresentationTime: presentationTime)

        CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
    }
}
마요Mayo
  • 86
  • 6
  • Hey @마요mayo! Thanks for replying to this. I like your second suggestion more about recording the `MTLTexture` into a video file. However, will the converting `UIView`s to `MTLTexture`s and recording (first suggestion) slow down the recording process? I'm also worried about re-writing the rendering stuff with Metal. Do you think it is worth that? How much performance increase can I expect from Metal? Best, Aibek – aibek Nov 21 '22 at 20:23
  • In simulator 13pro and real device 13pro, real time playing video, it didn't drop frame. Does your method performs very bad? – 마요Mayo Nov 22 '22 at 06:31
  • Rendering of UIViews with the approach above with video output 1080p @ 60fps and video duration of 2:30 takes around ~20-25 minutes. – aibek Nov 22 '22 at 12:00
  • oh.. then you can use this method. 1080p, 3:00, iPhone 13pro, it takes 3:00. Sure, I didn't used https://stackoverflow.com/a/61862728/13680955 this. But, with Metal, it doesn't take that long. – 마요Mayo Nov 23 '22 at 01:17
  • Hi Mayo! Just wanted to share that I've checked your approach. From my perspective, I didn't see any performance increase from converting the UIView to MTLTexture and recording it with your recorder. But I started researching how Metal works in hope that I can re-create all the animations in Metal and use Metal + Recorder to export videos. So far I could make some basic animations and tried to record textures as video frames. Looks like it performs fast enough, considering I write frames in realtime. Thanks for helping me out! – aibek Jan 30 '23 at 12:39
1

Converting the UIViews into MTLTextures and recording them into a video file using the Recorder provided by Mayo didn't increase the performance actually.

However, the recorder is able to write MTLTextures in real-time. That meant for me that I can re-write all the animations using Metal and use the recorder.

aibek
  • 161
  • 8
  • Long time no see. What do you mean that `re-write all the animations`? – 마요Mayo Feb 03 '23 at 02:52
  • True, it's been a while! I mean, I have to adopt all the processing stuff with Metal. Currently, I use `UIView`s to draw animated stuff into the `CALayer`. This differs from how Metal works. So, I need to re-write all the processing code to create primitives of all my elements and write shaders functions to properly display those primitives. – aibek Feb 03 '23 at 10:38
  • 1
    Good for you anyway that you figure out what you have to do :) – 마요Mayo Feb 17 '23 at 04:40
  • 1
    Good luck for all your codes – 마요Mayo Feb 17 '23 at 04:40