Извлечение данных из CMSampleBuffer для создания глубокой копии

Я пытаюсь создать копию CMSampleBuffer, которая возвращается captureOutput в AVCaptureVideoDataOutputSampleBufferDelegate.

Поскольку CMSampleBuffers поступают из предварительно выделенного пула (15) буферов, если я добавлю ссылку на них, они не могут быть восстановлены. Это приводит к удалению всех оставшихся кадров.

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

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

Очевидно, что я должен скопировать CMSampleBuffer, но CMSampleBufferCreateCopy() создаст только поверхностную копию. Таким образом, я пришел к выводу, что я должен использовать CMSampleBufferCreate(). Я заполнил 12! параметры, которые нужны конструктору, но столкнулись с проблемой, что мои CMSampleBuffers не содержат blockBuffer (не совсем уверен, что это такое, но это кажется важным).

Этот вопрос задавался несколько раз, но не получил ответа.

Глубокое копирование CMImageBuffer или CVImageBuffer и создание копии CMSampleBuffer в Swift 2.0

Один из возможных ответов: "Я наконец-то понял, как использовать это для создания глубокого клона. Все методы копирования повторно использовали данные в куче, которая хранилась, блокировали AVCaptureSession. Поэтому мне пришлось извлечь данные в объект NSMutableData, а затем создал новый образец буфера. " кредит Робу на ТАК. Однако я не знаю, как сделать это правильно.

Если вы заинтересованы, это вывод print(sampleBuffer), Нет никакого упоминания о blockBuffer, иначе CMSampleBufferGetDataBuffer возвращает nil. Существует imageBuffer, но создание "копии" с использованием CMSampleBufferCreateForImageBuffer, похоже, также не освобождает CMSampleBuffer.


РЕДАКТИРОВАТЬ: так как этот вопрос был опубликован, я пытался еще больше способов копирования памяти.

Я сделал то же самое, что попробовал пользователь Kametrixom. Это моя попытка использовать ту же идею: сначала скопировать CVPixelBuffer, а затем использовать CMSampleBufferCreateForImageBuffer для создания буфера окончательного примера. Однако это приводит к одной из двух ошибок:

  • EXC_BAD_ACCESS в инструкции memcpy. AKA - это ошибка от попыток получить доступ за пределами памяти приложения.
  • Или память будет успешно скопирована, но CMSampleBufferCreateReadyWithImageBuffer() произойдет сбой с кодом результата -12743, который "Указывает, что формат данного носителя не соответствует заданному описанию формата. Например, описание формата в сочетании с CVImageBuffer, который завершается ошибкой CMVideoFormatDescriptionMatchesImageBuffer."

Вы можете видеть, что и Kametrixom, и я использовали CMSampleBufferGetFormatDescription(sampleBuffer) попытаться скопировать описание формата исходного буфера. Таким образом, я не уверен, почему формат данного носителя не соответствует данному описанию формата.

5 ответов

Решение

Хорошо, я думаю, что наконец-то понял. Я создал вспомогательное расширение, чтобы сделать полную копию CVPixelBuffer:

extension CVPixelBuffer {
    func copy() -> CVPixelBuffer {
        precondition(CFGetTypeID(self) == CVPixelBufferGetTypeID(), "copy() cannot be called on a non-CVPixelBuffer")

        var _copy : CVPixelBuffer?
        CVPixelBufferCreate(
            nil,
            CVPixelBufferGetWidth(self),
            CVPixelBufferGetHeight(self),
            CVPixelBufferGetPixelFormatType(self),
            CVBufferGetAttachments(self, kCVAttachmentMode_ShouldPropagate)?.takeUnretainedValue(),
            &_copy)

        guard let copy = _copy else { fatalError() }

        CVPixelBufferLockBaseAddress(self, kCVPixelBufferLock_ReadOnly)
        CVPixelBufferLockBaseAddress(copy, 0)

        for plane in 0..<CVPixelBufferGetPlaneCount(self) {
            let dest = CVPixelBufferGetBaseAddressOfPlane(copy, plane)
            let source = CVPixelBufferGetBaseAddressOfPlane(self, plane)
            let height = CVPixelBufferGetHeightOfPlane(self, plane)
            let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(self, plane)

            memcpy(dest, source, height * bytesPerRow)
        }

        CVPixelBufferUnlockBaseAddress(copy, 0)
        CVPixelBufferUnlockBaseAddress(self, kCVPixelBufferLock_ReadOnly)

        return copy
    }
}

