Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

AVPlayer on macOS: Item Readiness, Time Observers, and Playback State

Manage AVPlayer item replacement, readiness, periodic observer tokens, seeking, buffering states, and UI teardown without stale playback callbacks.

AVPlayer coordinates playback of an AVPlayerItem, but it does not by itself provide a video interface or a durable playback model. The player owns transport state; the item represents one media presentation; AVPlayerView or AVPlayerLayer presents video. A reliable player UI observes the right state, tears down every observer it registers, and rejects callbacks from an item that has already been replaced.

Use AVPlayerView when its controls and presentation behavior fit the product. Build a custom transport only when there is a concrete UX requirement, because then the application owns play/pause, seeking, live and indefinite timelines, buffering indicators, accessibility, and observer lifecycle.

Player and item are different state owners

An AVPlayer can replace its current item. The player may continue to exist while an item is loading, ready, failed, or removed. Observe AVPlayerItem.status for readiness and errors, and observe player state such as timeControlStatus to distinguish playing, paused, and waiting-to-play behavior. A Boolean “is playing” cannot express all the states the interface needs.

Create a new item for a new URL or asset, attach observers for that item, and remove old observers when replacing it. Key each asynchronous callback to the item’s identity or a monotonically increasing generation. A delayed readiness notification from an old item must not turn the new item’s play button into a ready state.

import AVFoundation
import Foundation

final class PlaybackClock {
    let player: AVPlayer
    private var timeObserver: Any?

    init(url: URL) {
        player = AVPlayer(playerItem: AVPlayerItem(url: url))
        let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
        timeObserver = player.addPeriodicTimeObserver(
            forInterval: interval,
            queue: .main
        ) { time in
            guard time.isNumeric else { return }
            // Publish time to a UI model on the main queue.
        }
    }

    func tearDown() {
        if let timeObserver {
            player.removeTimeObserver(timeObserver)
            self.timeObserver = nil
        }
        player.pause()
    }
}

Keep the opaque token returned by addPeriodicTimeObserver strongly for as long as observation is active, then pass it to removeTimeObserver(_:). Apple documents that releasing a token without removing it results in undefined behavior. The observer interval is interpreted in the current item’s timeline, and callbacks can also occur when time jumps or playback starts or stops; do not treat the callback as a metronome for wall-clock time.

Observe time with the purpose-built API

Key-value observation works for general state properties, but it is not the right mechanism for a continuously changing playback clock. Use periodic observation to update a time display or scrubber and a boundary observer for specific timeline moments. Choose an interval that matches the UI’s visual update needs rather than requesting extremely frequent callbacks.

Always remove periodic and boundary observers before their owner goes away or before replacing the player. Centralize tokens in a controller or playback coordinator. If a block captures the owner strongly and the owner retains the player, an observer cycle can keep the entire playback graph alive. Use weak ownership where appropriate and still remove the token explicitly.

Keep the observation policy tied to the UI that needs it. A compact now-playing row may need only coarse progress updates, while a visible scrubber can justify more frequent refreshes during active playback. Pause or reduce presentation work when the view is hidden, but do not mistake stopping UI observation for stopping playback. Separate those controls so closing a window cannot accidentally change audio policy owned by another part of the application.

Convert CMTime carefully. A live or indefinite asset may not have a finite duration. Check whether times are numeric and whether duration is finite before calculating a percentage or formatting a countdown. Do not divide current time by zero or display “100%” for a stream whose end is unknown.

Readiness and error handling

Media assets can load asynchronously. A player item becoming ready indicates that playback can begin under its current conditions; it does not guarantee that the entire remote resource is already buffered or that playback will never stall. Surface waiting and failed states separately, and observe item error information where the app needs diagnostics.

For local content, verify that the URL is readable and the item reaches a playable state. For remote content, handle connection interruption, access failure, expired URLs, and server errors. Do not call play repeatedly on a timer while the player is waiting. Let AVFoundation manage loading and expose a user-visible retry action if recovery requires a new URL or authorization.

Seeking is asynchronous in many playback cases. Update the scrubber’s target separately from the player’s confirmed current time, and use a seek completion or generation check before applying a delayed result. If the user drags rapidly, coalesce intermediate seeks and issue the final target. An old seek completion should not overwrite a more recent user request.

Item end and replay policy

Observe item completion to update the transport, but distinguish end-of-item from failure and user stop. Decide whether the app should rewind, advance a playlist, or remain at the end. If the same item is replayed, make sure end notifications and observer registrations are not duplicated. Keep playlist identity separate from the current player’s item object.

For looping, use the documented looping helper or an explicit replacement policy; do not assume setting the time to zero inside an observer callback is equivalent to a gapless loop. A seamless loop may require media whose boundaries are prepared for the transition and a playback architecture designed for it.

If the product supports a queue, model the queue independently from AVPlayer.currentItem. A playlist can advance because an item ended, was removed, or failed, and those cases need different UI and telemetry. Resolve the next item from the current playlist revision, not from an array index captured when playback began. This prevents a delayed completion from advancing to the wrong track after the user reorders or edits the queue.

Presentation and control responsibilities

AVPlayer and AVPlayerItem are nonvisual. On macOS, AVPlayerView is the standard AVKit presentation component. AVPlayerLayer is appropriate when an app needs a custom view but it does not supply transport controls. Keep the view’s presentation layer attached to the same player that the controller owns, and detach it when the document or window changes.

Accessibility should expose play/pause state, current time, duration when known, and seek controls. Do not rely on color or animation alone to show buffering. If playback can continue after the user closes the view, define an app-level ownership policy; otherwise pause and tear down deliberately. AVPlayer does not decide the product’s background playback expectations.

Diagnostics and performance

Record item generation, status transitions, player time-control state, seek start/end, observer count, and error domain/code. Avoid logging signed media URLs or private metadata. Use AVPlayerItemAccessLog and error-log events where their documented fields help distinguish network and media issues, while treating server-provided strings as diagnostic data rather than trusted application state.

Do not update a high-cost interface every time the playback clock callback fires. Coalesce UI publication and keep the player callback short. If work must leave the callback queue, capture the item generation and discard the result if the current item has changed before it returns.

Acceptance tests

Test immediate local playback, delayed remote readiness, invalid media, item replacement while playing, rapid seek, pause during buffering, end-of-item, live stream with indefinite duration, window closure, and player teardown while callbacks are pending. Assert exactly one active time observer per active playback model and no UI update from a stale item generation.

Measure time to first frame, seek completion latency, buffering duration, CPU use during clock updates, and observer cleanup after closing and reopening the player. Test audio-only and video media, finite and indefinite timelines, and multiple simultaneous player windows if the product supports them.

A stable AVPlayer integration assigns ownership to the player and item, observes state according to its semantics, and makes teardown explicit. AVFoundation manages playback mechanics; the application still owns UI truth, item transitions, and lifecycle cleanup.

Related:

Sources:

Comments