将 Metal MTKView 实时捕获为电影?

Posted

技术标签:

【中文标题】将 Metal MTKView 实时捕获为电影?【英文标题】:Capture Metal MTKView as Movie in realtime? 【发布时间】:2017-10-05 21:24:22 【问题描述】:

MTKView 捕获帧的最有效方法是什么?如果可能的话,我想实时保存帧中的 .mov 文件。是否可以渲染成 AVPlayer 框架之类的?

目前正在使用此代码进行绘制(基于@warrenm PerformanceShaders project):

func draw(in view: MTKView) 
     _ = inflightSemaphore.wait(timeout: DispatchTime.distantFuture)
             updateBuffers()

    let commandBuffer = commandQueue.makeCommandBuffer()

    commandBuffer.addCompletedHandler [weak self] commandBuffer in
        if let strongSelf = self 
            strongSelf.inflightSemaphore.signal()
        
    
   // Dispatch the current kernel to perform the selected image filter
    selectedKernel.encode(commandBuffer: commandBuffer,
        sourceTexture: kernelSourceTexture!,
        destinationTexture: kernelDestTexture!)

    if let renderPassDescriptor = view.currentRenderPassDescriptor, let currentDrawable = view.currentDrawable
    
        let clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
        renderPassDescriptor.colorAttachments[0].clearColor = clearColor

        let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
        renderEncoder.label = "Main pass"

        renderEncoder.pushDebugGroup("Draw textured square")
        renderEncoder.setFrontFacing(.counterClockwise)
        renderEncoder.setCullMode(.back)

        renderEncoder.setRenderPipelineState(pipelineState)
        renderEncoder.setVertexBuffer(vertexBuffer, offset: MBEVertexDataSize * bufferIndex, at: 0)
        renderEncoder.setVertexBuffer(uniformBuffer, offset: MBEUniformDataSize * bufferIndex , at: 1)
        renderEncoder.setFragmentTexture(kernelDestTexture, at: 0)
        renderEncoder.setFragmentSamplerState(sampler, at: 0)
        renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)

        renderEncoder.popDebugGroup()
        renderEncoder.endEncoding()

        commandBuffer.present(currentDrawable)
    

    bufferIndex = (bufferIndex + 1) % MBEMaxInflightBuffers

    commandBuffer.commit()
 

【问题讨论】:

这当然是可能的。由于在 ios 上从 IOSurface 创建 CVPixelBuffer 不是受支持的操作,因此最快的方法可能是使用 AVAssetWriterInputPixelBufferAdaptor(包装像素缓冲池),您可以在其中读取内容每一帧的drawable的纹理。有一些示例说明如何使用浮动的 OpenGL ES 执行此操作。我最终会尝试发布一个包含所有细节的更完整的答案,但也许这会对你有所帮助。另一个要考虑的选择是 ReplayKit,如果它足够灵活,可以做你想做的事。 【参考方案1】:

这是一个小类,它执行编写捕获 Metal 视图内容的电影文件的基本功能:

class MetalVideoRecorder 
    var isRecording = false
    var recordingStartTime = TimeInterval(0)

    private var assetWriter: AVAssetWriter
    private var assetWriterVideoInput: AVAssetWriterInput
    private var assetWriterPixelBufferInput: AVAssetWriterInputPixelBufferAdaptor

    init?(outputURL url: URL, size: CGSize) 
        do 
            assetWriter = try AVAssetWriter(outputURL: url, fileType: .m4v)
         catch 
            return nil
        

        let outputSettings: [String: Any] = [ AVVideoCodecKey : AVVideoCodecType.h264,
            AVVideoWidthKey : size.width,
            AVVideoHeightKey : size.height ]

        assetWriterVideoInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
        assetWriterVideoInput.expectsMediaDataInRealTime = true

        let sourcePixelBufferAttributes: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA,
            kCVPixelBufferWidthKey as String : size.width,
            kCVPixelBufferHeightKey as String : size.height ]

        assetWriterPixelBufferInput = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterVideoInput,
                                                                           sourcePixelBufferAttributes: sourcePixelBufferAttributes)

        assetWriter.add(assetWriterVideoInput)
    

    func startRecording() 
        assetWriter.startWriting()
        assetWriter.startSession(atSourceTime: .zero)

        recordingStartTime = CACurrentMediaTime()
        isRecording = true
    

    func endRecording(_ completionHandler: @escaping () -> ()) 
        isRecording = false

        assetWriterVideoInput.markAsFinished()
        assetWriter.finishWriting(completionHandler: completionHandler)
    

    func writeFrame(forTexture texture: MTLTexture) 
        if !isRecording 
            return
        

        while !assetWriterVideoInput.isReadyForMoreMediaData 

        guard let pixelBufferPool = assetWriterPixelBufferInput.pixelBufferPool else 
            print("Pixel buffer asset writer input did not have a pixel buffer pool available; cannot retrieve frame")
            return
        

        var maybePixelBuffer: CVPixelBuffer? = nil
        let status  = CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &maybePixelBuffer)
        if status != kCVReturnSuccess 
            print("Could not get pixel buffer from asset writer input; dropping frame...")
            return
        

        guard let pixelBuffer = maybePixelBuffer else  return 

        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)

        let frameTime = CACurrentMediaTime() - recordingStartTime
        let presentationTime = CMTimeMakeWithSeconds(frameTime, preferredTimescale: 240)
        assetWriterPixelBufferInput.append(pixelBuffer, withPresentationTime: presentationTime)

        CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
    

