Video thumbnails are a cornerstone of modern media-rich applications, offering users a quick visual preview without needing to play the full video. In iOS development, AVFoundation provides the most performant and flexible tools for extracting thumbnail images. This article walks through building a custom video thumbnail generator that can handle real-world scenarios—including local and remote files, caching, error handling, and performance tuning—using Swift and AVFoundation.

Understanding AVFoundation’s Role in Thumbnail Generation

AVFoundation is Apple’s primary framework for working with audiovisual media. For thumbnail generation, the key classes are AVAsset and AVAssetImageGenerator.

  • AVAsset: Represents a timed audiovisual asset (a local or remote video). It contains track information, duration, and metadata.
  • AVAssetImageGenerator: Provides methods to obtain CGImageRefs from the asset at specific time intervals. It respects video orientation, applies color space adjustments, and supports asynchronous bulk generation.

Using these classes directly gives you full control over image size, time accuracy, and transformation, unlike UIKit shortcuts that often hide configuration options.

Setting Up the Project

Start by importing AVFoundation into your Swift file:

import AVFoundation

You also need UIKit to display the generated images. Create an AVAsset from a URL. For local files, use URL(fileURLWithPath:). For remote videos, use URL(string:) and ensure the asset can be initialized:

// Local
let localURL = Bundle.main.url(forResource: "sample", withExtension: "mp4")!
let asset = AVAsset(url: localURL)

// Remote
let remoteURL = URL(string: "https://example.com/video.mp4")!
let remoteAsset = AVAsset(url: remoteURL)

Generating a Single Thumbnail

To generate one thumbnail at a specified time, create an AVAssetImageGenerator and call copyCGImage(at:actualTime:). This method is synchronous and should be run off the main thread to avoid blocking the UI.

let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true

let time = CMTime(seconds: 2.0, preferredTimescale: 600) // 2 seconds into the video

do {
    let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
    let thumbnail = UIImage(cgImage: cgImage)
    // Update UI on main thread
    DispatchQueue.main.async {
        imageView.image = thumbnail
    }
} catch {
    print("Thumbnail generation failed: \(error.localizedDescription)")
}

Important: Setting appliesPreferredTrackTransform to true ensures the resulting image respects the video’s rotation metadata (portrait vs. landscape). Without this, an upright video might appear sideways.

Choosing the Right Time

Thumbnails are often taken at the beginning of a video, but consider using a time that represents the content—e.g., 10% into the duration. To get a meaningful time point, query the asset’s duration:

let duration = asset.duration
let midTime = CMTimeMultiplyByFloat64(duration, multiplier: 0.1)

Handling Errors and Edge Cases

Thumbnail generation can fail for several reasons. Always handle errors gracefully and provide fallback UI.

  • Invalid asset: The video may be corrupted or the URL unreachable.
  • Missing video track: Some assets contain only audio; check asset.tracks(withMediaType: .video).
  • Time out of bounds: Ensure the requested time is between CMTime.zero and the asset’s duration.
  • Network errors: Remote assets may fail to load. Use AVAsset.loadValuesAsynchronously before generating thumbnails.

Example of preloading asset properties:

let assetKeys = ["tracks", "duration", "playable"]
asset.loadValuesAsynchronously(forKeys: assetKeys) {
    var error: NSError?
    let status = asset.statusOfValue(forKey: "tracks", error: &error)
    if status == .loaded {
        // Safe to generate thumbnails
    } else {
        // Handle error
    }
}

Customizing Thumbnail Output

AVAssetImageGenerator offers several properties to fine-tune the output image.

Maximum Size

Restrict the generated image to a specific bounding box using maximumSize. This reduces memory overhead and speeds up generation for large videos.

imageGenerator.maximumSize = CGSize(width: 640, height: 480)

The generated image will be scaled to fit within these dimensions while preserving aspect ratio.

Requested Time Tolerance

By default, copyCGImage returns the exact frame at the requested time (or as close as possible). You can trade accuracy for speed by setting requestedTimeToleranceBefore and requestedTimeToleranceAfter. For thumbnails, a tolerance of 0.5 seconds is often acceptable and faster:

imageGenerator.requestedTimeToleranceBefore = CMTime(seconds: 0.5, preferredTimescale: 600)
imageGenerator.requestedTimeToleranceAfter = CMTime(seconds: 0.5, preferredTimescale: 600)

Aperture Mode

apertureMode controls how the generated image handles clean aperture vs. production aperture. Use .cleanAperture to remove edge dust and produce a cleaner thumbnail:

imageGenerator.apertureMode = .cleanAperture

Applying Transformations

If you need to rotate or flip the image (e.g., for a specific orientation), use Core Graphics transforms. One approach is to capture the raw image and then apply a CGAffineTransform manually. Alternatively, you can set appliesPreferredTrackTransform to true (as shown) and trust the asset’s natural orientation.

