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:

preview

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

Simulator%20Screen%20Shot%20-%20iPhone%20SE%20%282nd%20generation%29%20-%202021-09-12%20at%2021.38.24

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:

  1. Internally use an AVQueuePlayer to support playing multiple videos.
  2. Allow for a custom overlay.
  3. 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.

Next Post Previous Post