/* HBPreviewGenerator.m $
This file is part of the HandBrake source code.
Homepage: .
It may be used under the terms of the GNU General Public License. */
//
#import "HBPreviewGenerator.h"
@import HandBrakeKit;
@interface HBPreviewGenerator ()
@property (nonatomic, readonly, weak) HBCore *scanCore;
@property (nonatomic, readonly, strong) HBJob *job;
@property (nonatomic, readonly) NSCache *previewsCache;
@property (nonatomic, readonly) NSCache *smallPreviewsCache;
@property (nonatomic, readonly) dispatch_queue_t queue;
@property (nonatomic, readonly) dispatch_group_t group;
@property (nonatomic, readonly) _Atomic bool invalidated;
@property (nonatomic, strong) HBCore *core;
@property (nonatomic) BOOL reloadInQueue;
@end
@implementation HBPreviewGenerator
- (instancetype)init
{
@throw nil;
}
- (instancetype)initWithCore:(HBCore *)core job:(HBJob *)job
{
self = [super init];
if (self)
{
_scanCore = core;
_job = job;
_previewsCache = [[NSCache alloc] init];
// Limit the cache to 60 1080p previews, the cost is in pixels
_previewsCache.totalCostLimit = 60 * 1920 * 1080;
_smallPreviewsCache = [[NSCache alloc] init];
_smallPreviewsCache.totalCostLimit = 60 * 320 * 180;
_imagesCount = [_scanCore imagesCountForTitle:self.job.title];
_queue = dispatch_queue_create("fr.handbrake.PreviewQueue", DISPATCH_QUEUE_SERIAL);
_group = dispatch_group_create();
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imagesSettingsDidChange) name:HBPictureChangedNotification object:job.picture];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imagesSettingsDidChange) name:HBFiltersChangedNotification object:job.filters];
}
return self;
}
- (void)dealloc
{
_invalidated = true;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSRunLoop mainRunLoop] cancelPerformSelectorsWithTarget:self];
[self.core cancelEncode];
}
#pragma mark -
#pragma mark Preview images
/**
* Returns the picture preview at the specified index
*
* @param index picture index in title.
*/
- (CGImageRef) copyImageAtIndex: (NSUInteger) index shouldCache: (BOOL) cache
{
if (index >= self.imagesCount)
{
return nil;
}
// The preview for the specified index may not currently exist, so this method
// generates it if necessary.
CGImageRef theImage = (__bridge CGImageRef)([_previewsCache objectForKey:@(index)]);
if (!theImage)
{
HBFilters *filters = self.job.filters;
BOOL deinterlace = (![filters.deinterlace isEqualToString:@"off"]);
theImage = (CGImageRef)[self.scanCore copyImageAtIndex:index
forTitle:self.job.title
pictureFrame:self.job.picture
deinterlace:deinterlace
rotate:self.job.filters.rotate
flipped:self.job.filters.flip];
if (cache && theImage)
{
// The cost is the number of pixels of the image
NSUInteger previewCost = CGImageGetWidth(theImage) * CGImageGetHeight(theImage);
[self.previewsCache setObject:(__bridge id)(theImage) forKey:@(index) cost:previewCost];
}
}
else
{
CFRetain(theImage);
}
return theImage;
}
/**
* Purges all images from the cache. The next call to imageAtIndex: will cause a new
* image to be generated.
*/
- (void) purgeImageCache
{
[self.previewsCache removeAllObjects];
}
- (CGSize)imageSize
{
return CGSizeMake(self.job.picture.displayWidth, self.job.picture.height);
}
- (void)imagesSettingsDidChange
{
// Purge the existing picture previews so they get recreated the next time
// they are needed.
[self purgeImageCache];
// Enquee the reload call on the main runloop
// to avoid reloading the same image multiple times.
if (self.reloadInQueue == NO)
{
[[NSRunLoop mainRunLoop] performSelector:@selector(postReloadNotification) target:self argument:nil order:0 modes:@[NSDefaultRunLoopMode]];
self.reloadInQueue = YES;
}
}
- (void)postReloadNotification
{
[self.delegate reloadPreviews];
self.reloadInQueue = NO;
}
- (NSString *)info
{
return self.job.picture.info;
}
#pragma mark - Small previews
- (void)copySmallImageAtIndex:(NSUInteger)index completionHandler:(void (^)(__nullable CGImageRef result))handler
{
if (_invalidated || index >= self.imagesCount)
{
handler(NULL);
return;
}
dispatch_group_async(_group, _queue,^{
if (self->_invalidated || index >= self.imagesCount)
{
handler(NULL);
return;
}
CGImageRef image;
// First try to look in the small previews cache
image = (__bridge CGImageRef)([self->_smallPreviewsCache objectForKey:@(index)]);
if (image != NULL)
{
handler(image);
return;
}
// Else try the normal cache
image = (__bridge CGImageRef)([self->_previewsCache objectForKey:@(index)]);
if (image == NULL)
{
image = (CGImageRef)[self.scanCore copyImageAtIndex:index
forTitle:self.job.title
pictureFrame:self.job.picture
deinterlace:NO
rotate:self.job.filters.rotate
flipped:self.job.filters.flip];
CFAutorelease(image);
}
if (image != NULL)
{
CGImageRef scaledImage = CreateScaledCGImageFromCGImage(image, 30);
// The cost is the number of pixels of the image
NSUInteger previewCost = CGImageGetWidth(scaledImage) * CGImageGetHeight(scaledImage);
[self.smallPreviewsCache setObject:(__bridge id)(scaledImage) forKey:@(index) cost:previewCost];
handler(scaledImage);
return;
}
handler(NULL);
});
}
#pragma mark -
#pragma mark Preview movie
+ (NSURL *) generateFileURLForType:(NSString *) type
{
NSURL *previewDirectory = [[HBUtilities appSupportURL] URLByAppendingPathComponent:[NSString stringWithFormat:@"/Previews/%d", getpid()] isDirectory:YES];
if (![[NSFileManager defaultManager] createDirectoryAtPath:previewDirectory.path
withIntermediateDirectories:YES
attributes:nil
error:nil])
{
return nil;
}
return [previewDirectory URLByAppendingPathComponent:[NSString stringWithFormat:@"preview_temp.%@", type]];
}
/**
* This function start the encode of a movie preview, the delegate will be
* called with the updated the progress info and the fileURL.
*
* @param index picture index in title.
* @param duration the duration in seconds of the preview movie.
*/
- (BOOL) createMovieAsyncWithImageAtIndex: (NSUInteger) index duration: (NSUInteger) seconds;
{
// return if an encoding if already started.
if (self.core || index >= self.imagesCount)
{
return NO;
}
NSURL *destURL = nil;
// Generate the file url and directories.
if (self.job.container & 0x030000 /*HB_MUX_MASK_MP4*/)
{
// we use .m4v for our mp4 files so that ac3 and chapters in mp4 will play properly.
destURL = [HBPreviewGenerator generateFileURLForType:@"m4v"];
}
else if (self.job.container & 0x300000 /*HB_MUX_MASK_MKV*/)
{
destURL = [HBPreviewGenerator generateFileURLForType:@"mkv"];
}
// return if we couldn't get the fileURL.
if (!destURL)
{
return NO;
}
// See if there is an existing preview file, if so, delete it.
[[NSFileManager defaultManager] removeItemAtURL:destURL error:NULL];
HBJob *job = [self.job copy];
job.title = self.job.title;
job.outputFileName = destURL.lastPathComponent;
job.outputURL = destURL.URLByDeletingLastPathComponent;
job.range.type = HBRangePreviewIndex;
job.range.previewIndex = (int)index + 1;;
job.range.previewsCount = (int)self.imagesCount;
job.range.ptsToStop = seconds * 90000LL;
// Note: unlike a full encode, we only send 1 pass regardless if the final encode calls for 2 passes.
// this should suffice for a fairly accurate short preview and cuts our preview generation time in half.
job.video.twoPass = NO;
// Init the libhb core
int level = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue];
self.core = [[HBCore alloc] initWithLogLevel:level name:@"PreviewCore"];
HBStateFormatter *formatter = [[HBStateFormatter alloc] init];
formatter.twoLines = NO;
formatter.showPassNumber = NO;
formatter.title = NSLocalizedString(@"preview", @"Preview -> progress formatter title");
self.core.stateFormatter = formatter;
// start the actual encode
[self.core encodeJob:job
progressHandler:^(HBState state, HBProgress progress, NSString *info) {
[self.delegate updateProgress:progress.percent
info:info];
}
completionHandler:^(HBCoreResult result) {
// Encode done, call the delegate and close libhb handle
if (result == HBCoreResultDone)
{
[self.delegate didCreateMovieAtURL:destURL];
}
else
{
[self.delegate didCancelMovieCreation];
}
self.core = nil;
}];
return YES;
}
/**
* Cancels the encoding process
*/
- (void)cancel
{
if (self.core.state == HBStateWorking || self.core.state == HBStatePaused)
{
[self.core cancelEncode];
}
}
- (void)invalidate
{
_invalidated = true;
dispatch_group_wait(_group, DISPATCH_TIME_FOREVER);
}
@end