For the next couple of posts, I’m going to take a short break from my Tetris stuff. I’m starting to hit my limits with what I know how to do, and I’m needing to dive into some new topics. For instance, I want to set the background of my game to some kind of looping video. This leads me to this next post: playing a looping video.

In my research, I found 2 conceptually different ways of playing videos. Both ways use AVFoundation, but they take very different approaches.

The first way I found uses an AVPlayer with a single video, and will rewind the video to the beginning when the video ends. Most of the forums I found talking about looping videos in SwiftUI use this approach, but this never worked quite right for me. The videos stutter (even after stripping the audio track), and it’s not the way Apple recommends.

The second approach uses an AVQueuePlayer with an AVPlayerLooper to keep adding the video to the end of the queue. This approach is endorsed in this 2016 WWDC talk, and I found that this works so much better than rewinding the video. This also helped me understand all the issues I was running into with the rewinding approach.

Because I want this article to be all-inclusive, I’ll go over both approaches. If you’re looking for the a quick solution, please jump down to the AVQueuePlayer section.

Some Basic Setup

Before getting into looping a video, let’s find a video to loop.

I’m going to be using this Video from pixabay. I ran this video through resolve to add a green block right before the video loops so that you can also see where the video loops. Adding the square has been tremendously helpful in figuring out if there’s a stutter or not.

Importing the video is pretty straightforward. I just created a new group to hold my assets, copied the asset there (making sure the target checkbox was checked), and then added it to my assets file.

When using this asset, you’ll normally reference it by its URL. You get the URL for the asset using Bundle.main.url, and passing this the asset name (VideoWithBlock) and it’s type (mov). This was the first time I ever needed to get the URL for an asset, so this took me longer than I’d like to admit to figure out.

let fileUrl = Bundle.main.url(forResource: "VideoWithBlock", withExtension: "mov")!

Way 1: AVPlayer

As mentioned above, this is not the Apple approved approach. I want to cover this a bit because this was the first solution I found, and a lot of people recommended this. This way did not work properly for me, and it caused me a ton of grief.

My code is going to be based off this post on medium, and this is going to be an AVPlayer that’s wrapped with UIViewRepresentable. As with a lot of things in SwiftUI, the guts of this will not be SwiftUI.

On top of a the base AVPlayer, we’ll need to add functionality that listens for when the video ends. We can listen for an AVPlayerItemDidPlayToEndTime notification, and this will call a function that rewinds our player to the start of the video.

import SwiftUI import AVKit import AVFoundation struct PlayerView: UIViewRepresentable { func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) { } func makeUIView(context: Context) -> UIView { return PlayerUIView(frame: .zero) } } class PlayerUIView: UIView { private let playerLayer = AVPlayerLayer() required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override init(frame: CGRect) { super.init(frame: frame) // Load the resource let fileUrl = Bundle.main.url(forResource: "VideoWithBlock", withExtension: "mov")! // Setup the player let player = AVPlayer(url: fileUrl) playerLayer.player = player playerLayer.videoGravity = .resizeAspectFill layer.addSublayer(playerLayer) // Setup looping player.actionAtItemEnd = .none NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) // Start the movie player.play() } @objc func playerItemDidReachEnd(notification: Notification) { playerLayer.player?.seek(to: CMTime.zero) } override func layoutSubviews() { super.layoutSubviews() playerLayer.frame = bounds } }

Do you see how this stutters a bit after the green box? This is very clearly not seamless playback.

Way 2: AVPlayerLooper

Now that we see that the StackOverflow recommended AVPlayer way doesn’t work, let’s check out the Apple approved way.

First, we’ll need to load the video into a AVPlayerItem. The AVPlayerLooper will take in this item, and re-use it so that we don’t have to load the same video a bunch of times.

The next thing we do is create an AVQueuePlayer. This is a special type of AVPlayer that has a bunch of videos queued up, and it pre-loads videos so that we don’t have to worry about breaks in playback when a video ends.

Finally, we setup an AVPlayerLooper with the video we want to play. The AVPlayerLooper will handle all the queueing up so that our AVQueuePlayer will continue to loop the video. We’ll also need to keep a reference to this object to avoid it getting cleaned up.

import SwiftUI import AVKit import AVFoundation struct PlayerView: UIViewRepresentable { func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) { } func makeUIView(context: Context) -> UIView { return LoopingPlayerUIView(frame: .zero) } } class LoopingPlayerUIView: UIView { private let playerLayer = AVPlayerLayer() private var playerLooper: AVPlayerLooper? required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override init(frame: CGRect) { super.init(frame: frame) // Load the resource let fileUrl = Bundle.main.url(forResource: "VideoWithBlock", withExtension: "mov")! let asset = AVAsset(url: fileUrl) let item = AVPlayerItem(asset: asset) // Setup the player let player = AVQueuePlayer() playerLayer.player = player playerLayer.videoGravity = .resizeAspectFill layer.addSublayer(playerLayer) // Create a new player looper with the queue player and template item playerLooper = AVPlayerLooper(player: player, templateItem: item) // Start the movie player.play() } override func layoutSubviews() { super.layoutSubviews() playerLayer.frame = bounds } }

Buttery smooth!

Fixing A Stuttering Video

When I first implemented this, I kept running into stuttering. I originally thought it was some sort of lag, and it was driving me crazy. One of the assets I was using looped totally fine in other programs I was using, but not using AVPlayer.

I saw this posted to StackOverflow a few time, and I eventually came across a response that mentioned that the audio may be slightly longer than the video. I ran the video through resolve to strip out the audio track, and that fixed it. The WWDC talk I linked to in the intro mentions this issue as well, and they say you can programmatically do this by setting the playback time to be the shortest of all the tracks.

Note: You may still experience stuttering if you’re using an AVPlayer instead of the AVQueuePlayer approach. However, this reduced the stuttering a ton with the AVPlayer.

Final Thoughts

There’s so much information out there about Swift and SwiftUI, but this was a lesson in validating that information. The first approach I found on StackOverflow didn’t work, and it’s explicitly advised against by Apple. This is a reminder to everyone that what you read online may be misleading or incorrect.

Feel free to check out my Github Repo, or watch my tutorial about this on Youtube:

Thanks for reading!

– SchwiftyUI