如何在iOS应用的SceneKit中创建循环播放的视频素材?

10

我该如何在SceneKit中创建一个播放循环视频的材质?


DanielJonsson,你好,如果你看到这个旧问题的话。请注意,幸运的是,苹果现在已经简化了这个过程,你只需要将一个视频播放器放在一个节点上(.firstMaterial?.diffuse.contents = .. 你的视频播放器)。谢谢! - Fattie
5个回答

29

使用SpriteKit场景作为几何体的材料,可以在SceneKit中实现此功能。

以下示例将创建一个SpriteKit场景,向其中添加一个带有视频播放器的视频节点,使视频播放器循环播放,创建一个SceneKit场景,向其中添加一个SceneKit平面,最后将SpriteKit场景添加为平面的漫反射材料。

import UIKit
import SceneKit
import SpriteKit
import AVFoundation

class ViewController: UIViewController, SCNSceneRendererDelegate {

    @IBOutlet weak var sceneView: SCNView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // A SpriteKit scene to contain the SpriteKit video node
        let spriteKitScene = SKScene(size: CGSize(width: sceneView.frame.width, height: sceneView.frame.height))
        spriteKitScene.scaleMode = .aspectFit

        // Create a video player, which will be responsible for the playback of the video material
        let videoUrl = Bundle.main.url(forResource: "videos/video", withExtension: "mp4")!
        let videoPlayer = AVPlayer(url: videoUrl)

        // To make the video loop
        videoPlayer.actionAtItemEnd = .none
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(ViewController.playerItemDidReachEnd),
            name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
            object: videoPlayer.currentItem)

        // Create the SpriteKit video node, containing the video player
        let videoSpriteKitNode = SKVideoNode(avPlayer: videoPlayer)
        videoSpriteKitNode.position = CGPoint(x: spriteKitScene.size.width / 2.0, y: spriteKitScene.size.height / 2.0)
        videoSpriteKitNode.size = spriteKitScene.size
        videoSpriteKitNode.yScale = -1.0
        videoSpriteKitNode.play()
        spriteKitScene.addChild(videoSpriteKitNode)

        // Create the SceneKit scene
        let scene = SCNScene()
        sceneView.scene = scene
        sceneView.delegate = self
        sceneView.isPlaying = true

        // Create a SceneKit plane and add the SpriteKit scene as its material
        let background = SCNPlane(width: CGFloat(100), height: CGFloat(100))
        background.firstMaterial?.diffuse.contents = spriteKitScene
        let backgroundNode = SCNNode(geometry: background)
        scene.rootNode.addChildNode(backgroundNode)

        ...
    }

    // This callback will restart the video when it has reach its end
    func playerItemDidReachEnd(notification: NSNotification) {
        if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
            playerItem.seek(to: kCMTimeZero)
        }
    }

    ...
}

1
我注意到你在多个场合下都在提问并回答自己的问题。为什么这样做? - gwinyai
@DanielJonsson 当我使用上述代码时,只有视频的四分之一部分可见。如何查看完整视频? - yaali
4
请注意,最近的 SceneKit 版本中,不需要使用 SKSceneSKVideoNode。您可以直接将 AVPlayer 设置为 SCNMaterialProperty 实例的内容。 - mnuages
@mnuages 我无法在11.4或12.0中直接将AVPlayer添加到材质的内容中。我在调试控制台中得到了一些"[SceneKit] Error: Cannot get pixel buffer (CVPixelBufferRef)"消息,但没有视频播放。使用相同的AVPlayer对象在SKScene中的SKVideo Node中并将其设置为材质内容可以正常工作。您是否有一些直接设置AVPlayer并正确工作的示例代码?谢谢。 - LenK
@LenK,您遇到的问题是在哪种设备上出现的?是 iPhone 5s 吗? - mnuages

7

2019年解决方案:

let mat = SCNMaterial()
let videoUrl = Bundle.main.url(forResource: "YourVideo", withExtension: "mp4")!
let player = AVPlayer(url: videoUrl)
mat.diffuse.contents = player
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(self,
                                       selector: #selector(playerItemDidReachEnd(notification:)),
                                       name: .AVPlayerItemDidPlayToEndTime,
                                       object: player.currentItem)
