Categories
Programming

Playing Videos in SwiftUI

As of WWDC 2020, we have a way to play videos in SwiftUI without bailing out to UIKit with UIViewRepresentable. At first glance, it’s pretty simple, as well:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL

	var body: some View {
		let player = AVPlayer(url: videoURL)
		VideoPlayer(player: player)
	}
}

Et voila, you’re playing a video! You can overlay content on top of the video player pretty easily:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL

	var body: some View {
		let player = AVPlayer(url: videoURL)
		VideoPlayer(player: player) {
			Text("Watermark")
		}
	}
}

Seems like we’re good to go, no?

Well, not quite. Let’s talk memory management.

VideoPlayerView is a struct – it’s immutable. SwiftUI allows us to mutate the state of our views with user interaction using things like @State, thanks to some Compiler Magic.

Every time some aspect of the state changes, SwiftUI calls the body getter again.

Spotted the catch yet?

We’re declaring the AVPlayer instance inside the body getter. That means it gets reinitalized every time body gets called. Not the best for something that’s streaming a video file over a network.

But wait, we’ve already mentioned the Compiler Magic we can use to persist state: @State! Let’s try:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL
	@State var player = AVPlayer(url: videoURL)

	var body: some View {
		VideoPlayer(player: player)
	}
}

Whoops. We’ve got a problem – self isn’t available during initialization, so we can’t initialize the AVPlayer like that. Alright, we’ll write our own init:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL
	@State var player: AVPlayer

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = State(initialValue: AVPlayer(url: videoURL))
	}

	var body: some View {
		VideoPlayer(player: player)
	}
}

(I suppose we could drop the let videoURL: URL there, since we’re using it immediately instead of needing to store it, but for consistency’s sake I’m leaving it in.)

Okay, sounds good – except, hang on, @State is only intended for use with structs, and if we peek at AVPlayer it’s a class.

Okay, no worries, that’s what @StateObject is for, one more tweak:

import SwiftUI
import AVKit

struct VideoPlayerView: View {
	let videoURL: URL
	@StateObject var player: AVPlayer

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = StateObject(wrappedValue: AVPlayer(url: videoURL))
	}

	var body: some View {
		VideoPlayer(player: player)
	}
}

There, we should be good to go now, right? Right?

Alas, the compiler says no. AVPlayer doesn’t conform to ObservableObject, so we’re out of luck.

Fortunately, ObservableObject is pretty easy to conform to, and we can make our own wrapper.

import SwiftUI
import AVKit
import Combine

class PlayerHolder: ObservableObject {
	let player: AVPlayer
	init(videoURL: URL) {
		player = AVPlayer(url: videoURL)
	}
}

struct VideoPlayerView: View {
	let videoURL: URL
	@StateObject var playerHolder: PlayerHolder

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = StateObject(wrappedValue: PlayerHolder(videoURL: videoURL))
	}

	var body: some View {
		VideoPlayer(player: playerHolder.player)
	}
}

Phew. At long last, we’ve got a stable way to hold onto a single AVPlayer instance. And, as a bonus, we can do stuff with that reference:

import SwiftUI
import AVKit
import Combine

class PlayerHolder: ObservableObject {
	let player: AVPlayer
	init(videoURL: URL) {
		player = AVPlayer(url: videoURL)
	}
}

struct VideoPlayerView: View {
	let videoURL: URL
	@StateObject var playerHolder: PlayerHolder

	init(videoURL: URL) {
		self.videoURL = videoURL
		self._player = StateObject(wrappedValue: PlayerHolder(videoURL: videoURL))
	}

	var body: some View {
		VideoPlayer(player: playerHolder.player)
			.onAppear {
				playerHolder.player.play()
			}
	}
}

Will start playing the video as soon as the view opens. Similarly, you could add some Buttons, build your own UI on top of the video player, all sorts of fun.

And, from the other end, you can put more logic in the PlayerHolder, as well. Say you need some additional logic to get from a video ID to the actual URL that the AVPlayer can handle? Try something like this:

class PlayerHolder: ObservableObject {
	@Published var player: AVPlayer? = nil
	init(videoID: Video.ID) {
		NetworkLayer.shared.lookupVideoInformation(videoID) { result in 
			self.player = AVPlayer(url: result.url)
		}
	}
}

(Now, that’s not the nice, Combine-y way to do it, but I’ll leave that as an exercise for the reader. Or possibly another post.)

2 replies on “Playing Videos in SwiftUI”

Or simply…

import SwiftUI
import AVKit

struct VideoView: View {
    let videoURL: URL
    @State private var avPlayer = AVPlayer()
    
    var body: some View {
        VideoPlayer(player: avPlayer)
            .onAppear {
                avPlayer.replaceCurrentItem(with: AVPlayerItem(url: videoURL))
                avPlayer.play()
            }
            .onDisappear {
                avPlayer.pause()
            }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.