How do I create a material in SceneKit that plays a looping video?
-
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 Answers
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)
}
}
...
}

- 3,261
- 5
- 45
- 66
-
1I'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
-
4Note 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
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)

- 27,874
- 70
- 431
- 719

- 500
- 1
- 7
- 12
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) }

- 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
-
1Hi @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
-
1maybe throw a print statement in the "loopy()" to ensure it's getting there ... – Fattie Dec 04 '22 at 20:26
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()
}

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

- 49,178
- 17
- 136
- 220
-
1AndyJazz, 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