player.play()

方法在选择器中的代码:

@objc private func playerItemDidReachEnd(notification: Notification) {
    if let playerItem = notification.object as? AVPlayerItem {
        playerItem.seek(to: .zero, completionHandler: nil)
    }
}

注意:从现在起十年开始,您不需要删除运行选择器的通知(除非您正在使用块时)。

如果您穿越到2015年之前: 当对象被释放时,请勿忘记删除您的通知观察者!类似于 NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)


太棒了!对我来说完美地运作了。 - roosevelt

3

2022年,现在使用Scene Kit处理这个问题已经很容易了。请参阅苹果文档。

请注意,苹果文档明确说明,您现在可以将视频放置在SCNNode上。

// make some mesh. whatever size you want.
let mesh = SCNPlane()
mesh.width = 1.77
mesh.height = 1

// put the mesh on your node
yourNode.geometry = mesh

// add the video to the mesh
plr = AVPlayer(url: "https .. .m4v")
yourNode.geometry?.firstMaterial?.diffuse.contents = plr

请注意,您可以在网格上放置任何您想要的东西(“geometry”是指网格)。这很容易。例如,如果您只想要一个纯色:
请保留HTML标签,不提供解释。
... firstMaterial?.diffuse.contents = UIColor.yellow

请注意,问题是关于循环视频的。这很简单,并且与使用SceneKit无关。您可以看到有关循环视频的无数QA,它很容易:
NotificationCenter.default.addObserver(self,
  selector: #selector(loopy),
  name: .AVPlayerItemDidPlayToEndTime,
  object: plr.currentItem)

然后

@objc func loopy() { plr.seek(to: .zero) }

嘿@Fattie。如果我采用你的方法,在Xcode 14.1中为目标iOS 16.1,我的本地.mov视频不会循环播放。 - Andy Jazz
1
嗨@AndyJazz,这是完全标准的代码,而且它能够正常工作! :) 当然,你需要保留对播放器变量(在示例中为“plr”)的引用。因此,你需要在某个地方添加类似于“var plr: AVPlayer?”的代码(或者根据你的编码风格进行修改)。 - Fattie
1
也许在“loopy()”中加入一个打印语句以确保它已经到达那里... - Fattie

2

可以将AVPlayer用作场景背景的内容。 但是,直到我向sceneView发送.play(nil)之前,它才能正常工作。

override func viewDidLoad() {
    super.viewDidLoad()

    // Set the view's delegate
    sceneView.delegate = self

    // Show statistics such as fps and timing information
    sceneView.showsStatistics = true

    // Create a new scene
    let scene = SCNScene(named: "art.scnassets/ship.scn")!

    // create and add a camera to the scene
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    scene.rootNode.addChildNode(cameraNode)

    // Set the scene to the view
    sceneView.scene = scene

    let movieFileURL = Bundle.main.url(forResource: "example", withExtension: "mov")!
    let player = AVPlayer(url:movieFileURL)
    scene.background.contents = player
    sceneView.play(nil) //without this line the movie was not playing

    player.play()
}

1

Swift 5.7

1. SceneKit + SpriteKit的循环视频材质

...运行良好-视频无缝循环...

我在macOS Ventura 13.0.1上的Xcode 14.1模拟器(iOS 16.1)上测试了此应用程序。对于视频纹理,我使用了分辨率为1600x900的QuickTime .mov文件,并采用H.264编解码器。3D模型以.scn格式呈现。

import SceneKit
import AVFoundation
import SpriteKit

class GameViewController: UIViewController {
    
    var sceneView: SCNView? = nil
    private var avPlayer: AVQueuePlayer? = nil
    private var looper: AVPlayerLooper? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.sceneView = self.view as? SCNView
        guard let scene = SCNScene(named: "tv.scn") else { return }
        sceneView?.scene = scene
        sceneView?.allowsCameraControl = true
        
        self.spriteKitScene(self.sceneKitNode())
    }

    private func spriteKitScene(_ node: SCNNode) { ... }           // A

    internal func sceneKitNode() -> SCNNode { ... }                // B
    
    fileprivate func loadVideoMaterial() -> AVPlayer? { ... }      // C
}

SpriteKit场景可以播放.mov视频文件:

private func spriteKitScene(_ node: SCNNode) {
    
    let screenGeo: SCNPlane = node.geometry as! SCNPlane

    let videoNode = SKVideoNode(avPlayer: self.loadVideoMaterial()!)

    let skScene = SKScene(size: CGSize(width: screenGeo.width * 1600,
                                      height: screenGeo.height * 900))
    
    videoNode.position = CGPoint(x: skScene.size.width / 2,
                                 y: skScene.size.height / 2)
    
    videoNode.size = skScene.size
    skScene.addChild(videoNode)
    
    let screenMaterial = screenGeo.materials.first
    screenMaterial?.diffuse.contents = skScene
    videoNode.play()
    sceneView?.scene?.rootNode.addChildNode(node)
}

SceneKit的材质被用作SpriteKit视频的媒介:

internal func sceneKitNode() -> SCNNode {
    
    if let screen = sceneView?.scene?.rootNode.childNode(withName: "screen",
                                                      recursively: false) {
        
        screen.geometry?.firstMaterial?.lightingModel = .constant
        screen.geometry?.firstMaterial?.diffuse.contents = UIColor.black
        return screen
    }
    return SCNNode()
}

最后,用于加载视频的方法包含一个AVPlayerLooper对象:
fileprivate func loadVideoMaterial() -> AVPlayer? {
    
    guard let path = Bundle.main.path(forResource: "video", ofType: "mov")
    else { return nil }
    
    let videoURL = URL(fileURLWithPath: path)
    let asset = AVAsset(url: videoURL)
    let item = AVPlayerItem(asset: asset)
    self.avPlayer = AVQueuePlayer(playerItem: item)

    if let avPlayer {
        avPlayer.isMuted = true
        self.looper = AVPlayerLooper(player: avPlayer, templateItem: item)
        return avPlayer
    }
    return AVPlayer()
}

enter image description here

2. 纯 SceneKit 循环视频材质

……工作不正确——视频循环会有延迟……

你可以采用一个更短的路线来解决这个任务,但问题是,这种方法的效果并不像应该的那样——视频循环会带有等于整个视频时长的延迟。

import SceneKit
import AVFoundation

class GameViewController: UIViewController {
    
    var sceneView: SCNView? = nil
    private var avPlayer: AVQueuePlayer? = nil
    private var looper: AVPlayerLooper? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.sceneView = self.view as? SCNView
        sceneView?.isPlaying = true               // if view is playing?
        guard let scene = SCNScene(named: "tv.scn") else { return }
        sceneView?.scene = scene
        sceneView?.allowsCameraControl = true
        
        self.loadModelWithVideoMaterial()
    }

    fileprivate func loadModelWithVideoMaterial() { ... }
}

在这里,我们将一个AVQueuePlayer对象分配给素材的内容:
fileprivate func loadModelWithVideoMaterial() {
    
    guard let path = Bundle.main.path(forResource: "video", ofType: "mov")
    else { return }
    
    let videoURL = URL(fileURLWithPath: path)
    let asset = AVAsset(url: videoURL)
    let item = AVPlayerItem(asset: asset)
    self.avPlayer = AVQueuePlayer(playerItem: item)

    if let avPlayer {
        avPlayer.isMuted = true

        guard let screen = sceneView?.scene?.rootNode.childNode(
                                                       withName: "screen",
                                                    recursively: true)
        else { return }
            
        screen.geometry?.firstMaterial?.lightingModel = .constant
        screen.geometry?.firstMaterial?.diffuse.contents = avPlayer
        sceneView?.scene?.rootNode.addChildNode(screen)
        
        self.looper = AVPlayerLooper(player: avPlayer, templateItem: item)
        avPlayer.playImmediately(atRate: 20)      // speed x20 for testing
    }
}

1
AndyJazz,令人着迷的是可以使用SpriteKit。但您是否知道可以将视频放在场景工具包节点上?这很容易,yourNode.geometry?.firstMaterial?.diffuse.contents = AVPlayer(url: videoUrl) 您只需添加一个简单的平面作为网格即可。我会把它放在答案中。 - Fattie
关于SpriteKit - 我希望我的两个答案相互关联。 - Andy Jazz

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