civil-and-structural-engineering
Creating a Custom Video Thumbnail Generator in Ios with Avfoundation
Table of Contents
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.zeroand the asset’s duration. - Network errors: Remote assets may fail to load. Use
AVAsset.loadValuesAsynchronouslybefore 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
maximumSizeto reduce the output image dimensions. A size of 320x240 is often sufficient for list thumbnails. - Use
requestedTimeToleranceto 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:
- AVAssetImageGenerator Official Documentation
- AVAsset Reference
- WWDC 2019: AVFoundation Performance Tips
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.