Теперь вы можете использовать это в своем didOutputSampleBuffer метод:

guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }

let copy = pixelBuffer.copy()

toProcess.append(copy)

Но имейте в виду, что один из таких pixelBuffer занимает около 3 МБ памяти (1080p), что означает, что в 100 кадрах вы получили уже около 300 МБ, что соответствует точке, в которой iPhone говорит STAHP (и вылетает).

Обратите внимание, что вы на самом деле не хотите копировать CMSampleBuffer так как он действительно содержит только CVPixelBuffer потому что это изображение.

Это Swift 3 решение с самым высоким рейтингом ответа.

extension CVPixelBuffer {
func copy() -> CVPixelBuffer {
    precondition(CFGetTypeID(self) == CVPixelBufferGetTypeID(), "copy() cannot be called on a non-CVPixelBuffer")

    var _copy : CVPixelBuffer?
    CVPixelBufferCreate(
        kCFAllocatorDefault,
        CVPixelBufferGetWidth(self),
        CVPixelBufferGetHeight(self),
        CVPixelBufferGetPixelFormatType(self),
        nil,
        &_copy)

    guard let copy = _copy else { fatalError() }

    CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly)
    CVPixelBufferLockBaseAddress(copy, CVPixelBufferLockFlags(rawValue: 0))


    let copyBaseAddress = CVPixelBufferGetBaseAddress(copy)
    let currBaseAddress = CVPixelBufferGetBaseAddress(self)

    memcpy(copyBaseAddress, currBaseAddress, CVPixelBufferGetDataSize(self))

    CVPixelBufferUnlockBaseAddress(copy, CVPixelBufferLockFlags(rawValue: 0))
    CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly)


    return copy
}
}

Я провел пару часов, пытаясь заставить это работать. Оказывается, что необходимы как вложения из исходного CVPixelBuffer, так и параметры IOSurface, найденные в PixelBufferMetalCompatibilityKey.

(К сожалению, PixelTransferSession VideoToolbox предназначен только для MacOS, к сожалению.)

Вот чем я закончил. В конце я оставил несколько строк, которые позволяют вам проверить memcpy, сравнивая средние цвета как исходного, так и скопированного CVPixelBuffers. Он замедляет работу, поэтому его следует удалить, если вы уверены, что функция copy() работает так, как ожидалось. Расширение CIImage.averageColour адаптировано из этого кода.

extension CVPixelBuffer {
    func copy() -> CVPixelBuffer {
        precondition(CFGetTypeID(self) == CVPixelBufferGetTypeID(), "copy() cannot be called on a non-CVPixelBuffer")

        let ioSurfaceProps = [
            "IOSurfaceOpenGLESFBOCompatibility": true as CFBoolean,
            "IOSurfaceOpenGLESTextureCompatibility": true as CFBoolean,
            "IOSurfaceCoreAnimationCompatibility": true as CFBoolean
        ] as CFDictionary

        let options = [
            String(kCVPixelBufferMetalCompatibilityKey): true as CFBoolean,
            String(kCVPixelBufferIOSurfacePropertiesKey): ioSurfaceProps
        ] as CFDictionary

        var _copy : CVPixelBuffer?
        CVPixelBufferCreate(
            nil,
            CVPixelBufferGetWidth(self),
            CVPixelBufferGetHeight(self),
            CVPixelBufferGetPixelFormatType(self),
            options,
            &_copy)

        guard let copy = _copy else { fatalError() }

        CVBufferPropagateAttachments(self as CVBuffer, copy as CVBuffer)

        CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags.readOnly)
        CVPixelBufferLockBaseAddress(copy, CVPixelBufferLockFlags(rawValue: 0))

