5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftAdvent Calendar 2021

Day 12

SCNViewの描画結果をPIPで表示する

Last updated at Posted at 2021-12-11

SCNView の描画結果を PIP で表示する

SCNView の描画結果を AVPictureInPictureController を使って PIP で表示させる方法です
アプリがバックグラウンドの時に更新されないので有用性は未知数ですが、、、(止まるの忘れてました)

ソースコードはこちらです

SCNView の描画結果を取得する

SCNView は CAMetalLayer で描画しています
SCNSceneRendererDelegate の didRenderScene で描画されたことを検出して、CAMetalLayer で描画された内容を取得します
method swizzling して、一番最後に使った nextDrawable の texture にアクセスできるようにしておきます


extension CAMetalLayer {
    private struct AssociatedObjectKeyList {
        static var lastNextDrawableTextureKey = "lastNextDrawableTextureKey"
    }
    public static func swizzling() {
        let cls = CAMetalLayer.self
        let original = class_getInstanceMethod(cls, #selector(nextDrawable))!
        let swizzling = class_getInstanceMethod(cls, #selector(swizzled_nextDrawable))!
        method_exchangeImplementations(original, swizzling)
    }
    public var lastNextDrawableTexture: MTLTexture? {
        get {
            return objc_getAssociatedObject(self, &AssociatedObjectKeyList.lastNextDrawableTextureKey) as? MTLTexture
        }
        set {
            objc_setAssociatedObject(self, &AssociatedObjectKeyList.lastNextDrawableTextureKey, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
    @objc private func swizzled_nextDrawable() -> CAMetalDrawable? {
        let swizzled = swizzled_nextDrawable()
        lastNextDrawableTexture = swizzled?.texture
        return swizzled
    }
}

SCNView.layer.lastNextDrawableTexture を PIP で表示させます

MTLTexture を PIP で表示する

CIImage の作成

取得した MTLTexture は上下反転しているので、transform させます

let ci = CIImage(mtlTexture: texture, options: nil)!
let flippedCi = ci.transformed(by: .init(scaleX: 1, y: -1))
                .transformed(by: .init(translationX: 0, y: CGFloat(texture.height)))

CMTime と CIImage から CMSampleBuffer を作成します

func make(time: CMTime, ci: CIImage) -> CMSampleBuffer? {
    let buff: CVPixelBuffer = makePixelBuffer(width: Int(ci.extent.size.width), height: Int(ci.extent.size.height))
    let desc: CMVideoFormatDescription?
    CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault,
                                                 imageBuffer: buff,
                                                 formatDescriptionOut: &desc)
    CVPixelBufferLockBaseAddress(buff, CVPixelBufferLockFlags(rawValue: 0))
    context.render(ci, to: buff)
    var tmp: CMSampleBuffer?
    var sampleTiming = CMSampleTimingInfo()
    sampleTiming.presentationTimeStamp = time
    _ = CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault,
                                           imageBuffer: buff,
                                           dataReady: true,
                                           makeDataReadyCallback: nil,
                                           refcon: nil,
                                           formatDescription: desc!,
                                           sampleTiming: &sampleTiming,
                                           sampleBufferOut: &tmp)
    CVPixelBufferUnlockBaseAddress(buff, CVPixelBufferLockFlags(rawValue: 0))
    return tmp
}

AVSampleBufferDisplayLayer を使って frame を表示する

AVSampleBufferDisplayLayer を持つ、SampleBufferDisplayView を作ります

class SampleBufferDisplayView: UIView {
    override class var layerClass: AnyClass {
        get { return AVSampleBufferDisplayLayer.self }
    }

    var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer {
        return layer as! AVSampleBufferDisplayLayer
    }
}

AVSampleBufferDisplayLayer.enqueue を使って、MTLTexture から作成した CMSampleBuffer を表示させます

self.sampleBufferDisplayView.sampleBufferDisplayLayer.enqueue(cm)

AVPictureInPictureController で PIP する

AVAudioSession の category を .playback に設定しておきます
AVSampleBufferDisplayLayer を使って ContentSource を作成します
作成した ContentSource から AVPictureInPictureController を用意します
isPictureInPicturePossible が true になるのを待ってから、PIP を開始できるようにすると良いようです

try! AVAudioSession.sharedInstance().setCategory(.playback)
let pipContentSource = AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: sampleBufferDisplayView.sampleBufferDisplayLayer,
                                                                  playbackDelegate: self)
pipController = AVPictureInPictureController(contentSource: pipContentSource)
pipController
    .publisher(for: \.isPictureInPicturePossible, options: [.initial, .new])
    .sink { possible in
        print("isPictureInPicturePossible: \(possible)")
    }
    .store(in: &cancellables)

PIP の開始、終了

ボタンのタップで開始、終了します

@IBAction func onTapButton(_ sender: Any) {
    if pipController.isPictureInPictureActive {
        pipController.stopPictureInPicture()
    } else {
        pipController.startPictureInPicture()
    }
}

プロジェクトの設定

Capability から Background Modes を追加して、Picture in Picture を有効にします

image.png

まとめ

SCNView の描画結果を PIP で表示する方法でした
AVPictureInPictureController は自由に表示できるので、夢が広がります

5
3
1

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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?