SwiftUI - observing AVPlayer playback state

I am learning SwiftUI, I want to observe an AVPlayer status so I know when the videos is paused or not.

My current approach is more less like this:

  • I have VideosView that holds a list of a videos (in ZStack cards).
  • VideoViews has a VideosViewModel.
  • in VideosView i am calling in onAppear VideosViewModel.getItems...
struct ItemModel: Identifiable, Codable, Hashable, Equatable {
  var id: String
  var author: String // video owner
  var url: URL? // url to the video
  var player: AVPlayer? // AVPlayer created based on self.url...

  mutating func setPlayer(_ avPlayer: AVPlayer) {
    self.player = avPlayer
  }
}

// vm

class FeedViewModel: ObservableObject {
  @Published private(set) var items: [ItemModel] = []


  func getItems() async {
    do {
      // fetch data from the API
      let data = try await dataService.fetchFeeds()

      // download and attach videos
      downloadFeedVideos(data)
    } catch {
      // ....
    }

  }

private func downloadFeedVideos(_ feeds: [ItemModel]) {

    for index in feeds.indices {
      var item = feeds[index]
      if let videoURL = item.url {

        self.downloadQueue.queueDownloadIfFileNotExists(
          videoURL,
          DownloadOperation(
            session: URLSession.shared,
            downloadTaskURL: videoURL,
            completionHandler: { [weak self] (localURL, response, error) in

              guard let tempUrl = localURL else { return }

             
              let saveResult = self?.fileManagerService.saveInTemp(tempUrl, fileName: videoURL.lastPathComponent)

              switch saveResult {
              case .success(let savedURL):

                DispatchQueue.main.async {
                  // maybe this is a wrong place to have it?
                  item.setPlayer(AVPlayer(url: savedURL))

                  self?.items.append(item)

                  if self?.items.count ?? 0 > 1 {
                    // once first video is downloaded, use all device cores to fetch next videos
                    // all newest iOS devices has 6 cores
                    self?.downloadQueue.setMaxConcurrentOperationCount(.max)
                  }
                }

              case .none: break
              case .failure(_):
                EventTracking().track("Video download fail", [
                  "id": item.id,
                  "ulr": videoURL.absoluteString.decodeURL()
                ])
              }

            }), { fileCacheURL in
              // file already downloaded

              DispatchQueue.main.async {
                item.setPlayer(AVPlayer(url: fileCacheURL))
                self.items.append(item)
              }
            })
      }
    }

  }
}

I found this article with some pseudo-code of how to track video playback state but I'm not sure how to implement it in my code....

https://developer.apple.com/documentation/avfoundation/media_playback/observing_playback_state

Replies

Hi, @breq

The ItemModel above is almost right, except there is no need to store the AVPlayer in it. You can instantiate a single AVPlayer on app init, and set it as the player in a SwiftUI VideoPlayer instance. Then, when you want to play a certain video, you create an AVAsset using the URL in the Item and set it as the AVPlayer current item using:

let asset = AVAsset(url: item.url)
let playerItem = AVPlayerItem(asset: asset)
player.replaceCurrentItem(with: playerItem)

As for knowing when it pauses and plays, you can use a rate change observer in the class holding your AVPlayer like this:

let name = AVPlayer.rateDidChangeNotification
for await _ in NotificationCenter.default.notifications(named: name) { 
    //  in here you can read the player.rate, player.currentTime() and do your processing
}

Just remember that when the player is paused, it's rate == 0.0.

Also, you should never set the rate directly. Use .pause() and .play() instead because the user may have chosen a different playing speed on the UI...

Happy coding!