10

How do I create a material in SceneKit that plays a looping video?

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
Daniel Jonsson
  • 3,261
  • 5
  • 45
  • 66
  • DanielJonsson, cheers, if you see this old question. Note that fortunately Apple now made it easy, you can just put a video player on a node. (`.firstMaterial?.diffuse.contents` = .. your video player). – Fattie Dec 03 '22 at 13:43

5 Answers5

29

It's possible to achieve this in SceneKit using a SpriteKit scene as the geometry's material.

The following example will create a SpriteKit scene, add a video node to it with a video player, make the video player loop, create a SceneKit scene, add a SceneKit plane, and finally add the SpriteKit scene as the plane's diffuse material.

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)
        }
    }

    ...
}
Daniel Jonsson
  • 3,261
  • 5
  • 45
  • 66
  • 1
    I've noticed you've been asking and answering your own questions on more than one occasion. Why are you doing that? – gwinyai Feb 26 '17 at 13:25
  • @DanielJonsson Only quarter portion of the video is visible when i use the above code. How to view it in full? – Vidhya Sri Nov 09 '17 at 12:06
  • 4
    Note that in recent versions of SceneKit using a `SKScene` and a `SKVideoNode` is not necessary. You can directly set the `AVPlayer` as the contents of a `SCNMaterialProperty` instance. – mnuages Apr 22 '18 at 22:47
  • @mnuages I've not been able to get adding an AVPlayer directly to the contents of a material to work at all in 11.4 or 12.0. I get a handful of "[SceneKit] Error: Cannot get pixel buffer (CVPixelBufferRef)" messages logged to the debug console and no video plays. Using the same AVPlayer object with an SKVideo Node in an SKScene and setting that as the material contents works OK. Do you have some sample code that directly sets the AVPlayer and works correctly? Thanks. – LenK Sep 15 '18 at 23:41
  • @LenK on what device are you experiencing the issue? Is that an iPhone 5s? – mnuages Sep 16 '18 at 11:54
7

Year 2019 solution:

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()

Code for method in selector:

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

Note: for ten years now you do NOT remove notifications that run a selector. (You only need to do so in the obscure case you're using a block.)


If you have time-travelled to before 2015: Don't forget to remove your notification observer when the object is deallocated! Something like NotificationCenter.default .removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)

Fattie
  • 27,874
  • 70
  • 431
  • 719
4tuneTeller
  • 500
  • 1
  • 7
  • 12
3

2022, it's now trivial to do this in Scene Kit. See Apple docs.

Note that the apple doco clearly states you can now just put video on an 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

Note that you can put anything you want on the mesh. ("geometry" is the mesh.) It's easy. For example, if you just want a plain color:

... firstMaterial?.diffuse.contents = UIColor.yellow

Note that the question asks about looping the video. This is trivial and unrelated to using SceneKit. You can see a million QA about looping video, it's this easy:

NotificationCenter.default.addObserver(self,
  selector: #selector(loopy),
  name: .AVPlayerItemDidPlayToEndTime,
  object: plr.currentItem)

and then

@objc func loopy() { plr.seek(to: .zero) }
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Hey @Fattie. If I apply your approach, my local `.mov` video is not looping in Xcode 14.1 for target iOS 16.1. – Andy Jazz Dec 04 '22 at 10:53
  • 1
    Hi @AndyJazz; it's completely standard code, and works! :) Note that of course you'd have to keep a reference to the player variable ("plr" in the example). So you'd have like "var plr: AVPlayer?" somewhere (or whatever your style is). – Fattie Dec 04 '22 at 20:25
  • 1
    maybe throw a print statement in the "loopy()" to ensure it's getting there ... – Fattie Dec 04 '22 at 20:26
2

It is possible to use an AVPlayer as the content of the scene's background. However, it was not working for me until I sent .play(nil) to the sceneView.

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()
}
Psycrow
  • 21
  • 2
1

Swift 5.7

1. SceneKit + SpriteKit's Looping Video Material

...works fine – video is looping seamlessly...

I tested this app on Xcode 14.1 Simulator (iOS 16.1) on macOS Ventura 13.0.1. For video texture I used QuickTime 1600x900 .mov file with H.264 codec. 3D model's in .scn format.

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 scene is capable of playing back a .mov video file:

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)
}

The SceneKit's material is used as a medium for the SpriteKit's video:

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()
}

And, at last, the method used for loading the video contains an AVPlayerLooper object:

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. Pure SceneKit's Looping Video Material

...works incorrectly – video is looping with delay...

You can take a shorter route to solve this task, but the problem is, an approach does not work as it should - the video loop plays with a delay equal to the duration of the whole video.

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() { ... }
}

Here we are assigning an AVQueuePlayer object to the content of the material:

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
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
  • 1
    AndyJazz, it is fascinating that one can use an SpriteKit. But were you aware you can simply put a video on a scene kit node? it's this easy, `yourNode.geometry?.firstMaterial?.diffuse.contents = AVPlayer(url: videoUrl)` You just add a simple plane as the mesh. I'll put it in an answer. – Fattie Dec 03 '22 at 13:19
  • About SpriteKit – I wanted both of my answers to be interrelated. )) – Andy Jazz Dec 03 '22 at 13:36