ios - 在 SceneKit 中播放视频的最佳方式是什么?
问题描述
我尝试了多种不同的技术在 SceneKit 中播放视频,但它们似乎都不能正常工作。
我尝试将 AVPlayerLayer 分配给 SCNMaterial.diffuse.contents 并且根本无法显示任何视频帧,尽管声音播放正常。
我还尝试将包含 SKVideoNode 子节点的 SpriteKite SKScene 分配给 SCNMaterial.diffuse.contents,并且效果很好,尽管(正如 StackOverflow 上的其他人所指出的那样)它每次播放视频时都会泄漏兆字节的内存,直到最后 iOS杀死应用程序。
最后,由于它现在在 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()
}
})
}
}
更新:我现在已经尝试了几十种自动加载AssetKeys 和.preroll 的组合,但到目前为止,它们都没有做任何事情来最小化白色闪烁问题。然而,我确实想出了一些几乎可以解决问题的方法。
我注意到如果节点的不透明度为零,SceneKit 视频渲染器不会做任何事情,所以我更改了这行代码:
player.play()
添加一点延迟:
planeNode.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
player.play()
self.planeNode.opacity = 1
}
像这样添加延迟对我来说似乎总是一种技巧,但我根本没有发现任何其他有效的方法。有没有人有更好的解决方法?
解决方案
我使用 SKVideoNode 和 SKScene 然后设置 plane.firstMaterial?.diffuse.contents = skScene。我还为 AVPlayer 设置了一个观察者,当它播放到结束时间时,它会寻找开始并再次播放。
推荐阅读
- javascript - Live-wire - 实时聊天没有正确响应
- c# - 如何在主项目单击时用 MasterDetailsView 中的特定页面替换详细信息窗格
- pandas - 无法将变量值转换为 DATE(镶木地板/雪花)
- reactjs - 任何内置的反应模块,如 ReactTable、react-router 等都不能在我的反应应用程序中工作。我该如何解决?吨
- javascript - 单击 React JS 中的任何链接时如何防止重定向到顶部
- javascript - 在使用赛普拉斯进行测试时,有没有办法可以防止下载
- javascript - On Scroll - 添加类 - 对于页面上的每个元素
- node.js - Nodejs Redis Scan不返回所有项目
- c++ - 如何将“IF CONDITION”重写为 ON 3 分钟和 OFF 10 分钟直到 4 小时(240 分钟)
- android - ViewModel 和 LiveData 观察者未调用