        let copyBaseAddress = CVPixelBufferGetBaseAddress(copy)
        let currBaseAddress = CVPixelBufferGetBaseAddress(self)

        memcpy(copyBaseAddress, currBaseAddress, CVPixelBufferGetDataSize(self))

        CVPixelBufferUnlockBaseAddress(copy, CVPixelBufferLockFlags(rawValue: 0))
        CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags.readOnly)

        // let's make sure they have the same average color
//        let originalImage = CIImage(cvPixelBuffer: self)
//        let copiedImage = CIImage(cvPixelBuffer: copy)
//
//        let averageColorOriginal = originalImage.averageColour()
//        let averageColorCopy = copiedImage.averageColour()
//
//        assert(averageColorCopy == averageColorOriginal)
//        debugPrint("average frame color: \(averageColorCopy)")

        return copy
    }
}

Потратил некоторое время, пытаясь собрать это вместе. Мне нужна была функция, которая могла бы создать глубокую копию CMSampleBuffer с CVPixelBuffer внутри, а не только копию пиксельного буфера.

Вот что я придумал, и это работает для меня в iOS 15, Swift 5:

      extension CVPixelBuffer {
func copy() -> CVPixelBuffer {
    precondition(CFGetTypeID(self) == CVPixelBufferGetTypeID(), "copy() cannot be called on a non-CVPixelBuffer")

    var _copy : CVPixelBuffer?
    CVPixelBufferCreate(
            nil,
            CVPixelBufferGetWidth(self),
            CVPixelBufferGetHeight(self),
            CVPixelBufferGetPixelFormatType(self),
            nil,
            &_copy)
    guard let copy = _copy else { fatalError() }

    CVBufferPropagateAttachments(self, copy)


    CVPixelBufferLockBaseAddress(self, .readOnly)
    CVPixelBufferLockBaseAddress(copy, CVPixelBufferLockFlags())

    for plane in 0 ..< CVPixelBufferGetPlaneCount(self) {
        let dest = CVPixelBufferGetBaseAddressOfPlane(copy, plane)
        let source = CVPixelBufferGetBaseAddressOfPlane(self, plane)
        let height = CVPixelBufferGetHeightOfPlane(self, plane)
        let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(self, plane)

        memcpy(dest, source, height * bytesPerRow)
    }

    CVPixelBufferUnlockBaseAddress(copy, CVPixelBufferLockFlags())
    CVPixelBufferUnlockBaseAddress(self, .readOnly)

    return copy
  }
}

Расширение буфера проб

      extension CMSampleBuffer {
func copy() -> CMSampleBuffer {
    guard let pixelBuffer = CMSampleBufferGetImageBuffer(self) else { fatalError("CMSampleBuffer copy: get image buffer")  }

    let copiedPixelBuffer = pixelBuffer.copy()
    let format = CMSampleBufferGetFormatDescription(self)!
    var timing = CMSampleTimingInfo()
    CMSampleBufferGetSampleTimingInfo(self, at: 0, timingInfoOut: &timing)

    var copiedSampleBuffer : CMSampleBuffer?

    let status = CMSampleBufferCreateReadyWithImageBuffer(allocator: nil,
            imageBuffer: copiedPixelBuffer,
            formatDescription: format,
            sampleTiming: &timing,
            sampleBufferOut: &copiedSampleBuffer)
    guard let bufOut = copiedSampleBuffer else { fatalError("CMSampleBuffer copy: CreateReady \(status)") }
    return bufOut
  }
}

И призыв к копированию:

      func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    let bufferCopy = sampleBuffer.copy()
    // free to use bufferCopy as it won't stop video recording
    // don't forget to put it into autoreleasepool { } if used in non-main thread
}

Я считаю, что с VideoToolbox.framework, вы можете использовать VTPixelTransferSession копировать пиксельные буферы. Фактически, это единственное, что делает этот класс.

Ссылка: https://developer.apple.com/documentation/videotoolbox/vtpixeltransfersession-7cg

Другие вопросы по тегам