When do you use AVAssetReader instead of a player, and what do the output settings change?
Predict first
What do the reads return?
try await makeVideo(at: clip, frames: 30, fps: 30)
defer { try? FileManager.default.removeItem(at: clip) }
let asset = AVURLAsset(url: clip)
let track = try await asset.loadTracks(withMediaType: .video)[0]
let bgra = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
func readAll(range: CMTimeRange? = nil, settings: [String: Any]?) throws -> (frames: Int, first: CMSampleBuffer?) {
let reader = try AVAssetReader(asset: asset)
if let range { reader.timeRange = range }
let output = AVAssetReaderTrackOutput(track: track, outputSettings: settings)
reader.add(output)
reader.startReading()
var count = 0, first: CMSampleBuffer?
while let sample = output.copyNextSampleBuffer() { if first == nil { first = sample }; count += 1 }
return (count, first)
}
let all = try readAll(settings: bgra)
let pixels = CMSampleBufferGetImageBuffer(all.first!)!
print("decoded frames:", all.frames, "| BGRA pixels:", CVPixelBufferGetPixelFormatType(pixels) == kCVPixelFormatType_32BGRA,
"| size:", CVPixelBufferGetWidth(pixels), "x", CVPixelBufferGetHeight(pixels))
let window = CMTimeRange(start: CMTime(seconds: 0.5, preferredTimescale: 600), duration: CMTime(seconds: 0.2, preferredTimescale: 600))
print("frames with timeRange 0.5s..0.7s:", try readAll(range: window, settings: bgra).frames)
let compressed = try readAll(settings: nil)
print("outputSettings nil: samples are still compressed, so no image buffer:", CMSampleBufferGetImageBuffer(compressed.first!) == nil)
- 30 BGRA 64x64 frames, 6 frames for a 0.2 s range, and nil image buffers when settings are nil
- 0 frames because the reader needs a player
- compressed samples have an image buffer
Mental model. A player is a theatre showing the film at film speed; a reader is a clerk flipping through the reels as fast as they can for an inventory.
A player renders in real time for display; AVAssetReader pulls samples as fast as you ask for processing, analysis or transcoding. Add an AVAssetReaderTrackOutput per track, call startReading(), then loop copyNextSampleBuffer() until it returns nil and check reader.status. With outputSettings naming BGRA the check decoded all 30 frames into 64×64 pixel buffers; with a timeRange of 0.5…0.7 s it produced 6 frames; with outputSettings: nil the samples stay compressed, so CMSampleBufferGetImageBuffer is nil (that is the passthrough route for remuxing). One reader reads one pass, forwards only: create a new one to start again, and keep readers on a background queue.
Pick the pixel format for the consumer: BGRA is convenient for Core Graphics and Core Image but larger than the native YUV, so ask for the biplanar 4:2:0 format when feeding Metal or a writer (see the pixel-buffer question) and release sample buffers promptly or memory climbs. A reader does not play audio and does no drift correction; the pairing for edits is reader → processing → AVAssetWriter, with compositions (Q389) for timeline edits. Not run: audio readers, HEVC, and very long assets.
Output settings choose decoded or compressed samples.
Common traps
- Reusing a finished reader. It reads once; make another.
- Holding every sample buffer. Decoded frames are large.
They might follow up with
- How do you read a range? Set
timeRangebeforestartReading.
Related questions
- 18.9.2 Why does AVAssetImageGenerator return a frame at a different time than you asked for, and how do you get the…
- 18.9.5 How is a CVPixelBuffer laid out, why are rows padded, and how do pools limit memory?
- 18.1.2 How do AVMutableComposition, tracks and time ranges work for editing, and how do you export the result?
- 18.3.2 What are CVPixelBuffer, CMSampleBuffer and IOSurface, and how do you process frames without copying them?
← Why does AVAssetImageGenerator return a frame at a different time… How does CMTime avoid rounding errors, and how can sloppy conversion… →