Play Video in SwiftUI (Sequence, Loop, etc.)
Reading time: 3 min
This recipe shows how to play videos in SwiftUI. It also shows how to add more advanced functionality, such as playing a sequence of videos, adding loop playback and custom end actions.
The end result looks like this:
If you wish to know more about what happens in that video, visit this recipe :).
Basic player
AVKit ships with the awesome VideoPlayer
view, which contains virtually all the functionality you'll need:
import AVKit
let videoURL = Bundle.main.url(forResource: "VideoTest", withExtension: "mov")!
var body: some View {
VideoPlayer(player: AVPlayer(url: videoURL))
}
And that's it! Notice that the video URL must be local - streaming from a remote platform (such as YouTube) isn't supported.
Drawing video overlay
You can also add an overlay view on top of the video. This view will sit between the video surface and the controls, meaning that it won't receive taps and other events if a player control is right above it:
VideoPlayer(player: AVPlayer(url: videoURL)) {
VStack {
Text("Viewer Discretion Advised")
.fontWeight(.bold)
.padding()
.background(RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow))
Spacer()
}
.padding()
}
Queueing multiple videos
If you wish to play a sequence of videos one after another, just pass an AVQueuePlayer
instance to VideoPlayer
:
let urls = [videoURL1, videoURL2, videoURL3]
...
VideoPlayer(player: AVQueuePlayer(items: urls.map { AVPlayerItem(url: $0) }))
Loop playback
Supporting loops is a bit trickier, and will require us to wrap VideoPlayer
in a custom view, which we'll aptly name EnchancedVideoPlayer
. It will:
- Internally use an
AVQueuePlayer
to support playing multiple videos. - Allow for a custom overlay.
- Have an action that will be performed after all the videos are done playing. For starters, we'll implement looping the video sequence (which, of course, works for a single video as well).
struct EnhancedVideoPlayer<VideoOverlay: View>: View {
@StateObject private var viewModel: ViewModel
@ViewBuilder var videoOverlay: () -> VideoOverlay
init(_ urls: [URL],
endAction: EndAction = .none,
@ViewBuilder videoOverlay: @escaping () -> VideoOverlay) {
_viewModel = StateObject(wrappedValue: ViewModel(urls: urls, endAction: endAction))
self.videoOverlay = videoOverlay
}
var body: some View {
VideoPlayer(player: viewModel.player, videoOverlay: videoOverlay)
}
class ViewModel: ObservableObject {
let player: AVQueuePlayer
init(urls: [URL], endAction: EndAction) {
let playerItems = urls.map { AVPlayerItem(url: $0) }
player = AVQueuePlayer(items: playerItems)
player.actionAtItemEnd = .none // we'll manually set which video comes next in playback
if endAction != .none {
// this notification is triggered whenever a player item finishes playing
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: nil,
queue: nil) { [self] notification in
let currentItem = notification.object as? AVPlayerItem
if endAction == .loop,
let currentItem = currentItem {
player.seek(to: .zero) // set the current player item to beginning
player.advanceToNextItem() // move to next video manually
player.insert(currentItem, after: nil) // add it to the end of the queue
}
}
}
}
}
enum EndAction: Equatable {
case none,
loop
}
extension EnhancedVideoPlayer where VideoOverlay == EmptyView {
init(_ urls: [URL], endAction: EndAction) {
self.init(urls, endAction: endAction) {
EmptyView()
}
}
}
Then, you can use it like this:
EnhancedVideoPlayer(videoURLs, endAction: .loop)
Custom end action
It's quite easy to further enhance our EnhancedVideoPlayer
to support performing any custom action after all the videos have finished playing. This is useful to, say, move to the next screen without requiring user input.
First, extend the EndAction
enum with a new case, representing the custom action:
enum EndAction: Equatable {
case none,
loop,
perform(() -> Void)
static func == (lhs: EnhancedVideoPlayer<VideoOverlay>.EndAction,
rhs: EnhancedVideoPlayer<VideoOverlay>.EndAction) -> Bool {
if case .none = lhs,
case .none = rhs {
return true
}
if case .loop = lhs,
case .loop = rhs {
return true
}
if case .perform(_) = lhs,
case .perform(_) = rhs {
return true
}
return false
}
}
Then, add its handler in the notification observer:
/// ....
player.insert(currentItem, after: nil)
} else if currentItem == playerItems.last,
case let .perform(action) = endAction {
action()
}
// ....
You can find the full code of EnhancedVideoPlayer
in this gist.