【问题标题】:All black frames when trying to write Metal frames to Quicktime file with AVFoundation AVAssetWriter尝试使用 AVFoundation AVAssetWriter 将 Metal 帧写入 Quicktime 文件时出现所有黑帧
【发布时间】:2018-11-11 00:48:13
【问题描述】:

我正在使用这个 Swift 类(最初显示在这个问题的答案中: Capture Metal MTKView as Movie in realtime?) 尝试将我的 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: AVFileTypeAppleM4V)
        } catch {
            return nil
        }

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

        assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, 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: kCMTimeZero)

        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, 240)
        assetWriterPixelBufferInput.append(pixelBuffer, withPresentationTime: presentationTime)

        CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
    }
}

我没有看到任何错误,但生成的 Quicktime 文件中的帧都是黑色的。帧大小正确,我的像素格式正确(bgra8Unorm)。有谁知道为什么它可能不起作用?

我在呈现和提交当前可绘制对象之前调用 writeFrame 函数,如下所示:

        if let drawable = view.currentDrawable {

            if BigVideoWriter != nil && BigVideoWriter!.isRecording {
                commandBuffer.addCompletedHandler { commandBuffer in
                    BigVideoWriter?.writeFrame(forTexture: drawable.texture)
                }
            }

            commandBuffer.present(drawable)
            commandBuffer.commit()      
        }

我最初确实收到了一个错误,我的 MetalKitView 层是“framebufferOnly”。所以我在尝试录制之前将其设置为 false。这摆脱了错误,但框架都是黑色的。我也尝试在程序一开始就将其设置为 false,但我得到了相同的结果。

我也尝试使用“addCompletedHandler”而不是“addScheduledHandler”,但这给了我错误“[CAMetalLayerDrawable texture] 在已经呈现此drawable 后不应调用。取而代之的是nextDrawable。 ”。

感谢您的任何建议!


编辑:我在@Idogy 的帮助下解决了这个问题。测试显示原始版本适用于 iOS,但不适用于 Mac。他说,因为我有一个 NVIDIA GPU,所以帧缓冲区是私有的。所以我不得不在纹理上添加一个带有同步调用的 blitCommandEncoder,然后它开始工作。像这样:

   if let drawable = view.currentDrawable {

        if BigVideoWriter != nil && BigVideoWriter!.isRecording {
 #if ISMAC
            if let blitCommandEncoder = commandBuffer.makeBlitCommandEncoder() {
                blitCommandEncoder.synchronize(resource: drawable.texture)
                blitCommandEncoder.endEncoding()
            }
 #endif
            commandBuffer.addCompletedHandler { commandBuffer in
                BigVideoWriter?.writeFrame(forTexture: drawable.texture)
            }
        }

        commandBuffer.present(drawable)
        commandBuffer.commit()    
    }

【问题讨论】:

    标签: swift avfoundation metal


    【解决方案1】:

    我相信你写帧太早了——通过在渲染循环中调用writeFrame,你实际上是在它仍然为空的时候捕获可绘制对象(GPU 还没有渲染它) .

    请记住,在您调用 commmandBuffer.commit() 之前,GPU 甚至还没有开始渲染您的帧。在尝试抓取结果帧之前,您需要等待 GPU 完成渲染。这个顺序有点混乱,因为您在调用commit() 之前还要调用present(),但这不是运行时的实际操作顺序。 present 调用只是告诉 Metal 安排调用以在 GPU 完成渲染后将帧呈现到屏幕上。

    您应该在完成处理程序中调用writeFrame(使用commandBuffer.addCompletedHandler())。那应该解决这个问题。

    更新:虽然上面的答案是正确的,但它只是部分的。由于 OP 使用的是带有私有 VRAM 的离散 GPU,因此 CPU 无法看到渲染目标像素。该问题的解决方案是添加MTLBlitCommandEncoder,并使用synchronize() 方法确保将渲染像素从GPU 的VRAM 复制回RAM。

    【讨论】:

    • 哦,对了。那是愚蠢的。但是,它并没有解决问题。我仍然得到黑框。我使用 addCompletedHandler() 和 addScheduledHandler() 进行了尝试,就像我提供的链接中的示例一样。没有骰子。还有其他想法吗?我更新了我的帖子以显示我现在拥有的内容。
    • 另外——当我使用'addCompletedHandler'时,它给了我这个错误:“[CAMetalLayerDrawable texture] 不应该在已经呈现这个drawable之后被调用。取而代之的是一个nextDrawable。使用'addScheduledHandler',就像示例代码,不会报错,但仍然会出现黑框。
    • 我认为您应该在提交命令缓冲区之前保留纹理。因此,您将在进入完成处理程序之前保存 view.currentDrawable.texture,并且您将从块中引用它。我相信这应该有效。为了清楚起见,您必须从完成处理程序(顺便说一下,不是从计划中)调用您的 writeFrames。即使您仍然收到黑框,请将其保留在完成处理程序中 - 这只是意味着您在其他地方还有另一个错误...
    • 嗯 - 抓住纹理确实可以防止错误消息,但我仍然只得到黑框。
    • 有趣。您是否检查过您的 pixelBufferBytes 以确保其中有实际值而不是全零?
    猜你喜欢
    • 1970-01-01
    • 2022-01-04
    • 2020-12-15
    • 2020-04-08
    • 1970-01-01
    • 2021-10-15
    • 1970-01-01
    • 1970-01-01
    • 2015-04-24
    相关资源
    最近更新 更多