如何在SceneKit中最佳地播放视频?

8
我尝试了多种不同的技术来在SceneKit中播放视频,但似乎没有一种能正常工作。
我曾尝试将AVPlayerLayer分配给SCNMaterial.diffuse.contents,但根本无法显示任何视频帧,尽管声音播放正常。
我还尝试将包含SKVideoNode子节点的SpriteKite SKScene分配给SCNMaterial.diffuse.contents,这样可以正常播放,但是每次视频播放时会泄漏数兆字节的内存,最终导致iOS终止应用程序。 (如StackOverflow上其他人所指出的)

最后,由于iOS 11.0+已经支持并得到@mnuages等人的推荐,我尝试直接将AVPlayer分配给SCNMaterial.diffuse.contents。虽然它可以播放且不会泄漏内存,但遗憾的是这种方法存在许多不良视觉效果(即使在iOS 12.1.2上也是如此)。特别是,正如其他开发者所指出的,每次将AVPlayer实例分配给材质并开始播放时,SceneKit都会在控制台上记录以下内容:

[SceneKit] 错误:无法获取像素缓冲区(CVPixelBufferRef)

虽然日志消息可能还算可以接受,但每次出现错误时,场景中的视频帧都显示为全白色,这会导致每当新视频播放时闪烁一小段时间。

更糟糕的是,这种技术的另一个不良视觉效果是我称之为“迷幻彩虹”效应,即在将AVPlayer分配给材质时随机向相邻节点添加闪烁的彩色噪点图案,而与是否正在播放或暂停无关。看起来很酷(请参见附加的屏幕截图),但并不完全符合我们应用程序的需求。

有人知道如何可靠地使这些技术之一工作吗?

以下是我用于测试将AVPlayer直接分配给材质的代码:

class GameViewController: UIViewController {

var plane: SCNPlane!
var planeNode: SCNNode!
var playerObserver: NSKeyValueObservation?
var playerCompletionObserver: NSObjectProtocol?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    guard let scnView = self.view as? SCNView else {return}

    guard let scene = SCNScene(named: "art.scnassets/scene.scn") else {return}
    scnView.scene = scene

    plane = SCNPlane(width: 10, height: 10)
    planeNode = SCNNode(geometry: plane)
    planeNode.position.y = 10.0
    scene.rootNode.addChildNode(planeNode)

    scnView.isPlaying = true
    scnView.loops = true
    addPlayer()
}

func addPlayer() {

    guard let url = Bundle.main.url(forResource: "Clover", withExtension: "mov") else {return}
    let asset = AVURLAsset(url: url)
    let playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: [#keyPath(AVAsset.tracks), #keyPath(AVAsset.duration)])
    let player = AVPlayer(playerItem: playerItem)

    print("\(url.lastPathComponent) \(player.error?.localizedDescription ?? "loaded")")

    playerObserver = playerItem.observe(\.status, options:  [.new, .old], changeHandler: {[weak self] (playerItem, change) in
        guard let strongSelf = self else {return}

        if playerItem.status == .readyToPlay {

            strongSelf.playerObserver?.invalidate()
            strongSelf.playerObserver = nil

            if let material = strongSelf.plane.firstMaterial {

                material.diffuse.contents = player
                let videoSize = playerItem.asset.tracks(withMediaType: .video).first?.naturalSize ?? CGSize(width: 640, height: 480)
                material.diffuse.contentsTransform = SCNMatrix4Translate(SCNMatrix4MakeScale(Float(videoSize.height / videoSize.width), 1, 1), 0.25, 0, 0)
                material.isDoubleSided = true
            }

            strongSelf.playerCompletionObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) {[weak self](Notification) in
                guard let strongSelf = self else {return}
                NotificationCenter.default.removeObserver(strongSelf.playerCompletionObserver!)
                strongSelf.addPlayer()
            }

            player.play()
            }
        })

}

}

更新: 我已经尝试了许多自动加载的资源密钥和.preroll的组合,但到目前为止,它们都没有减少白色闪烁问题。然而,我想出了一些几乎可以消除这个问题的方法。
我注意到,如果节点的不透明度为零,SceneKit视频渲染器将不会执行任何操作,因此我更改了以下代码行:
player.play()

添加一点延迟:

planeNode.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
     player.play()
     self.planeNode.opacity = 1
}

像这样添加延迟总是让我觉得很笨拙,但我还没有找到其他的解决方案。有人有更好的修复方法吗?

SceneKit visual artifacts

1个回答

0
我使用了SKVideoNode和SKScene,然后设置plane.firstMaterial?.diffuse.contents = skScene。我还为AVPlayer设置了一个观察者,当它播放到结束时间时,它会寻找开始并重新播放。

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