I’ve been working on a video streaming macOS app for a while, and I need to find a way to get the thumbnail of a bunch of videos. Just like many other things, there are multiple ways of achieving this. So in this post I will show you the two solutions I found and do some comparison.

AVAssetImageGenerator

Well Cocoa does offer a native API for this job. Using AVAssetImageGenerator we can easily get the thumbnail of a video:

1
2
3
4
5
6
7
8
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:@"http://www.html5videoplayer.net/videos/toystory.mp4"] options:nil];
AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
generator.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMakeWithSeconds(asset.duration.value/2, 600); // half point of the video
generator.maximumSize = CGSizeMake(320, 180);
[generator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:time]] completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
UIImage *img = [UIImage imageWithCGImage:image]; // now we have the UIImage
}];

qlmanage

qlmanage is the CLI utility for QuickLook in macOS (here‘s the man page). The command we are using is qlmanage -t, as described in the man page:

qlmanage -t displays the Quick Look generated thumbnails (if available) for the specified files

So we can use it to get a video thumbnail like this:

1
qlmanage -ti /path/to/video.mp4 -o /path/to/destination/

A above command will save a file named video.mp4.png to the destination folder we specified.

To use it in our Cocoa app, we need to wrap it as an NSTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)thumbnailWithFilePath: (NSString *) filePath andHandler: (void (^)(NSError *error, NSImage *img)) handler{
NSTask *task = [NSTask new];
task.launchPath = @"/usr/bin/qlmanage";
task.arguments = @[@"-ti", filePath, @"-s", @"320", @"-o", [filePath stringByDeletingLastPathComponent]];
task.standardOutput = nil;
task.terminationHandler = ^(NSTask *t){
NSString *imgPath = [filePath stringByAppendingPathExtension:@"png"];
// check the output png file exists
if (![[NSFileManager defaultManager] fileExistsAtPath:imgPath]){
NSError *err = ...; // generate your own NSError for this
handler(err, nil);
return;
}
NSImage *img = [[NSImage alloc] initWithContentsOfFile:imgPath];
// delete the generated png file
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:imgPath error:&error];
if (error){
handler(error, img);
return;
}
handler(nil, img);
};
[task launch];
}

Comparison

I download Rick Astley - Never Gonna Give You Up from YouTube as a .mp4 file for testing. Its file size is 47.3MB.

1
2
3
4
5
6
[self thumbnailWithFilePath:@"/Users/alex_ling/Desktop/Rick Astley - Never Gonna Give You Up.mp4" andHandler:^(NSError *error, NSImage *img) {
if (error) {
NSLog(@"error: %@", error);
}
_imgView.image = img;
}];

I implemented two versions of thumbnailWithFilePath: andHandler: using the two methods mentioned above.

AVAssetImageGenerator version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)thumbnailWithFilePath: (NSString *) filePath andHandler: (void (^)(NSError *error, NSImage *img)) handler {
NSURL *URL = [[NSURL alloc] initFileURLWithPath:filePath];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:URL options:nil];
AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
generator.appliesPreferredTrackTransform = YES;
generator.maximumSize = CGSizeMake(320, 320); // make sure the thumbnail size in both version are the same
CMTime time = CMTimeMakeWithSeconds(asset.duration.value/2, 600); // 1/2 point of the video
[generator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:time]] completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
if (result != AVAssetImageGeneratorSucceeded && error){
handler(error, nil);
return;
}
NSImage *img = [[NSImage alloc] initWithCGImage:image size:NSZeroSize];
handler(nil, img);
}];
}

qlmanage version (same of above):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)thumbnailWithFilePath: (NSString *) filePath andHandler: (void (^)(NSError *error, NSImage *img)) handler{
NSTask *task = [NSTask new];
task.launchPath = @"/usr/bin/qlmanage";
task.arguments = @[@"-ti", filePath, @"-s", @"320", @"-o", [filePath stringByDeletingLastPathComponent]];
task.standardOutput = nil;
task.terminationHandler = ^(NSTask *t){
NSString *imgPath = [filePath stringByAppendingPathExtension:@"png"];
if (![[NSFileManager defaultManager] fileExistsAtPath:imgPath]){
NSError *err = [NSError errorWithDomain:@"com.hkalexling.air-stream.utility" code:200 userInfo:@{@"error": @"Thumbnail Generation Failed"}];
handler(err, nil);
return;
}
NSImage *img = [[NSImage alloc] initWithContentsOfFile:imgPath];
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:imgPath error:&error];
if (error){
handler(error, img);
return;
}
handler(nil, img);
};
[task launch];
}

And here are the thumbnails generated by the two methods:

AVAssetImageGenerator thumbnail:

qlmanage thumbnail:

With above examples, we can instantly see their pros and cons.

AVAssetImageGenerator generate thumbnail without the annoying black border, but since we need to specific the time of the thumbnail, we might be getting thumbnail that doesn’t look so good, like the example above.

On the other hand, with qlmanage you are pretty much guaranteed to get a nice and clear thumbnail every time, but they always come with the black border and shadow.

What about the efficiency of the two solutions? Using two clever macros provided in this SO answer, I did a mini benchmark:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define TICK NSDate *startTime = [NSDate date]
#define TOCK NSLog(@"Time: %f", -[startTime timeIntervalSinceNow])
- (void)viewDidLoad {
[super viewDidLoad];
TICK;
[self thumbnailWithFilePath:@"/Users/alex_ling/Desktop/Rick Astley - Never Gonna Give You Up.mp4" andHandler:^(NSError *error, NSImage *img) {
if (error) {
NSLog(@"error: %@", error);
}
TOCK;
_imgView.image = img;
}];
}

And here’s the result:

  • AVAssetImageGenerator: 0.344054s
  • qlmanage: 0.891336s

So we see that qlmanage is much slower than AVAssetImageGenerator. I think it’s due to the fact that qlmanage version involves writing the PNG file and deleting it.

Conclusion

At the end I used the qlmanage method in my app, since most of the time it produces much better thumbnail than using AVAssetImageGenerator. However if efficiency is your main concern, I would recommend using AVAssetImageGenerator.