Generating Multiple Thumbnails Efficiently

For video grids, timeline scrubbing, or storyboard previews, you often need dozens of thumbnails. Avoid calling copyCGImage in a loop on the main thread. Instead, use the asynchronous method generateCGImagesAsynchronously(forTimes:completionHandler:). It processes multiple time requests efficiently, leveraging internal parallelism.

var times: [NSValue] = []
for i in 0..<10 {
    let time = CMTime(seconds: Double(i) * 2.0, preferredTimescale: 600)
    times.append(NSValue(time: time))
}

imageGenerator.generateCGImagesAsynchronously(forTimes: times) { requestedTime, cgImage, actualTime, result, error in
    if result == .succeeded, let cgImage = cgImage {
        let thumbnail = UIImage(cgImage: cgImage)
        DispatchQueue.main.async {
            // Append to array or update collection view cell
        }
    } else {
        print("Failed at \(requestedTime): \(error?.localizedDescription ?? "unknown")")
    }
}

This method scales well and lets you update the UI incrementally. Remember to call cancelAllCGImageGeneration() if the user leaves the screen to free resources.

Working with Remote Videos

When the video URL points to a remote server, you must wait for the asset to be fully loaded or at least its track information to be available. Use AVURLAsset with appropriate caching headers to avoid repeated downloads.

let urlAsset = AVURLAsset(url: remoteURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
urlAsset.loadValuesAsynchronously(forKeys: ["tracks"]) {
    // Then create image generator and proceed
}

For production apps, consider caching thumbnails on disk so that repeated access to the same video does not re‑fetch the image. Use NSCache or file‑based cache using the video URL as a key.

Caching Strategy Example

class ThumbnailCache {
    static let shared = ThumbnailCache()
    private let cache = NSCache<NSString, UIImage>()
    
    func thumbnail(for url: URL) -> UIImage? {
        return cache.object(forKey: url.absoluteString as NSString)
    }
    
    func setThumbnail(_ image: UIImage, for url: URL) {
        cache.setObject(image, forKey: url.absoluteString as NSString)
    }
}

Combine this with the image generator to avoid redundant work.

Performance Considerations

Generating thumbnails is a CPU‑intensive operation, especially for high‑resolution 4K videos. Follow these best practices:

  • Always perform synchronous calls on a background queue (e.g., DispatchQueue.global(qos: .userInitiated)).
  • Set a reasonable maximumSize to reduce the output image dimensions. A size of 320x240 is often sufficient for list thumbnails.
  • Use requestedTimeTolerance to allow the generator to skip the nearest frame instead of solving the exact decode.
  • Cancel any in‑progress asynchronous generation when the view disappears.
  • For display in a collection view, generate thumbnails at the exact cell size and avoid resizing on the main thread.

Integrating with Swift Concurrency (Async/Await)

If your app targets iOS 15+, you can wrap the blocking call in a continuation for clean async/await code:

func generateThumbnail(forVideoAt url: URL, at time: CMTime) async throws -> UIImage {
    let asset = AVAsset(url: url)
    let imageGenerator = AVAssetImageGenerator(asset: asset)
    imageGenerator.appliesPreferredTrackTransform = true
    imageGenerator.maximumSize = CGSize(width: 640, height: 480)
    
    return try await withCheckedThrowingContinuation { continuation in
        let times = [NSValue(time: time)]
        imageGenerator.generateCGImagesAsynchronously(forTimes: times) { _, cgImage, _, result, error in
            if let cgImage = cgImage, result == .succeeded {
                let image = UIImage(cgImage: cgImage)
                continuation.resume(returning: image)
            } else {
                continuation.resume(throwing: error ?? ThumbnailError.generationFailed)
            }
        }
    }
}

Advanced Customizations

Thumbnails with Video Composition

If your video uses an AVVideoComposition (e.g., watermark overlay, color adjustments), you can pass it to the image generator:

imageGenerator.videoComposition = myVideoComposition

The generated thumbnail will reflect the composited output.

Generating Thumbnails for Live Photos

Live Photos are a combination of a video and a still image. To extract a thumbnail from the underlying video, use the PHAsset and request a video URL before feeding it into AVFoundation.

Accessibility and User Experience

Thumbnails enhance visual scanning, but always provide a text alternative for VoiceOver users. Use the accessibilityLabel on the image view, describing the video content if possible.

imageView.isAccessibilityElement = true
imageView.accessibilityLabel = "Thumbnail of video: \(videoTitle)"

External Resources

For deeper understanding, consult the following:

Conclusion

Building a custom video thumbnail generator with AVFoundation gives you complete freedom over image quality, timing, and performance. By leveraging AVAssetImageGenerator both synchronously and asynchronously, handling errors, and caching results, you can deliver a seamless user experience in your iOS app. Whether you are processing local files or remote videos, the techniques covered here will help you produce crisp, context‑aware thumbnails quickly and efficiently.