Захват металла MTKView как фильм в реальном времени?

Каков наиболее эффективный способ захвата кадров с 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()
 }

person Jeshua Lacock    schedule 08.05.2017    source источник
comment
Это, безусловно, возможно. Поскольку создание CVPixelBuffer из IOSurface не поддерживается на iOS, самый быстрый способ сделать это, вероятно, будет использовать AVAssetWriterInputPixelBufferAdaptor (который обертывает пул буферов пикселей), в который вы можете считывать содержимое вашей текстуры drawable каждый кадр . Есть несколько примеров того, как это сделать с помощью OpenGL ES. В конце концов я постараюсь опубликовать более полный ответ со всеми подробностями, но, возможно, это поможет вам. Другой вариант, который стоит рассмотреть, — это ReplayKit, если он достаточно гибкий, чтобы делать то, что вы хотите.   -  person warrenm    schedule 08.05.2017


Ответы (3)


Вот небольшой класс, который выполняет основные функции записи файла фильма, который захватывает содержимое представления 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, [])
    }
}

После инициализации одного из них и вызова startRecording() вы можете добавить запланированный обработчик в буфер команд, содержащий ваши команды рендеринга, и вызвать writeFrame (после завершения кодирования, но до представления рисуемого объекта или фиксации буфера):

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

Когда вы закончите запись, просто позвоните endRecording, и видеофайл будет завершен и закрыт.

Предупреждения:

Этот класс предполагает, что исходная текстура имеет формат по умолчанию, .bgra8Unorm. Если это не так, вы получите сбои или повреждение. При необходимости преобразуйте текстуру с помощью вычислительного или фрагментного шейдера или используйте Accelerate.

Этот класс также предполагает, что текстура имеет тот же размер, что и видеокадр. Если это не так (если изменяется размер рисунка или ваш экран автоматически поворачивается), вывод будет поврежден, и вы можете увидеть сбои. Уменьшите это, масштабируя или обрезая исходную текстуру в соответствии с требованиями вашего приложения.

person warrenm    schedule 09.05.2017
comment
вы потрясающие! Большое спасибо! Могу ли я пожертвовать немного денег, купив еще один экземпляр вашей книги Metal? - person Jeshua Lacock; 09.05.2017
comment
Рад помочь. Вы мне ничего не должны, но вы, безусловно, можете оплатить это другим способом, наставничеством, спонсорством или благотворительными пожертвованиями. . - person warrenm; 09.05.2017
comment
Почему вы используете «addScheduledHandler» вместо «addCompletedHandler»? - person bsabiston; 01.06.2018
comment
Я считаю, что использование запланированного обработчика неверно, так как блок будет выполняться ДО того, как графический процессор выполнит буфер команд. Вам нужно использовать обработчик завершения. - person ldoogy; 01.06.2018
comment
Следует также отметить, что для свойства MTKView framebufferOnly должно быть установлено значение false. Недавно я столкнулся с проблемами из-за этого и получил артефакты / сбои в видео. - person hartw; 17.12.2019
comment
Я получаю Thread 1: EXC_BAD_ACCESS в texture.getBytes(...). Любые идеи? Провел дни, пытаясь отладить. - person JCutting8; 13.06.2020
comment
Это может быть что угодно, от буфера неправильного размера до сообщения, отправленного освобожденному объекту. Я рекомендую задать новый вопрос, включая ваш исходный код. - person warrenm; 13.06.2020

Обновлен до 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, [])
    }
}
person Oscar    schedule 17.02.2020

Я использую этот пост для записи пользовательского вида металла, но у меня возникли некоторые проблемы. Когда я начинаю запись, я переключаюсь с 60 кадров в секунду на ~ 20 кадров в секунду на iPhone 12 Pro Max. После профилирования функция, которая все замедляет, называется texture.getBytes, так как она захватывает буфер из графического процессора в центральный процессор.

Другая проблема, не уверен, что это следствие, заключается в том, что видео и звук не синхронизированы. Я не уверен, должен ли я перейти к маршруту семафоров для решения этой проблемы или есть какой-либо другой потенциальный обходной путь.

В моем случае размер текстуры равен размеру экрана, так как я создаю ее из потока камеры, а затем обрабатываю парой CIFilters. Я не уверен, что проблема в том, что он слишком велик, поэтому getBytes не может поддерживать этот размер текстур в режиме реального времени.

Если мне нужно определить приоритеты, моим приоритетом №1 будет устранение рассинхронизации между аудио и видео. Любые мысли будут очень полезны.

person jmrueda    schedule 21.03.2021
comment
@warrenm, может быть, у тебя есть какие-то мысли по этому поводу? - person jmrueda; 21.03.2021
comment
Кажется, вы разместили вопрос как ответ; вам, вероятно, следует опубликовать свой собственный вопрос. - person Jeshua Lacock; 22.03.2021