代码之家  ›  专栏  ›  技术社区  ›  bsabiston

尝试使用AVFoundation AVAssetWriter将金属帧写入Quicktime文件时出现的所有黑色帧

  •  4
  • bsabiston  · 技术社区  · 7 年前

    我正在使用这个Swift类(最初显示在这个问题的答案中: Capture Metal MTKView as Movie in realtime? )尝试将我的金属应用程序框架录制到电影文件中。

    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)。有人知道为什么它可能不起作用吗?

    我在呈现并提交当前可绘制文件之前调用WriteName函数,如下所示:

            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之后被调用。取而代之的是下一个drawable。 ".

    谢谢你的建议!


    编辑:我在@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()    
        }
    
    1 回复  |  直到 7 年前
        1
  •  4
  •   ldoogy    7 年前

    我认为你写框架太早了——打电话 writeFrame 在渲染循环中,基本上是在可绘制图形仍然为空(GPU还没有渲染它)的时候捕获它。

    在你打电话之前记住这一点 commmandBuffer.commit() GPU甚至没有 开始 渲染帧。您需要等待GPU完成渲染,然后再尝试获取生成的帧。顺序有点混乱,因为你也在打电话 present() 打电话之前 commit() ,但这不是运行时的实际操作顺序。那个 present call只是告诉Metal安排一次通话,将你的画面呈现在屏幕上 一旦GPU完成渲染 .

    你应该打电话 WriteName 在完成处理程序中(使用 commandBuffer.addCompletedHandler() ).这应该会解决的。

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