在初始化其中一个并调用 startRecording() 后,您可以将调度的处理程序添加到包含渲染命令的命令缓冲区并调用 writeFrame(在结束编码之后,但在呈现可绘制对象或提交缓冲区之前):

let texture = currentDrawable.texture
commandBuffer.addCompletedHandler  commandBuffer in
    self.recorder.writeFrame(forTexture: texture)

录制完成后,只需拨打endRecording,视频文件将完成并关闭。

注意事项

此类假定源纹理为默认格式.bgra8Unorm。如果不是,您将遇到崩溃或损坏。如有必要,使用计算或片段着色器转换纹理,或使用 Accelerate。

该类还假设纹理与视频帧的大小相同。如果不是这种情况(如果可绘制大小发生变化,或者您的屏幕自动旋转),输出将被破坏并且您可能会看到崩溃。根据您的应用程序需要,通过缩放或裁剪源纹理来缓解此问题。

【讨论】:

你好沃伦。我有一个问题可能是因为pixelFormat。我在这里有一个问题:***.com/questions/46146593/… 和 github 上的一个问题:github.com/svtek/SceneKitVideoRecorder/issues/3 你为什么用'addScheduledHandler'而不是'addCompletedHandler'? 我认为使用调度处理程序是不正确的,因为该块将在 GPU 执行命令缓冲区之前运行。您需要使用完成处理程序。 还应该注意MTKView的属性framebufferOnly应该设置为false。由于这个原因,我最近遇到了问题,并且在视频中出现了伪影/崩溃 我在texture.getBytes(...) 收到Thread 1: EXC_BAD_ACCESS。有任何想法吗?花了几天时间尝试调试。【参考方案2】:

升级到 Swift 5


import AVFoundation

class MetalVideoRecorder 
    var isRecording = false
    var recordingStartTime = TimeInterval(0)

    private var assetWriter: AVAssetWriter
    private var assetWriterVideoInput: AVAssetWriterInput
    private var assetWriterPixelBufferInput: AVAssetWriterInputPixelBufferAdaptor

    init?(outputURL url: URL, size: CGSize) 
        do 
          assetWriter = try AVAssetWriter(outputURL: url, fileType: AVFileType.m4v)
         catch 
            return nil
        

      let outputSettings: [String: Any] = [ AVVideoCodecKey : AVVideoCodecType.h264,
            AVVideoWidthKey : size.width,
            AVVideoHeightKey : size.height ]

      assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: outputSettings)
        assetWriterVideoInput.expectsMediaDataInRealTime = true

        let sourcePixelBufferAttributes: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_32BGRA,
            kCVPixelBufferWidthKey as String : size.width,
            kCVPixelBufferHeightKey as String : size.height ]

        assetWriterPixelBufferInput = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterVideoInput,
                                                                           sourcePixelBufferAttributes: sourcePixelBufferAttributes)

        assetWriter.add(assetWriterVideoInput)
    

    func startRecording() 
        assetWriter.startWriting()
      assetWriter.startSession(atSourceTime: CMTime.zero)

        recordingStartTime = CACurrentMediaTime()
        isRecording = true
    

    func endRecording(_ completionHandler: @escaping () -> ()) 
        isRecording = false

        assetWriterVideoInput.markAsFinished()
        assetWriter.finishWriting(completionHandler: completionHandler)
    

    func writeFrame(forTexture texture: MTLTexture) 
        if !isRecording 
            return
        

        while !assetWriterVideoInput.isReadyForMoreMediaData 

        guard let pixelBufferPool = assetWriterPixelBufferInput.pixelBufferPool else 
            print("Pixel buffer asset writer input did not have a pixel buffer pool available; cannot retrieve frame")
            return
        

        var maybePixelBuffer: CVPixelBuffer? = nil
        let status  = CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &maybePixelBuffer)
        if status != kCVReturnSuccess 
            print("Could not get pixel buffer from asset writer input; dropping frame...")
            return
        

        guard let pixelBuffer = maybePixelBuffer else  return 

        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)

        let frameTime = CACurrentMediaTime() - recordingStartTime
        let presentationTime = CMTimeMakeWithSeconds(frameTime, preferredTimescale:   240)
        assetWriterPixelBufferInput.append(pixelBuffer, withPresentationTime: presentationTime)

        CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
    

【讨论】:

【参考方案3】:

我正在使用这篇文章来记录自定义金属视图,但我遇到了一些问题。当我开始录制时,我在 iPhone 12 Pro Max 上从 60fps 提高到 ~20fps。在 Profiling 之后,减慢一切的函数是 texture.getBytes,因为它正在将缓冲区从 GPU 抓取到 CPU。

另一个问题是视频和音频不同步,不确定是否会导致这种情况。我不确定我是否应该进入信号量路线来解决这个问题,或者还有其他潜在的解决方法。

在我的例子中,纹理大小与屏幕大小一样大,因为我从相机流创建它,然后通过几个 CIFilter 对其进行处理。我不确定问题是否在于它太大,所以 getBytes 无法实时支持这种大小的纹理。

如果我需要定义优先级,我的第一要务是解决音频和视频之间的不同步问题。任何想法都会非常有帮助。

【讨论】:

@warrenm 也许你对此有一些想法 您似乎发布了一个问题作为答案;您可能应该发布自己的问题。

以上是关于将 Metal MTKView 实时捕获为电影?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 macOS 上渲染半透明的 MTKView?

MTKView 绘图性能

如何将 Vignette CIFilter 应用于 iOS 中的实时摄像机源?

如何检测 MTKView 中鼠标按下事件发生的位置?

是否需要在主线程上绘制到 MTKView 或 CAMetalLayer?

MTKView 显示广色域 P3 色彩空间