从 CMSampleBuffer 中提取数据以创建深层副本

Posted

技术标签:

【中文标题】从 CMSampleBuffer 中提取数据以创建深层副本【英文标题】:Pulling data from a CMSampleBuffer in order to create a deep copy 【发布时间】:2016-11-15 01:52:49 【问题描述】:

我正在尝试创建一个由 AVCaptureVideoDataOutputSampleBufferDelegate 中的 captureOutput 返回的 CMSampleBuffer 的副本。

由于 CMSampleBuffers 来自 (15) 个缓冲区的预分配池,如果我附加对它们的引用,它们将无法被重新收集。这会导致所有剩余的帧都被丢弃。

为保持最佳性能,一些样本缓冲区直接引用可能需要由设备系统和其他捕获输入重用的内存池。对于未压缩的设备本机捕获,这种情况经常发生,其中内存块被复制得越少越好。如果多个样本缓冲区引用此类内存池的时间过长,输入将不再能够将新样本复制到内存中,并且这些样本将被丢弃。

如果您的应用程序由于保留提供的 CMSampleBufferRef 对象太久而导致样本被丢弃,但它需要长时间访问样本数据,请考虑将数据复制到新缓冲区中,然后释放样本缓冲区(如果它以前被保留),以便它引用的内存可以被重用。

显然我必须复制 CMSampleBuffer 但 CMSampleBufferCreateCopy() 只会创建一个浅拷贝。因此我得出结论,我必须使用 CMSampleBufferCreate()。我填了12!构造函数需要的参数,但遇到了我的 CMSampleBuffers 不包含 blockBuffer 的问题(不完全确定那是什么,但它似乎很重要)。

这个问题已经被问了好几次,但没有回答。

Deep Copy of CMImageBuffer or CVImageBuffer 和 Create a copy of CMSampleBuffer in Swift 2.0

一个可能的答案是“我终于想出了如何使用它来创建深度克隆。所有的复制方法都重用了堆中的数据,这些数据会锁定 AVCaptureSession。所以我不得不将数据提取到 NSMutableData对象,然后创建一个新的样本缓冲区。” credit to Rob on SO。但是,我不知道如何正确执行此操作。

如果你有兴趣,this 是print(sampleBuffer) 的输出。没有提到 blockBuffer,也就是 CMSampleBufferGetDataBuffer 返回 nil。有一个 imageBuffer,但是使用 CMSampleBufferCreateForImageBuffer 创建一个“副本”似乎也不能释放 CMSampleBuffer。


编辑:自从发布了这个问题以来,我一直在尝试更多复制内存的方法。

我做了用户Kametrixom 尝试过的同样的事情。 This 是我尝试相同的想法,首先复制 CVPixelBuffer 然后使用 CMSampleBufferCreateForImageBuffer 创建最终的样本缓冲区。但是,这会导致以下两个错误之一:

memcpy 指令上的 EXC_BAD_ACCESS。 AKA 尝试访问应用程序内存之外的段错误。 或者,内存将成功复制,但CMSampleBufferCreateReadyWithImageBuffer() 将失败,结果代码为 -12743,“表示给定媒体的格式与给定的格式描述不匹配。例如,与 CVImageBuffer 配对的格式描述CMVideoFormatDescriptionMatchesImageBuffer 失败。”

您可以看到,Kametrixom 和我都使用CMSampleBufferGetFormatDescription(sampleBuffer) 来尝试复制源缓冲区的格式描述。因此,我不确定为什么给定媒体的格式与给定的格式描述不匹配。

【问题讨论】:

我代表你离开了a comment。 @JoshCaswell 你是一个绅士和一个学者。 @bennyty 你将如何深度复制音频样本? @bennyty 你能检查类似的question。我已经为它打开了赏金 - Neil Galiaskarov 15 秒前编辑 【参考方案1】:

好吧,我想我终于明白了。我创建了一个辅助扩展来制作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 占用大约 3MB 的内存 (1080p),这意味着在 100 帧中您已经获得了大约 300MB,这大约是 iPhone 显示 STAHP(并崩溃)的时间点。

请注意,您实际上并不想复制CMSampleBuffer,因为它实际上只包含CVPixelBuffer,因为它是一个图像。

【讨论】:

伴侣。你太棒了。我使用了你的扩展并添加了一个 AVAssetWriterInputPixelBufferAdaptor。一切正常,我很高兴。本文中的代码有可能包含在已发布的开源应用程序中;如果您想在可能发表的论文中获得致谢或署名,请给我发电子邮件至 bennytytyty@gmail.com,我们可以安排详细信息。无论哪种方式,您都会在源代码中得到认可(如果您愿意,请使用真实姓名,给我发电子邮件。) @bennyty 谢谢!不需要归因或任何东西,这样做很有趣;) @Kametrixom 这太棒了。感谢您发布。复制 CVPixelBuffer 后是否使用 AVAssetWriterInput.appendSampleBuffer:(CMSampleBuffer) 方法?我一直在尝试将 CVPixelBuffer 转回 CMSampleBuffer 以使用资产编写器,但不断收到“无效参数不令人满意:sampleBuffer != ((void*)0)'”。最终目标是实现与苹果相同的动态延时摄影,其中捕获的 fps 取决于延时摄影视频的长度。 谢谢,这太棒了!我对 Swift 有点陌生,想知道这个前提条件是否必要。扩展不只是将这个方法添加到CVPixelBuffer类吗? @Kametrixom 你能检查类似的question。我已经为它开了赏金【参考方案2】:

这是评分最高的答案的 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


【讨论】:

这在 ios 14 中不再起作用。(EXC_BAD_ACCESS)【参考方案3】:

我花了好几个小时试图让它发挥作用。原来 CVPixelBuffer 的附件和 PixelBufferMetalCompatibilityKey 中的 IOSurface 选项都是必需的。

(仅供参考,遗憾的是,VideoToolbox 的 PixelTransferSession 仅适用于 macOS。)

这就是我最终的结果。我在最后留下了几行,允许您通过比较原始和复制的 CVPixelBuffers 的平均颜色来验证 memcpy。它确实会减慢速度,因此一旦您确信您的 copy() 按预期工作,就应该将其删除。 CIImage.averageColour 扩展名改编自this code。

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
    

【讨论】:

在iOS 13上,如果我们将AVCaptureVideoDataOutput.videoSettings设置为[kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],并在自拍相机获取的CMSampleBuffer上使用这种方式,图片会损坏,即图片的一部分底部的将被复制到顶部。很奇怪。然而,最初的***方法是有效的。【参考方案4】:

我相信通过 VideoToolbox.framework,您可以使用VTPixelTransferSession 来复制像素缓冲区。事实上,这是这个类唯一要做的事情。

参考: https://developer.apple.com/documentation/videotoolbox/vtpixeltransfersession-7cg

【讨论】:

以上是关于从 CMSampleBuffer 中提取数据以创建深层副本的主要内容,如果未能解决你的问题,请参考以下文章

从CMSampleBuffer获取曝光时间(EXIF)

将 AudioBufferList 转换为 CMSampleBuffer 会产生意外结果

从 CMSampleBuffer 转换为 UIImage 对象

使用 CMSampleBuffer 生成 Float32 数组(Float32 PCM 数据)

从 JSON 数组中提取数据以创建 3 个不同的数组

如何从表中提取信息以填充已创建数据仓库中的 CALENDAR 表