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) } }
Code language: Swift (swift)

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") } } }
Code language: Swift (swift)

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) } }
Code language: Swift (swift)

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) } }
Code language: Swift (swift)

(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) } }
Code language: Swift (swift)

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) } }
Code language: Swift (swift)

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() } } }
Code language: Swift (swift)

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) } } }
Code language: Swift (swift)

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