LoginSignup
3
2

More than 3 years have passed since last update.

AVCaptureVideoDataOutputSampleBufferDelegateのcaptureOutputでフレームを配列で保持するとサンプルを取得できなくなる問題【対処法】

Last updated at Posted at 2020-08-18

13フレームで止まる問題

ビデオフレームを取得して処理できるデリゲートメソッド "captureOutput" の CMSampleBuffer を配列として保持すると、新しいフレームを取得できなくなります。
13フレーム取得すると、それより後のフレームは取れなくなります。
CVPixelBufferやUIImageに変換して配列に保持しても、同様に取得できなくなります。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
           buffers.append(sampleBuffer)
           //このように配列(buffers)に保持してしまうと、新しいフレームが取れなくなる
}

ドキュメントによると、

最適なパフォーマンスを維持するために、一部のサンプルバッファーは、デバイスシステムや他のキャプチャ入力で再利用する必要があるメモリのプールを直接参照します。これは、メモリブロックができるだけコピーされない非圧縮デバイスのネイティブキャプチャの場合によく見られます。複数のサンプルバッファーがそのようなメモリプールを長時間参照している場合、入力は新しいサンプルをメモリにコピーできなくなり、それらのサンプルは削除されます。

提供されたオブジェクトを長時間保持することでアプリケーションがサンプルをドロップする原因となっているが、サンプルデータに長期間アクセスする必要がある場合は、データを新しいバッファーにコピーしてからサンプルバッファーを解放することを検討してください

要するに、メモリをリサイクルできるように解放しないと、新しいバッファー用のメモリプールの空きがなくなる、ということだと思います。

メモリプールは解放しづらい

ではメモリを解放しましょう、となっても、これがなかなかサクッといかない。

・CIImageなどに変換して配列に保持しても、参照が解放されないらしく、同様に13フレームで止まってしまう。

・CMSampleBufferCreateCopy メソッドは浅いコピーを作るので、メモリプールが解放されず、やはり13フレームで止まってしまう。

・ Objective-C で明示的にメモリ解放すればいいのかもしれないが、大変そう(個人的問題ですが)。

対処法

CVPixelBufferに変換してディープコピーを作成することで、メモリプールを解放できます。

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, CVAttachmentMode(rawValue: 1)!),
            &_copy)

        guard let copy = _copy else { fatalError() }

        CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags(rawValue: 0x00000001))
        CVPixelBufferLockBaseAddress(copy, CVPixelBufferLockFlags(rawValue: 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, CVPixelBufferLockFlags(rawValue: 0))
        CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags(rawValue: 0))
        _copy = nil
        return copy
    }
}

captureOutput メソッド内で使用する。

guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                return
            }
            let copiedBuffer = pixelBuffer.copy()
            pixelBuffers.append(copiedBuffer)
            //CVPixelBufferの配列で保持

(注)配列で保持したCVPixelBuffer分のメモリはしっかり消費していくので、フレームの管理は注意しておこなってください。

***
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2