How do you read what is in a media file, and why are the loads async and throwing?
Predict first
What does the asset report?
try await makeVideo(at: clip, frames: 30, fps: 30)
defer { try? FileManager.default.removeItem(at: clip) }
let asset = AVURLAsset(url: clip)
let (duration, isPlayable, tracks) = try await asset.load(.duration, .isPlayable, .tracks)
print("playable:", isPlayable, "| tracks:", tracks.count, "| duration:", duration.seconds, "s")
let video = try await asset.loadTracks(withMediaType: .video)[0]
let (size, rate, transform, formats) = try await video.load(.naturalSize, .nominalFrameRate, .preferredTransform, .formatDescriptions)
print("naturalSize:", size, "| nominalFrameRate:", rate, "| identity transform:", transform.isIdentity)
print("codec is H.264:", CMFormatDescriptionGetMediaSubType(formats[0]) == kCMVideoCodecType_H264)
print("estimated data rate is positive:", try await video.load(.estimatedDataRate) > 0)
let missing = AVURLAsset(url: URL(fileURLWithPath: "/no/such/file.mov"))
do { _ = try await missing.load(.duration) } catch { print("loading a missing file throws instead of returning zero") }
- playable, 1 track, 1.0 s, 64x64, 30 fps, identity transform, H.264, and a missing file throws
- a missing file reports 0 s
- the frame rate is 0 until playback starts
Mental model. Reading the label on a tape before you press play, rather than starting the tape to find out how long it is.
An AVURLAsset is a lazy description: you ask for properties with try await asset.load(.duration, .isPlayable, .tracks), and for per-track facts with track.load(.naturalSize, .nominalFrameRate, .preferredTransform, .formatDescriptions). The check wrote a 30-frame H.264 clip with AVAssetWriter and read back: playable, one track, 1.0 s, 64×64, 30 fps, identity transform, codec H.264 from the format description’s subtype, and a positive estimated data rate. Loading is async because the data may be remote or the container large, and it throws because the file may be missing or unreadable: a missing path throws rather than reporting a zero duration. The old synchronous asset.duration properties are deprecated for that reason.
Use preferredTransform before trusting naturalSize: a portrait phone video is stored landscape with a rotation transform, so the displayed size swaps width and height. Read formatDescriptions for codec, colour tags and dimensions, and isPlayable / isExportable before offering actions. Writing the clip itself needs a pixel-buffer adaptor with buffers from its pool and a loop that respects isReadyForMoreMediaData (shown in the hidden helper). Not run: remote, HLS and protected assets, and hardware encoders on device.
Asset properties load asynchronously and failures throw.
Common traps
- Using
naturalSizewithout the transform. Portrait video reports landscape. - Calling the deprecated sync accessors. They can block on I/O.
They might follow up with
- How do you load several properties together? Pass them all to one
loadcall to batch the work.
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.1.1 How do AVAsset, AVPlayerItem and AVPlayer fit together, what is CMTime, and why does playback “not start”?
- 18.1.2 How do AVMutableComposition, tracks and time ranges work for editing, and how do you export the result?
Why does AVAssetImageGenerator return a frame at a different time… →