soranoba
soranoba Author of soranoba.net
programming

UIViewの表示内容をCMSampleBufferにする

任意のUIViewをPinPに利用しようとすると、CMSampleBufferにUIViewの内容を変換する必要があります。
iOSで任意のUIViewをピクチャーインピクチャーするという素晴らしい記事と実装を公開してくださっているのですが、CMSampleBufferへの変換方法が少し微妙な気がしたので、そこの部分のみ実装を紹介しようと思います。

実装

extension UIView {
    func makeSampleBuffer() -> CMSampleBuffer? {
        let scale = UIScreen.main.scale
        let size = CGSize(width: bounds.width * scale, height: bounds.height * scale)

        var pixelBuffer: CVPixelBuffer?
        var status = CVPixelBufferCreate(kCFAllocatorDefault,
                                         Int(size.width),
                                         Int(size.height),
                                         kCVPixelFormatType_32ARGB,
                                         [
                                             kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue!,
                                             kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue!,
                                             kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionary,
                                         ] as CFDictionary,
                                         &pixelBuffer)
        if status != kCVReturnSuccess {
            assertionFailure("Failed to create CVPixelBuffer: \(status)")
            return nil
        }

        CVPixelBufferLockBaseAddress(pixelBuffer!, [])
        defer {
            CVPixelBufferUnlockBaseAddress(pixelBuffer!, [])
        }

        let context = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer!),
                                width: Int(size.width),
                                height: Int(size.height),
                                bitsPerComponent: 8,
                                bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!),
                                space: CGColorSpaceCreateDeviceRGB(),
                                bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)!
        context.translateBy(x: 0, y: size.height)
        context.scaleBy(x: scale, y: -scale)
        layer.render(in: context)

        var formatDescription: CMFormatDescription?
        status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault,
                                                              imageBuffer: pixelBuffer!,
                                                              formatDescriptionOut: &formatDescription)
        if status != kCVReturnSuccess {
            assertionFailure("Failed to create CMFormatDescription: \(status)")
            return nil
        }

        let now = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: 60)
        let timingInfo = CMSampleTimingInfo(duration: .init(seconds: 1, preferredTimescale: 60),
                                            presentationTimeStamp: now,
                                            decodeTimeStamp: now)
        do {
            return try CMSampleBuffer(imageBuffer: pixelBuffer!, formatDescription: formatDescription!,
                                      sampleTiming: timingInfo)
        } catch {
            assertionFailure("Failed to create CVSampleBuffer: \(error)")
            return nil
        }
    }
}

実装のポイント

ハマった箇所のみ説明を残しておくと、

  • kCVPixelBufferIOSurfacePropertiesKey: [:] as CFDictionaryの指定がないと実機で描画されません
  • UIScreen.main.scaleを利用して、Retinaディスプレイの解像度に耐えるように大きめのサイズでCVPixelBufferを作成しています
  • kCVPixelFormatType_32ARGB 色の順番を間違えると色がおかしくなります

(Updated: )

comments powered by Disqus