Every video app on an iPhone plays through the same engine. Social feeds, sports streams, subscription services: the frames on screen come out of AVPlayer, the playback controller Apple has shipped in AVFoundation since iOS 4.0. It now runs on iPadOS, macOS, tvOS, watchOS, and visionOS too.
That reach is also why AVPlayer trips up so many teams.
It isn’t a video player you drop into a view. It’s a transport controller sitting on top of an object graph (assets, player items, presentation layers), and most of the hard problems in an iOS video app come from misreading that graph rather than from the playback code itself.
What Is AVPlayer?
AVPlayer is a controller object in Apple’s AVFoundation framework that manages the playback and timing of a single media asset at a time. It handles play, pause, seeking, rate, volume, and buffering for local files and network streams. It draws nothing on its own.
Three root attributes define it:
- It’s a transport controller, not a view. Pair it with a presentation layer or you’ll see nothing.
- It plays one item at a time. Swap items with
replaceCurrentItem(with:), or step up toAVQueuePlayerfor a playlist. - Its state changes constantly and asynchronously. You read it through observation, never by polling.
AVPlayer exists because playback on Apple platforms needs a single object that owns the clock. Decoding, network buffering, audio routing, and rendering all have to agree on one timeline.
Apple’s AVPlayer reference describes that object as the thing that manages “the playback and timing of a media asset.”
Here’s where it runs, straight from Apple’s availability data:
| Platform | AVPlayer available since |
|---|---|
| iOS / iPadOS | 4.0 |
| macOS | 10.7 |
| Mac Catalyst | 13.1 |
| tvOS | 9.0 |
| watchOS | 1.0 |
| visionOS | 1.0 |
AVPlayer vs AVAudioPlayer vs the AVPlayer App
Three different things share the name, and search results mix all three.
AVPlayer (AVFoundation) is the API this guide covers. It plays audio and video, local files and network streams, and it’s the only one of the three that handles HTTP Live Streaming.
AVAudioPlayer is a separate AVFoundation class for audio only, and only for audio that’s already available as a local file or in memory. It can’t stream. If you’re playing a bundled sound effect, AVAudioPlayer is simpler. Anything over the network needs AVPlayer.
AVPlayer the app is a third-party media player for iOS from EPLAYWORKS, sold on the App Store, plus an unrelated Android app of the same name. It plays AVI, MKV, and other containers Apple’s own frameworks won’t touch. It has no connection to the API.
| AVPlayer (API) | AVAudioPlayer | AVPlayer (App Store app) | |
|---|---|---|---|
| Type | AVFoundation class | AVFoundation class | Consumer app |
| Media | Audio + video | Audio only | Audio + video |
| Network streaming | Yes (HLS, progressive) | No | Local files |
| Use case | Building playback into your app | Local sound playback | Watching downloaded files |
The rest of this guide means the AVFoundation class.
How Does AVPlayer Work?
AVPlayer sits at the end of a short chain. Understanding that chain is the difference between a player that works and one that shows a black rectangle.
- You describe the media with an asset.
AVAsset, usuallyAVURLAsset, models the static facts: duration, tracks, metadata. It knows nothing about playback position. - You wrap the asset in a player item.
AVPlayerItemmodels timing and presentation state during playback: current time, buffered ranges, status, selected audio and subtitle tracks. Every asset you play needs its own item. - You hand the item to a player. AVPlayer takes it through
init(playerItem:)orreplaceCurrentItem(with:)and becomes the owner of the clock. - The player loads and buffers. For a remote file it fetches byte ranges. For an HLS stream it pulls the M3U8 playlist, picks a rendition, and starts filling the buffer.
- Status flips to ready. The item’s
statusmoves to.readyToPlay, or to.failedwith an error you need to read. - You attach a presentation layer. An
AVPlayerLayer, anAVPlayerViewController, or a SwiftUIVideoPlayerrenders the video. Audio plays without any of this. - You call
play(). Rate goes to 1.0,timeControlStatusbecomes.playing, and the player starts pulling frames.
The step teams skip is step 5.
AVPlayer(url:) returns immediately, before the network has been touched, so calling play() on the next line often does nothing visible. Playback is asynchronous from the first instruction, and the API gives you no callback by default. You have to ask for one.
The Four Ways to Display AVPlayer Video
AVPlayer produces frames. Something else has to show them. Apple gives you four options, and picking the wrong one costs you either control or weeks spent rebuilding controls Apple already wrote.
1. AVPlayerViewController (UIKit, iOS/tvOS/visionOS)
The system player UI: scrubber, play/pause, AirPlay route picker, Picture in Picture button, subtitle menu, full-screen transition. Available since iOS 8.0 and tvOS 9.0.
It’s the fastest path to a shipping player, and it picks up accessibility and new OS features for free.
import AVKit
let player = AVPlayer(url: streamURL)
let controller = AVPlayerViewController()
controller.player = player
present(controller, animated: true) {
player.play()
}
Best for: apps that want a standard viewing experience without maintaining player controls.
2. VideoPlayer (SwiftUI)
VideoPlayer lives in AVKit, not SwiftUI, and has been available since iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, and visionOS 1.0. It’s the SwiftUI wrapper around the same system UI, and it accepts an optional AVPlayer.
import SwiftUI
import AVKit
struct PlayerScreen: View {
@State private var player = AVPlayer(url: streamURL)
var body: some View {
VideoPlayer(player: player)
.aspectRatio(16 / 9, contentMode: .fit)
.onAppear { player.play() }
.onDisappear { player.pause() }
}
}
You can pass an overlay closure to layer your own SwiftUI views on top, but the underlying controls stay Apple’s.
Best for: SwiftUI apps that want system controls with a little custom chrome.
3. AVPlayerLayer (custom UI)
AVPlayerLayer is a CALayer subclass that renders the player’s video and nothing else. No buttons, no scrubber, no gestures. You build all of it. Available since iOS 4.0 and macOS 10.7, though not on watchOS.
final class PlayerView: UIView {
override class var layerClass: AnyClass { AVPlayerLayer.self }
private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
func setAspectFit() {
playerLayer.videoGravity = .resizeAspect
}
}
Overriding layerClass is the pattern to use. Adding an AVPlayerLayer as a sublayer instead means resizing it by hand on every layout pass, which is where most “the video is the wrong size” bugs start.
videoGravity controls the fit. .resizeAspect letterboxes, .resizeAspectFill crops to fill, .resize stretches.
Best for: branded players, feed-style autoplay, and anything where Apple’s controls are in the way.
4. AVPlayerView (macOS)
macOS doesn’t get AVPlayerViewController. It gets AVPlayerView, an AppKit view available since macOS 10.9 with its own native playback controls. If you ship a Mac app alongside iOS, this is the branch in your code.
| Option | Platforms | Controls | Customization | Framework |
|---|---|---|---|---|
| AVPlayerViewController | iOS 8+, tvOS 9+, visionOS 1+ | System | Low | AVKit |
| VideoPlayer | iOS 14+, macOS 11+, tvOS 14+, watchOS 7+ | System | Low (overlay only) | AVKit |
| AVPlayerLayer | iOS 4+, macOS 10.7+, tvOS 9+ | None | Total | AVFoundation |
| AVPlayerView | macOS 10.9+ | System | Low | AVKit |
What AVPlayer Supports and What It Doesn’t
AVPlayer’s format support is narrower than most teams expect, and the gaps drive real architecture decisions.
Streaming protocols. AVPlayer supports HTTP Live Streaming natively and plays progressive MP4 over HTTP. It does not support MPEG-DASH. There’s no flag to turn it on and no first-party shim. If your delivery pipeline is DASH-only, you either add an HLS rendition or replace AVPlayer with a third-party engine. It also won’t play RTSP or RTMP directly, so camera and contribution feeds need repackaging before they reach the device.
Containers. MP4, MOV, M4V, M4A, and fragmented MP4 segments inside HLS. MKV and AVI aren’t supported, which is exactly why the third-party AVPlayer app exists.
Video codecs. H.264 and HEVC with hardware decode across the modern lineup, and AV1 hardware decode on newer Apple silicon starting with the A17 Pro and M3 generation. Older devices fall back to software decode for AV1 or fail outright, so an HEVC or H.264 rendition still belongs in your ladder.
Audio codecs. AAC, MP3, ALAC, FLAC, and spatial audio formats through the same pipeline.
HDR. AVPlayer exposes eligibleForHDRPlayback and availableHDRModes, so you can branch on whether the current device and display can actually show HDR before requesting an HDR rendition.
The short version: AVPlayer is an HLS player with local file support attached. Build your delivery around that and it’s excellent. Fight it and you’ll lose.
Advantages of AVPlayer
Hardware decoding by default
AVPlayer routes H.264 and HEVC through the dedicated decode block on the SoC. Battery drain and thermals stay far below any software decoder, which matters for a two-hour stream on a phone.
Adaptive bitrate without extra code
Point AVPlayer at a multi-rendition HLS playlist and it handles adaptive bitrate streaming on its own: measuring throughput, switching renditions, recovering from congestion. You write zero switching logic.
Free access to system features
AirPlay, Picture in Picture, CarPlay, Lock Screen Now Playing controls, and Spatial Audio all work through AVPlayer with configuration rather than implementation. allowsExternalPlayback and allowsAirPlayVideo are single properties.
One API across six platforms
The same player code runs on iPhone, iPad, Mac, Apple TV, Apple Watch, and Vision Pro. Only the presentation layer changes.
Native FairPlay support
AVPlayer is the only way to play FairPlay-protected content on Apple devices. AVContentKeySession, available since iOS 10.3, handles key requests and renewals for FairPlay DRM, which is a hard requirement for most premium content licensing.
Precise timing control
addPeriodicTimeObserver and addBoundaryTimeObserver give you frame-accurate hooks for captions, analytics events, chapter markers, and overlays, all synchronized to the player’s own clock rather than a wall timer.
Offline playback built in
AVAssetDownloadURLSession, available since iOS 9.0, downloads HLS streams for offline viewing with DRM intact. Background downloads, resumption, and storage management are included.
Limitations of AVPlayer
No DASH, no RTSP, no RTMP
This is the single biggest constraint. AVPlayer reads HLS and progressive HTTP. Every other protocol needs a server-side conversion step before the stream reaches an Apple device.
Teams running a DASH-based pipeline usually add an HLS output rather than replacing the player.
Asynchronous state you have to observe
Nothing about AVPlayer is synchronous. status, timeControlStatus, isPlaybackLikelyToKeepUp, and buffer ranges all change on their own schedule, and reading them at the wrong moment gives you a stale answer.
Before iOS 26, KVO was the only way to track them, which meant a pile of observer boilerplate and careful teardown.
Limited control over ABR decisions
You can cap the player with preferredPeakBitRate and preferredMaximumResolution, but you can’t replace its switching algorithm or force a specific rendition. Apps that need deterministic quality control hit this wall quickly.
Custom UI means building everything
AVPlayerLayer gives you pixels. Scrubbing, gesture handling, buffering indicators, subtitle menus, accessibility labels, and the full-screen transition are all yours to write and maintain across OS releases.
Easy to leak
Time observers and KVO observations hold strong references. Forget removeTimeObserver(_:) and player instances stay alive. A feed that creates a player per cell will run the device out of memory in a few dozen scrolls.
Sparse error messages
A failed AVPlayerItem hands you an NSError that frequently says little more than that the operation couldn’t be completed. Working out whether it’s a CORS rule, an App Transport Security block, a codec mismatch, or a malformed playlist usually means reproducing it against a known-good stream.
Now that the shape of the API is clear, the practical question is how to wire it up, and what has to exist on the server side before the player has anything to play.
How to Use AVPlayer: A Step-by-Step Guide
Step 1: Import the right frameworks
AVFoundation gives you AVPlayer, AVPlayerItem, and AVPlayerLayer. AVKit gives you AVPlayerViewController and VideoPlayer. Most apps need both.
Step 2: Configure the audio session
Skip this and your video plays silently when the ring/silent switch is off. It’s the single most reported “AVPlayer has no sound” bug.
import AVFoundation
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
try? AVAudioSession.sharedInstance().setActive(true)
The .playback category also enables background audio and Lock Screen controls, provided you’ve added the audio background mode to your app’s capabilities.
Step 3: Point the player at a stream
For a local file, pass a file URL. For streaming, pass the HLS playlist URL.
This is the step that depends on infrastructure you probably don’t want to build. To give AVPlayer something it can actually play, you need ingest, transcoding into a rendition ladder, segmenting into HLS, and CDN delivery.
A video streaming API collapses that into one call. LiveAPI takes RTMP or SRT ingest from any encoder, transcodes up to 4K with adaptive bitrate renditions, and returns a ready-to-play HLS URL delivered through Akamai, Cloudflare, and Fastly. The URL goes straight into AVPlayer(url:). No packager to run, no origin to maintain.
let url = URL(string: "https://stream.example.com/live/playlist.m3u8")!
let item = AVPlayerItem(url: url)
let player = AVPlayer(playerItem: item)
Step 4: Observe readiness before you play
Watch the item’s status so you know when playback can actually start, and watch timeControlStatus so your UI matches reality.
let statusObservation = item.observe(\.status, options: [.new]) { item, _ in
switch item.status {
case .readyToPlay: player.play()
case .failed: print("Playback failed:", item.error ?? "unknown")
default: break
}
}
On iOS 26 and later there’s a cleaner path. Setting the type property AVPlayer.isObservationEnabled to true makes new AVPlayer, AVQueuePlayer, AVPlayerItem, and AVPlayerItemTrack instances work with Swift Observation, so SwiftUI views update from player state without any KVO at all.
@main
struct StreamApp: App {
init() {
AVPlayer.isObservationEnabled = true
}
var body: some Scene {
WindowGroup { PlayerScreen() }
}
}
One rule matters here. The setting is process-wide and all-or-nothing: Apple’s documentation states that an exception is thrown if you set it to true after any of those objects have been created, or back to false once observable objects exist.
Set it in your app’s initializer, before anything touches a player.
Step 5: Track playback position
Use a periodic time observer for the scrubber and analytics, and keep the token so you can remove it.
let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
let token = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
currentSeconds = time.seconds
}
// In deinit or when tearing down the player:
player.removeTimeObserver(token)
addBoundaryTimeObserver(forTimes:queue:using:) is the better choice for firing at specific moments: ad markers, chapter boundaries, or a midpoint completion event.
Step 6: Seek accurately
The bare seek(to:) is fast but approximate, because it snaps to the nearest keyframe. When accuracy matters, pass zero tolerance.
await player.seek(to: target, toleranceBefore: .zero, toleranceAfter: .zero)
Zero-tolerance seeks force the decoder to walk from the previous keyframe, so they’re slower. Use them for frame-accurate editing, not for a scrubber the user is dragging.
Step 7: Add Picture in Picture and AirPlay
AVPlayerViewController and VideoPlayer give you both for free.
With a custom AVPlayerLayer, create an AVPictureInPictureController with that layer, and set allowsExternalPlayback on the player to enable AirPlay. Both need the audio background mode enabled.
Step 8: Tear down properly
Remove every time observer, invalidate KVO observations, call replaceCurrentItem(with: nil), and release the player.
In a scrolling feed, pool two or three player instances and swap items through them rather than creating a player per cell.
Tuning AVPlayer for Live Streaming
The defaults are tuned for on-demand video on a good connection. Live streaming needs different numbers, and AVPlayer exposes them on AVPlayerItem.
preferredPeakBitRate (iOS 8.0+) caps how much bandwidth a single item consumes, in bits per second. Set it to keep cellular viewers off your 4K rendition, or to leave headroom when several players share a connection.
preferredForwardBufferDuration (iOS 10.0+) sets how many seconds the player buffers ahead of the playhead. Larger values soak up network jitter. Smaller values cut startup delay and keep a live stream closer to the edge, though anything under a few seconds only works with low latency HLS and partial segments.
preferredMaximumResolution (iOS 11.0+) caps the rendition by pixel dimensions, which is the right knob for a small inline player that has no business pulling 1080p.
let item = AVPlayerItem(url: url)
item.preferredPeakBitRate = 2_500_000
item.preferredForwardBufferDuration = 6
item.preferredMaximumResolution = CGSize(width: 1280, height: 720)
player.replaceCurrentItem(with: item)
automaticallyWaitsToMinimizeStalling decides whether the player waits for a healthy buffer before starting. Leave it true for on-demand. For live, setting it false and calling playImmediately(atRate: 1.0) starts faster at the cost of a higher stall risk.
networkResourcePriority (iOS 26.0+) is new and useful if you run more than one player at once. It tells the system which player wins when several compete for bandwidth inside the same process, so a multiview sports app can prioritize the main feed over the thumbnails.
Segment length on the server side matters as much as any of these. Apple’s HLS authoring specification sets the rules your playlists need to follow for AVPlayer to behave predictably, including target durations and rendition ladder structure.
Streams that ignore it are the usual cause of buffering that looks like a client bug but isn’t.
The Infrastructure Behind an AVPlayer App
AVPlayer is the last five percent of a video feature. Here’s what has to exist upstream of it.
Ingest
Live sources speak RTMP or SRT, and IP cameras speak RTSP. AVPlayer speaks none of them. You need an ingest endpoint that accepts contribution feeds and hands them to a transcoder.
Transcoding and packaging
One source has to become a ladder of renditions, each segmented into HLS with a matching playlist. Get the ladder wrong (bad bitrate spacing, mismatched keyframes, no low rendition) and you’ll see stalls that look like player bugs.
CMAF packaging lets one set of segments serve both HLS and DASH, which helps if you also ship on Android and the web.
Delivery
A CDN close to your viewers, with enough capacity for peaks. Single-CDN setups fail regionally in ways that show up as buffering on one carrier and nowhere else.
A streaming backend, or an API
Ingest, transcoding, packaging, storage, and multi-CDN delivery take a competent team six to nine months to build, and the maintenance never stops. Using a video API takes days.
LiveAPI handles the whole chain behind the player: RTMP and SRT ingest, instant encoding so videos are playable seconds after upload, adaptive bitrate HLS output, three CDN partners for global delivery, and live to VOD recording that turns a finished stream into an on-demand asset automatically.
Your iOS code stays the same three lines (AVPlayerItem, AVPlayer, play()) and the pipeline behind the URL becomes someone else’s operational problem. Pricing is pay-as-you-grow on streaming minutes, so a prototype costs prototype money.
Protection and monetization
Content that needs locking down needs multi-DRM planning: FairPlay for Apple, Widevine and PlayReady elsewhere.
Content that needs ads pairs server-side ad insertion with AVPlayerInterstitialEventController (iOS 15.0+) to schedule interstitials without breaking the main timeline.
Captions
HLS carries subtitle renditions, and AVPlayer selects between them through AVMediaSelectionGroup and the media selection criteria properties. Choosing between closed captions and subtitles changes what you author, not how the player reads it.
Is AVPlayer Right for Your Project?
Use AVPlayer if:
- You’re shipping on Apple platforms and want hardware decoding and system integration for free
- Your content is delivered over HLS, or you can add an HLS output
- You need FairPlay DRM, AirPlay, Picture in Picture, or offline downloads
- You want the system player UI and don’t need custom controls
- Your team writes Swift and wants one playback API across iPhone, iPad, Mac, Apple TV, and Vision Pro
Look elsewhere if:
- Your delivery is DASH-only and adding HLS isn’t an option
- You need sub-second latency, which usually means WebRTC rather than any HLS-based player
- You need to replace the ABR algorithm with your own
- You’re playing MKV, AVI, or other containers Apple doesn’t decode
Cross-platform teams usually land on AVPlayer for iOS, ExoPlayer for Android, and hls.js or Shaka Player for the web. Three players, one HLS pipeline.
React Native video libraries wrap AVPlayer on the iOS side anyway, so everything here still applies underneath.
Common AVPlayer Problems and How to Fix Them
Video plays but there’s no sound
The audio session category is wrong. Set .playback before playing, or the ring/silent switch mutes you. Check isMuted and volume on the player too.
Black screen, audio works
No presentation layer, or the layer has a zero frame. With AVPlayerLayer, confirm you overrode layerClass. A manually added sublayer with no resize logic collapses to nothing.
play() does nothing
You called it before the item reached .readyToPlay. Observe status and start playback from the callback.
Stream loads on Wi-Fi, fails on cellular
Usually a bitrate ladder with no low rendition, or App Transport Security blocking an HTTP URL. Serve everything over HTTPS and include a rendition under 1 Mbps.
Playback stops when the app backgrounds
Enable the audio background mode, set the .playback session category, and check audiovisualBackgroundPlaybackPolicy (iOS 15.0+) if you want video to keep going rather than pause.
Memory climbs while scrolling a feed
A player per cell, plus time observers that were never removed. Pool players, remove observers in deinit, and null out currentItem when a cell goes off screen.
Seeking is slow
Zero-tolerance seeks decode from the previous keyframe. Use default tolerances for scrubbing, and shorten the keyframe interval in your encoding settings if precise seeks matter.
AVPlayer FAQ
What is AVPlayer?
AVPlayer is a class in Apple’s AVFoundation framework that controls playback and timing for a single media asset, local or remote. It manages play, pause, seek, rate, and buffering, and pairs with AVPlayerLayer, AVPlayerViewController, or SwiftUI’s VideoPlayer to put video on screen.
What’s the difference between AVAudioPlayer and AVPlayer?
AVAudioPlayer plays local audio files only and can’t stream. AVPlayer plays both audio and video, local and remote, including HLS streams. Use AVAudioPlayer for bundled sound effects and AVPlayer for anything over the network.
Does AVPlayer support DASH?
No. AVPlayer supports HTTP Live Streaming and progressive HTTP playback, not MPEG-DASH. To reach Apple devices from a DASH pipeline, add an HLS output. CMAF packaging lets both share the same segments.
Does AVPlayer support RTSP or RTMP?
Not directly. Both need server-side conversion to HLS before an Apple device can play them. A streaming API that accepts RTMP, SRT, or RTSP ingest and returns an HLS URL handles this for you.
What file formats does AVPlayer support?
MP4, MOV, M4V, M4A, and fragmented MP4 segments, with H.264, HEVC, and AV1 video (AV1 in hardware on A17 Pro and M3 class chips and newer) plus AAC, MP3, ALAC, and FLAC audio. MKV and AVI aren’t supported.
How do I loop a video with AVPlayer?
Use AVPlayerLooper (iOS 10.0+) with an AVQueuePlayer for gapless looping. The older approach of observing AVPlayerItemDidPlayToEndTime and seeking to zero works, but it shows a visible hitch at the loop point.
How do I play a video in SwiftUI?
Use VideoPlayer(player:) from AVKit, available since iOS 14.0. For custom controls, wrap an AVPlayerLayer-backed UIView in a UIViewRepresentable. On iOS 26 and later, set AVPlayer.isObservationEnabled = true at app launch so player state drives SwiftUI updates through Swift Observation.
Can AVPlayer play DRM-protected video?
Yes, FairPlay Streaming through AVContentKeySession (iOS 10.3+). It’s the only DRM system Apple devices accept for protected playback, so premium content generally needs FairPlay alongside Widevine and PlayReady for other platforms.
How do I reduce AVPlayer buffering?
Cap bandwidth with preferredPeakBitRate, tune preferredForwardBufferDuration for your latency target, and cap resolution with preferredMaximumResolution on small players. Most buffering is a server-side problem though, so check your rendition ladder and CDN before tuning the client.
Is AVPlayer thread-safe?
Treat it as main-thread-only. Create players, mutate properties, and attach presentation layers on the main queue, and pass .main as the queue for time observers whose callbacks touch UI.
Building on AVPlayer
AVPlayer is a well-built playback controller with one strong opinion: it wants HLS.
Accept that, get your rendition ladder right, observe state instead of assuming it, and tear down observers properly. It’ll handle hardware decoding, adaptive bitrate, AirPlay, Picture in Picture, and FairPlay without much help from you.
The part that actually takes months isn’t the player. It’s everything that produces the URL you hand it.
Ready to give AVPlayer something to play? LiveAPI delivers ingest, 4K adaptive bitrate encoding, HLS output, and multi-CDN delivery through one API, so your iOS app can go live in days instead of months. Get started with LiveAPI.


