How do AVAsset, AVPlayerItem and AVPlayer fit together, what is CMTime, and why does playback “not start”?
Predict first
What does a time of 2.5 seconds look like as a CMTime, and how does adding and comparing work?
import CoreMedia
let t = CMTime(seconds: 2.5, preferredTimescale: 600)
print(t.value, t.timescale, CMTimeGetSeconds(t))
let sum = CMTimeAdd(t, CMTime(value: 300, timescale: 600))
print(sum.value, CMTimeGetSeconds(sum), CMTimeCompare(t, sum), t < sum)
- 1500 600 2.5, then 1800 3.0 -1 true
- 2.5 1 2.5, then 3.0 1 0 false
- 1500 600 2.5, then 1800 3.0 1 false
Predict first
A fresh player with a stream item: what are its initial status, rate and waiting behaviour?
import AVFoundation
let item = AVPlayerItem(url: URL(string: "https://example.test/x.m3u8")!)
let player = AVPlayer(playerItem: item)
print(item.status.rawValue, player.rate, player.timeControlStatus.rawValue, player.automaticallyWaitsToMinimizeStalling)
- 0 0.0 0 true
- 1 1.0 2 false
- 0 1.0 0 true
Mental model. The asset is the film canister, the item is the reel being threaded (with a note on how far it has run), and the player is the projector. The projector can be switched on before the film is threaded, but nothing shows until the reel is ready.
Three objects and a clock. An AVAsset (AVURLAsset) is the static description of media: its tracks, duration, metadata; it loads properties asynchronously (try await asset.load(.duration, .tracks)) so you never block on the network. An AVPlayerItem is the dynamic state of playing one asset: the current time, buffered ranges, status (unknown 0, readyToPlay 1, failed 2), the error, and options such as preferredForwardBufferDuration (0 means “let the system decide”). An AVPlayer plays one item at a time (AVQueuePlayer a queue of them): play(), pause(), seek(to:), and rate (0 paused, 1 normal speed). Create AVPlayer(url:) or AVPlayer(playerItem:), and show it with an AVPlayerLayer or the system AVPlayerViewController (SwiftUI VideoPlayer), which gives you controls, Picture in Picture and AirPlay for free. A new item’s status is unknown until the item has loaded enough to decide: observe it (KVO or the async values(for:) sequence) and only act on readyToPlay; calling play() earlier is fine (it sets intent) but seeking and reading duration before ready gives invalid values. Also observe timeControlStatus (paused 0, waitingToPlayAtSpecifiedRate 1, playing 2) and its reasonForWaitingToPlay to show a spinner on stalls, and note automaticallyWaitsToMinimizeStalling is true by default (the player waits for enough buffer before starting).
CMTime is a rational number: a value and a timescale (ticks per second). CMTime(seconds: 2.5, preferredTimescale: 600) is 1500/600. Rationals avoid the drift of floating point (a 30 fps frame is exactly 1/30 s, and frames add without rounding), and 600 is the conventional timescale because it divides evenly by 24, 25 and 30 fps. Compare and combine with CMTimeAdd, CMTimeCompare (or the operators <, +), convert with CMTimeGetSeconds, and watch for the special values .zero, .invalid and .indefinite (check isValid before use: arithmetic on an invalid time is invalid). A CMTimeRange is a start and a duration with an end and containsTime. Track progress with addPeriodicTimeObserver(forInterval:queue:using:) (and keep and remove the returned token, or it leaks), and observe AVPlayerItem.didPlayToEndTimeNotification for the end. Common reasons playback does not start: the item is not ready (still loading), the audio session category blocks it (see the audio session question), the URL needs ATS-compliant HTTPS or a valid HLS manifest, a DRM or auth failure (check item.error and the error log), the network stalled (a waiting status), the player is not retained (it deallocates and stops), or the app is in the background without the audio background mode.
A rational stores 1500 ticks at 600 per second. Adding 300 ticks gives 1800 ticks, which is 3 seconds, and the earlier time compares as less (−1).
Nothing has loaded and nothing is playing: status unknown (0), rate 0, paused (0), and the player waits to avoid stalls by default.
Common traps
- Reading duration or seeking right after creating the item. The item is not ready; wait for
readyToPlayor load the property asynchronously. - Keeping time as
Doubleseconds across many operations. Rounding drifts; keepCMTimerationals and convert at the edges.
They might follow up with
- What does the status of a new
AVPlayerItemshow? Unknown (0) until it is ready or failed. - Why 600 as a timescale? It divides evenly by common frame rates (24, 25, 30).
Related questions
- 18.1.2 How do AVMutableComposition, tracks and time ranges work for editing, and how do you export the result?
- 18.2.1 What is the AVAudioSession, how do categories and modes decide behaviour, and how do you handle interruptions…
- 18.4.1 How does HLS adaptive streaming work, why do stalls happen, and what does FairPlay add?
How do AVMutableComposition, tracks and time ranges work for editing… →