/* 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 "HBUtilities.h" #import "HBCore.h" #import "HBJob.h" #import "HBJob+HBJobConversion.h" @interface HBPreviewGenerator () @property (nonatomic, readonly, retain) NSMutableDictionary *picturePreviews; @property (nonatomic, readonly) NSUInteger imagesCount; @property (nonatomic, readonly) HBCore *scanCore; @property (nonatomic, readonly) HBJob *job; @property (nonatomic) HBCore *core; @end @implementation HBPreviewGenerator - (instancetype)initWithCore:(HBCore *)core job:(HBJob *)job { self = [super init]; if (self) { _scanCore = core; _job = job; _picturePreviews = [[NSMutableDictionary alloc] init]; _imagesCount = [[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewsNumber"] intValue]; } return self; } #pragma mark - #pragma mark Preview images /** * Returns the picture preview at the specified index * * @param index picture index in title. */ - (NSImage *) imageAtIndex: (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. NSImage *theImage = [self.picturePreviews objectForKey:@(index)]; if (!theImage) { HBFilters *filters = self.job.filters; BOOL deinterlace = (filters.deinterlace && !filters.useDecomb) || (filters.decomb && filters.useDecomb); theImage = [HBPreviewGenerator makeImageForPicture:index libhb:self.scanCore.hb_handle picture:self.job.picture deinterlace:deinterlace]; if (cache && theImage) [self.picturePreviews setObject:theImage forKey:@(index)]; } return theImage; } /** * Purges all images from the cache. The next call to imageAtIndex: will cause a new * image to be generated. */ - (void) purgeImageCache { [self.picturePreviews removeAllObjects]; } /** * This function converts an image created by libhb (specified via pictureIndex) into * an NSImage suitable for the GUI code to use. If removeBorders is YES, * makeImageForPicture crops the image generated by libhb stripping off the gray * border around the content. This is the low-level method that generates the image. * -imageForPicture calls this function whenever it can't find an image in its cache. * * @param pictureIndex Index in title. * @param handle Handle to hb_handle_t. * @param title Handle to hb_title_t of desired title. * @param deinterlace Whether the preview image must be deinterlaced or not. */ + (NSImage *) makeImageForPicture: (NSUInteger) pictureIndex libhb: (hb_handle_t *) handle picture: (HBPicture *) picture deinterlace: (BOOL) deinterlace { NSImage *img = nil; hb_geometry_settings_t geo; memset(&geo, 0, sizeof(geo)); geo.geometry.width = picture.width; geo.geometry.height = picture.height; // HBPreviewController will scale the image later, // ignore the par. geo.geometry.par.num = 1; geo.geometry.par.den = 1; int crop[4] = {picture.cropTop, picture.cropBottom, picture.cropLeft, picture.cropRight}; memcpy(geo.crop, crop, sizeof(int[4])); hb_image_t *image; image = hb_get_preview2(handle, picture.title.hb_title->index, (int)pictureIndex, &geo, deinterlace); if (image) { // Create an NSBitmapImageRep and copy the libhb image into it, converting it from // libhb's format to one suitable for NSImage. // The image data returned by hb_get_preview2 is 4 bytes per pixel, BGRA format. // Alpha is ignored. NSBitmapImageRep *imgrep = [[[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil pixelsWide:image->width pixelsHigh:image->height bitsPerSample:8 samplesPerPixel:3 // ignore alpha hasAlpha:NO isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bitmapFormat:NSAlphaFirstBitmapFormat bytesPerRow:image->width * 4 bitsPerPixel:32] autorelease]; UInt8 *src_line = image->data; UInt32 *dst = (UInt32 *)[imgrep bitmapData]; for (int r = 0; r < image->height; r++) { UInt32 *src = (UInt32 *)src_line; for (int c = 0; c < image->width; c++) { #if TARGET_RT_LITTLE_ENDIAN *dst++ = Endian32_Swap(*src++); #else *dst++ = *src++; #endif } src_line += image->plane[0].stride; } img = [[[NSImage alloc] initWithSize: NSMakeSize(image->width, image->height)] autorelease]; [img addRepresentation:imgrep]; } hb_image_close(&image); return img; } #pragma mark - #pragma mark Preview movie + (NSURL *) generateFileURLForType:(NSString *) type { NSString *previewDirectory = [NSString stringWithFormat:@"%@/Previews/%d", [HBUtilities appSupportPath], getpid()]; if (![[NSFileManager defaultManager] fileExistsAtPath:previewDirectory]) { if (![[NSFileManager defaultManager] createDirectoryAtPath:previewDirectory withIntermediateDirectories:YES attributes:nil error:nil]) return nil; } return [[NSURL fileURLWithPath: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 & 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 & 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. if (![[NSFileManager defaultManager] fileExistsAtPath:destURL.path]) { [[NSFileManager defaultManager] removeItemAtPath:destURL.path error:NULL]; } HBJob *job = [[self.job copy] autorelease]; job.title = self.job.title; job.destURL = destURL; 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 loggingLevel = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue]; self.core = [[[HBCore alloc] initWithLoggingLevel:loggingLevel] autorelease]; self.core.name = @"PreviewCore"; // start the actual encode [self.core encodeJob:job progressHandler:^(HBState state, hb_state_t hb_state) { switch (state) { case HBStateWorking: { NSMutableString *info = [NSMutableString stringWithFormat: @"Encoding preview: %.2f %%", 100.0 * hb_state.param.working.progress]; if (hb_state.param.working.seconds > -1) { [info appendFormat:@" (%.2f fps, avg %.2f fps, ETA %02dh%02dm%02ds)", hb_state.param.working.rate_cur, hb_state.param.working.rate_avg, hb_state.param.working.hours, hb_state.param.working.minutes, hb_state.param.working.seconds]; } double progress = 100.0 * hb_state.param.working.progress; [self.delegate updateProgress:progress info:info]; break; } case HBStateMuxing: [self.delegate updateProgress:100.0 info:@"Muxing Preview…"]; break; default: break; } } completationHandler:^(BOOL success) { // Encode done, call the delegate and close libhb handle if (success) { [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]; } } #pragma mark - - (void) dealloc { [self.core cancelEncode]; [_core release]; _core = nil; [_picturePreviews release]; _picturePreviews = nil; [super dealloc]; } @end