Swift
AVPlayer
video playback
iOS development
media control

Check play state of AVPlayer

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Checking AVPlayer state is more nuanced than reading one property. Real playback behavior involves playing, pausing, waiting for buffering, end-of-item completion, and failure states. A robust solution combines timeControlStatus, rate, and item-level events so the UI reflects what the player is actually doing.

Start with timeControlStatus

For most user-facing playback state, timeControlStatus is the best high-level signal.

swift
1import AVFoundation
2
3func describe(player: AVPlayer) -> String {
4    switch player.timeControlStatus {
5    case .playing:
6        return "playing"
7    case .paused:
8        return "paused"
9    case .waitingToPlayAtSpecifiedRate:
10        let reason = player.reasonForWaitingToPlay?.rawValue ?? "unknown"
11        return "waiting: \(reason)"
12    @unknown default:
13        return "unknown"
14    }
15}

This is more accurate than checking only rate, because waiting and buffering are not the same thing as an intentional pause.

Observe Player State Changes

Playback state changes over time, so observation is often more useful than one-off inspection.

swift
1import AVFoundation
2
3final class PlayerObserver {
4    private var statusObservation: NSKeyValueObservation?
5    private var rateObservation: NSKeyValueObservation?
6
7    func bind(to player: AVPlayer) {
8        statusObservation = player.observe(\AVPlayer.timeControlStatus, options: [.initial, .new]) { p, _ in
9            print("timeControlStatus:", p.timeControlStatus.rawValue)
10        }
11
12        rateObservation = player.observe(\AVPlayer.rate, options: [.initial, .new]) { p, _ in
13            print("rate:", p.rate)
14        }
15    }
16}

This helps keep play buttons, spinners, and overlays synchronized with the actual player state.

Handle Completion and Failure Too

A player can stop because the item finished or because the item failed. Those are different states and should be handled differently.

swift
1import AVFoundation
2
3final class PlaybackEvents: NSObject {
4    private var token: NSObjectProtocol?
5
6    func register(for item: AVPlayerItem) {
7        token = NotificationCenter.default.addObserver(
8            forName: .AVPlayerItemDidPlayToEndTime,
9            object: item,
10            queue: .main
11        ) { _ in
12            print("Playback finished")
13        }
14
15        item.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
16    }
17
18    override func observeValue(
19        forKeyPath keyPath: String?,
20        of object: Any?,
21        change: [NSKeyValueChangeKey : Any]?,
22        context: UnsafeMutableRawPointer?
23    ) {
24        guard keyPath == "status", let item = object as? AVPlayerItem else { return }
25        if item.status == .failed {
26            print("Item failed:", item.error?.localizedDescription ?? "unknown")
27        }
28    }
29}

Without completion and failure handling, playback UI often gets stuck in an incorrect state.

Build a Small State Model

A small enum can keep the rest of the app from scattering playback logic across many files.

swift
1import AVFoundation
2
3enum PlaybackState {
4    case idle
5    case playing
6    case paused
7    case buffering
8    case failed(String)
9}
10
11func computeState(player: AVPlayer) -> PlaybackState {
12    if let item = player.currentItem, item.status == .failed {
13        return .failed(item.error?.localizedDescription ?? "unknown error")
14    }
15
16    switch player.timeControlStatus {
17    case .playing:
18        return .playing
19    case .paused:
20        return .paused
21    case .waitingToPlayAtSpecifiedRate:
22        return .buffering
23    @unknown default:
24        return .idle
25    }
26}

This gives the rest of the app one consistent source of truth.

One useful UI pattern is to map these playback states into a view-model layer rather than letting every button and overlay inspect AVPlayer directly. That keeps state transitions consistent and makes UI tests much easier to reason about.

Common Pitfalls

  • Using only rate to decide whether playback is active.
  • Ignoring .waitingToPlayAtSpecifiedRate and reporting buffering as paused.
  • Forgetting to observe item failure and completion separately from player status.
  • Leaving observers alive after a view or player is gone.
  • Spreading playback-state rules across many UI classes instead of centralizing them.

Summary

  • Use timeControlStatus as the primary AVPlayer state signal.
  • Observe player properties instead of relying only on one-time checks.
  • Handle completion and failure explicitly at the item level.
  • A small playback-state model makes the UI more consistent.
  • 'rate is useful, but by itself it is not a complete play-state answer.'

Course illustration
Course illustration

All Rights Reserved.