How do you read what is in a media file, and why are the loads async and throwing?

18.9.1 · L3 · Interview

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 naturalSize without 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 load call 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… →