从CMSampleBuffer中获取数据以创建深拷贝

25
我正在尝试创建一个CMSampleBuffer的副本,这是由AVCaptureVideoDataOutputSampleBufferDelegate中的captureOutput返回的。
由于CMSampleBuffers来自预分配的(15)个缓冲池,如果我附加了对它们的引用,它们就不能被重新收集。这将导致所有剩余的帧都被丢弃。
为保持最佳性能,一些样本缓冲区直接引用可能需要被设备系统和其他捕获输入重复使用的内存池。这在未压缩的设备本地捕获的情况下经常发生,其中尽可能少地复制内存块。如果多个样本缓冲区长时间引用这些内存池,输入将无法再将新样本复制到内存中,并且这些样本将被丢弃。
如果您的应用程序通过保留提供的CMSampleBufferRef对象太长时间导致样本被丢弃,但它需要长时间访问样本数据,请考虑将数据复制到新缓冲区,然后释放样本缓冲区(如果先前已保留),以便可以重复使用它引用的内存。
显然,我必须复制CMSampleBuffer,但是CMSampleBufferCreateCopy()只会创建浅层副本。因此,我得出结论,我必须使用CMSampleBufferCreate()。我填写了构造函数需要的12个参数,但遇到了问题,我的CMSampleBuffers不包含blockBuffer(不太确定它是什么,但它似乎很重要)。
这个问题已经被问过几次,但没有答案。 Deep Copy of CMImageBuffer or CVImageBufferCreate a copy of CMSampleBuffer in Swift 2.0

一种可能的答案是“我终于想出如何使用它来创建深克隆。所有的复制方法都重用了堆中的数据,这会锁定AVCaptureSession。所以我不得不将数据提取到NSMutableData对象中,然后创建一个新的样本缓冲区。” 感谢SO上的Rob。然而,我不知道如何正确地做这件事。

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


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

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

  • 在memcpy指令上发生EXC_BAD_ACCESS。也就是说,尝试访问应用程序内存外部而导致的段错误。
  • 或者,内存将成功复制,但是CMSampleBufferCreateReadyWithImageBuffer()将以结果代码-12743失败,该代码“表示给定媒体的格式与给定格式说明不匹配。例如,与CVImageBuffer配对的格式说明无法匹配CMVideoFormatDescriptionMatchesImageBuffer。”

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


1
我代表你留下了一条评论。链接 - jscs
@JoshCaswell 你是一位绅士和学者。 - bennyty
@bennyty 你如何进行音频样本的深度复制? - Neil Galiaskarov
@bennyty,你能否查看类似的问题。我已经为它开启了悬赏 - Neil Galiaskarov 15秒前 编辑 - Neil Galiaskarov
5个回答

17

好的,我想我终于明白了。我创建了一个辅助扩展程序,用于制作 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)

请注意,要知道,一个这样的像素缓冲区占用大约3MB的内存(1080p),这意味着在100帧中您已经使用了约300MB的内存,这大约是iPhone崩溃的点。

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


伙计,你太棒了。我使用了你的扩展并添加了一个AVAssetWriterInputPixelBufferAdaptor。一切都正常工作,我很高兴。这篇文章中的代码有可能被包含在一个已发布的开源应用程序中;如果你想在潜在的发表论文中获得认可或署名,请发送电子邮件至bennytytyty@gmail.com,我们可以安排细节。无论如何,你将在源代码中得到认可(如果你愿意,可以用真实姓名,给我发电子邮件)。 - bennyty
1
@bennyty 谢谢!不需要署名或其他什么,这很有趣 ;) - Kametrixom
@Kametrixom,这太棒了。感谢您的发布。您是否在将CVPixelBuffer复制后使用AVAssetWriterInput.appendSampleBuffer:(CMSampleBuffer)方法?我一直试图将CVPixelBuffer转换回CMSampleBuffer以使用资产编写器,但仍然会出现“无效参数不满足:sampleBuffer!= ((void*)0)”的错误。最终目标是创建与苹果相同类型的动态时间流逝视频,其中捕获的fps取决于时间流逝视频的长度。 - Dirk
谢谢,这太棒了!我对Swift还有些新,不知道那个前置条件是否必要。这个扩展只是将该方法添加到CVPixelBuffer类中,不是吗? - strikerdude10
我想知道“plane”代表什么。我将PixelBuffer视为具有行和列的矩阵,那么在这种情况下,“plane”实际上是什么,它是额外的数据还是只是命名不同行或列的快捷方式,我有点困惑。 - itMaxence
显示剩余2条评论

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

2

我花了好几个小时来尝试使它工作。结果发现原始CVPixelBuffer的附件和PixelBufferMetalCompatibilityKey中找到的IOSurface选项都是必需的。

(顺便说一下,VideoToolbox的PixelTransferSession仅适用于macOS。)

这是我最终得到的结果。我在最后留下了几行代码,可以通过比较原始和复制的CVPixelBuffer的平均颜色来验证memcpy。它会减慢速度,因此一旦您确信您的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
    }
}

在iOS 13上,如果我们将AVCaptureVideoDataOutput.videoSettings设置为[kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange]并在自拍相机获取的CMSampleBuffer上使用此方法,则图像将被损坏,即底部的一部分图像将被复制到顶部。非常奇怪。然而,原始的最佳方法仍然有效。 - utogaria
我一直在努力将像素缓冲区转换为数据,然后再转回像素缓冲区,但是当传递给didCaptureVideoFrame时没有显示任何内容。结果发现问题是因为获取像素缓冲区后IOSurface为空。经过一些非常艰苦的努力,你的代码解决了我的头痛问题。 - Jamil

0

0
花了一些时间来尝试整理这个。我需要一个函数,应该能够创建一个带有CVPixelBuffer的CMSampleBuffer深度副本,而不仅仅是像素缓冲区副本。
以下是我想出来的方法,在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
}

你好,能否提供一些方法来从 RTCVideoFrame 获取 CMSampleBuffer? - famfamfam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接