/* $Id: Controller.mm,v 1.79 2005/11/04 19:41:32 titer Exp $ This file is part of the HandBrake source code. Homepage: . It may be used under the terms of the GNU General Public License. */ #import "Controller.h" #import "HBOutputPanelController.h" #import "HBPreferencesController.h" #import "HBDVDDetector.h" #import "HBPresetsManager.h" #import "HBPreset.h" #import "HBPreviewController.h" #import "HBDockTile.h" #import "HBUtilities.h" #import "HBPresetsViewController.h" #import "HBAddPresetController.h" #import "HBAudioDefaults.h" #import "HBSubtitlesDefaults.h" #import "HBCore.h" #import "HBJob.h" NSString *HBContainerChangedNotification = @"HBContainerChangedNotification"; NSString *keyContainerTag = @"keyContainerTag"; // DockTile update freqency in total percent increment #define dockTileUpdateFrequency 0.1f @interface HBController () // The current job. @property (nonatomic, retain) HBJob *job; // The current selected preset. @property (nonatomic, retain) HBPreset *selectedPreset; @property (nonatomic) BOOL customPreset; /** * The HBCore used for scanning. */ @property (nonatomic, retain) HBCore *core; /** * The HBCore used for encoding. */ @property (nonatomic, retain) HBCore *queueCore; @end /******************************* * HBController implementation * *******************************/ @implementation HBController - (instancetype)init { self = [super init]; if (self) { // Register the defaults preferences [HBPreferencesController registerUserDefaults]; /* Check for and create the App Support Preview directory if necessary */ NSString *previewDirectory = [[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Previews"]; if (![[NSFileManager defaultManager] fileExistsAtPath:previewDirectory]) { [[NSFileManager defaultManager] createDirectoryAtPath:previewDirectory withIntermediateDirectories:YES attributes:nil error:NULL]; } // Inits the controllers outputPanel = [[HBOutputPanelController alloc] init]; fPictureController = [[HBPictureController alloc] init]; fPreviewController = [[HBPreviewController alloc] init]; fQueueController = [[HBQueueController alloc] init]; // we init the HBPresetsManager class NSURL *presetsURL = [NSURL fileURLWithPath:[[HBUtilities appSupportPath] stringByAppendingPathComponent:@"UserPresets.plist"]]; presetManager = [[HBPresetsManager alloc] initWithURL:presetsURL]; _selectedPreset = [presetManager.defaultPreset retain]; // Workaround to avoid a bug in Snow Leopard // we can switch back to [[NSApplication sharedApplication] applicationIconImage] // when we won't support it anymore. NSImage *appIcon = [NSImage imageNamed:@"HandBrake"]; [appIcon setSize:NSMakeSize(1024, 1024)]; // Load the dockTile and instiante initial text fields dockTile = [[HBDockTile alloc] initWithDockTile:[[NSApplication sharedApplication] dockTile] image:appIcon]; [dockTile updateDockIcon:-1.0 withETA:@""]; // Lets report the HandBrake version number here to the activity log and text log file NSString *versionStringFull = [[NSString stringWithFormat:@"Handbrake Version: %@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]] stringByAppendingString:[NSString stringWithFormat: @" (%@)", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]]]; [HBUtilities writeToActivityLog: "%s", [versionStringFull UTF8String]]; // Optional dvd nav UseDvdNav [HBCore setDVDNav:[[[NSUserDefaults standardUserDefaults] objectForKey:@"UseDvdNav"] boolValue]]; int loggingLevel = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue]; // Init libhb _core = [[HBCore alloc] initWithLoggingLevel:loggingLevel]; _core.name = @"ScanCore"; // Init a separate instance of libhb for user scanning and setting up jobs _queueCore = [[HBCore alloc] initWithLoggingLevel:loggingLevel]; _queueCore.name = @"QueueCore"; // Registers the observers to the cores notifications. [self registerScanCoreNotifications]; [self registerQueueCoreNotifications]; // Set the Growl Delegate [GrowlApplicationBridge setGrowlDelegate: self]; [fPictureController setDelegate:self]; fPreviewController.delegate = self; [fPreviewController setCore:self.core]; [fQueueController setHandle:self.queueCore.hb_handle]; [fQueueController setHBController:self]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(autoSetM4vExtension:) name:HBMixdownChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pictureSettingsDidChange) name:HBPictureChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pictureSettingsDidChange) name:HBFiltersChangedNotification object:nil]; } return self; } - (void) applicationDidFinishLaunching: (NSNotification *) notification { // Checks for presets updates [self checkBuiltInsForUpdates]; /* Init QueueFile .plist */ [self loadQueueFile]; [self initQueueFSEvent]; /* Run hbInstances to get any info on other instances as well as set the * pid number for this instance in the case of multi-instance encoding. */ hbInstanceNum = [self hbInstances]; /* If we are a single instance it is safe to clean up the previews if there are any * left over. This is a bit of a kludge but will prevent a build up of old instance * live preview cruft. No danger of removing an active preview directory since they * are created later in HBPreviewController if they don't exist at the moment a live * preview encode is initiated. */ if (hbInstanceNum == 1) { NSString *PreviewDirectory = [[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Previews"]; NSError *error; NSArray *files = [ [NSFileManager defaultManager] contentsOfDirectoryAtPath: PreviewDirectory error: &error ]; for( NSString *file in files ) { if( ![file isEqual: @"."] && ![file isEqual: @".."] ) { [ [NSFileManager defaultManager] removeItemAtPath: [ PreviewDirectory stringByAppendingPathComponent: file ] error: &error ]; if( error ) { //an error occurred [HBUtilities writeToActivityLog: "Could not remove existing preview at : %s",[file UTF8String] ]; } } } } [self enableUI: NO]; // Open debug output window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OutputPanelIsOpen"]) [self showDebugOutputPanel:nil]; // Open queue window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"QueueWindowIsOpen"]) [self showQueueWindow:nil]; [self openMainWindow:nil]; /* Now we re-check the queue array to see if there are * any remaining encodes to be done in it and ask the * user if they want to reload the queue */ if ([QueueFileArray count] > 0) { /* run getQueueStats to see whats in the queue file */ [self getQueueStats]; /* this results in these values * fPendingCount = 0; * fWorkingCount = 0; */ /* On Screen Notification * We check to see if there is already another instance of hb running. * Note: hbInstances == 1 means we are the only instance of HandBrake.app */ if (hbInstanceNum > 1) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"There is already an instance of HandBrake running.", @"")]; [alert setInformativeText:NSLocalizedString(@"HandBrake will now load up the existing queue.", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Reload Queue", nil)]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector(didDimissReloadQueue:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } else { if (fWorkingCount > 0 || fPendingCount > 0) { NSString *alertTitle; if (fWorkingCount > 0) { alertTitle = [NSString stringWithFormat: NSLocalizedString(@"HandBrake Has Detected %d Previously Encoding Item(s) and %d Pending Item(s) In Your Queue.", @""), fWorkingCount,fPendingCount]; } else { alertTitle = [NSString stringWithFormat: NSLocalizedString(@"HandBrake Has Detected %d Pending Item(s) In Your Queue.", @""), fPendingCount]; } NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:alertTitle]; [alert setInformativeText:NSLocalizedString(@"Do you want to reload them ?", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Reload Queue", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Empty Queue", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector(didDimissReloadQueue:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } else { /* Since we addressed any pending or previously encoding items above, we go ahead and make sure * the queue is empty of any finished items or cancelled items */ [self clearQueueAllItems]; if (self.core.state != HBStateScanning && !self.job) { // We show whichever open source window specified in LaunchSourceBehavior preference key if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source"]) { [self browseSources:nil]; } if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source (Title Specific)"]) { [self browseSources:(id)fOpenSourceTitleMMenu]; } } } } } else { if (self.core.state != HBStateScanning && !self.job) { // We show whichever open source window specified in LaunchSourceBehavior preference key if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source"]) { [self browseSources:nil]; } if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source (Title Specific)"]) { [self browseSources:(id)fOpenSourceTitleMMenu]; } } } currentQueueEncodeNameString = @""; } - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { [self openFile:filenames.firstObject]; [NSApp replyToOpenOrPrint:NSApplicationDelegateReplySuccess]; } - (void)openFile:(NSString *)filePath { if (self.core.state != HBStateScanning) { [browsedSourceDisplayName release]; browsedSourceDisplayName = [filePath.lastPathComponent retain]; [self performScan:filePath scanTitleNum:0]; } } #pragma mark - #pragma mark Multiple Instances /* hbInstances checks to see if other instances of HB are running and also sets the pid for this instance for multi-instance queue encoding */ /* Note for now since we are in early phases of multi-instance I have put in quite a bit of logging. Can be removed as we see fit. */ - (int) hbInstances { /* check to see if another instance of HandBrake.app is running */ NSArray *runningInstances = [NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]]; NSRunningApplication *runningInstance; NSRunningApplication *thisInstance = [NSRunningApplication currentApplication]; NSString *thisInstanceAppPath = [[NSBundle mainBundle] bundlePath]; [HBUtilities writeToActivityLog: "hbInstances path to this instance: %s", [thisInstanceAppPath UTF8String]]; int hbInstances = 0; NSString *runningInstanceAppPath; pid_t runningInstancePidNum; for (runningInstance in runningInstances) { /*Report the path to each active instances app path */ runningInstancePidNum = [runningInstance processIdentifier]; runningInstanceAppPath = [[runningInstance bundleURL] path]; [HBUtilities writeToActivityLog: "hbInstance found instance pidnum: %d at path: %s", runningInstancePidNum, [runningInstanceAppPath UTF8String]]; /* see if this is us*/ if ([runningInstance isEqual: thisInstance]) { /* If so this is our pidnum */ [HBUtilities writeToActivityLog: "hbInstance MATCH FOUND, our pidnum is: %d", runningInstancePidNum]; /* Get the PID number for this hb instance, used in multi instance encoding */ pidNum = runningInstancePidNum; /* Report this pid to the activity log */ [HBUtilities writeToActivityLog: "Pid for this instance: %d", pidNum]; /* Tell fQueueController what our pidNum is */ [fQueueController setPidNum:pidNum]; } hbInstances++; } return hbInstances; } #pragma mark - #pragma mark Drag & drop handling // This method is used by OSX to know what kind of files can be drag & drop on the NSWindow // We only want filenames (and so folders too) - (NSDragOperation)draggingEntered:(id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; if ([[pboard types] containsObject:NSFilenamesPboardType]) { NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType]; return paths.count == 1 ? NSDragOperationGeneric : NSDragOperationNone; } return NSDragOperationNone; } // This method is doing the job after the drag & drop operation has been validated by [self draggingEntered] and OSX - (BOOL)performDragOperation:(id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; if ([[pboard types] containsObject:NSFilenamesPboardType]) { NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType]; [self openFile:paths.firstObject]; } return YES; } #pragma mark - - (void) didDimissReloadQueue: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo { [HBUtilities writeToActivityLog: "didDimissReloadQueue number of hb instances:%d", hbInstanceNum]; if (returnCode == NSAlertSecondButtonReturn) { [HBUtilities writeToActivityLog: "didDimissReloadQueue NSAlertSecondButtonReturn Chosen"]; [self clearQueueAllItems]; /* We show whichever open source window specified in LaunchSourceBehavior preference key */ if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source"]) { [self browseSources:nil]; } if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"LaunchSourceBehavior"] isEqualToString: @"Open Source (Title Specific)"]) { [self browseSources:(id)fOpenSourceTitleMMenu]; } } else { [HBUtilities writeToActivityLog: "didDimissReloadQueue NSAlertFirstButtonReturn Chosen"]; if (hbInstanceNum == 1) { [self setQueueEncodingItemsAsPending]; } [self reloadQueue]; [self showQueueWindow:NULL]; } } - (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication *) app { if (self.queueCore.state != HBStateIdle) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"Are you sure you want to quit HandBrake?", nil)]; [alert setInformativeText:NSLocalizedString(@"If you quit HandBrake your current encode will be reloaded into your queue at next launch. Do you want to quit anyway?", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Quit", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Don't Quit", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; NSInteger result = [alert runModal]; [alert release]; if (result == NSAlertFirstButtonReturn) { return NSTerminateNow; } else { return NSTerminateCancel; } } // Warn if items still in the queue else if (fPendingCount > 0) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"Are you sure you want to quit HandBrake?", nil)]; [alert setInformativeText:NSLocalizedString(@"There are pending encodes in your queue. Do you want to quit anyway?",nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Quit", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Don't Quit", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; NSInteger result = [alert runModal]; [alert release]; if (result == NSAlertFirstButtonReturn) { return NSTerminateNow; } else { return NSTerminateCancel; } } return NSTerminateNow; } - (void)applicationWillTerminate:(NSNotification *)aNotification { [presetManager savePresets]; [presetManager release]; [self closeQueueFSEvent]; [currentQueueEncodeNameString release]; [browsedSourceDisplayName release]; [outputPanel release]; [fQueueController release]; [fPreviewController release]; [fPictureController release]; [dockTile release]; [[NSNotificationCenter defaultCenter] removeObserver:self]; self.core = nil; self.queueCore = nil; [HBCore closeGlobal]; } - (void) awakeFromNib { /* For 64 bit builds, the threaded animation in the progress * indicators conflicts with the animation in the advanced tab * for reasons not completely clear. jbrjake found a note in the * 10.5 dev notes regarding this possiblility. It was also noted * that unless specified, setUsesThreadedAnimation defaults to true. * So, at least for now we set the indicator animation to NO for * both the scan and regular progress indicators for both 32 and 64 bit * as it test out fine on both and there is no reason our progress indicators * should require their own thread. */ [fScanIndicator setUsesThreadedAnimation:NO]; [fRipIndicator setUsesThreadedAnimation:NO]; // Presets initialization [self buildPresetsMenu]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(buildPresetsMenu) name:HBPresetsChangedNotification object:nil]; [fPresetDrawer setDelegate:self]; NSSize drawerSize = NSSizeFromString([[NSUserDefaults standardUserDefaults] stringForKey:@"HBDrawerSize"]); if (drawerSize.width) { [fPresetDrawer setContentSize: drawerSize]; } /* Show/Dont Show Presets drawer upon launch based on user preference DefaultPresetsDrawerShow */ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBDefaultPresetsDrawerShow"]) { [fPresetDrawer open:self]; } /* Setup the start / stop popup */ [fEncodeStartStopPopUp removeAllItems]; [fEncodeStartStopPopUp addItemWithTitle: @"Chapters"]; [fEncodeStartStopPopUp addItemWithTitle: @"Seconds"]; [fEncodeStartStopPopUp addItemWithTitle: @"Frames"]; // Align the start / stop widgets with the chapter popups NSPoint startPoint = [fSrcChapterStartPopUp frame].origin; startPoint.y += 2; NSPoint endPoint = [fSrcChapterEndPopUp frame].origin; endPoint.y += 2; [fSrcTimeStartEncodingField setFrameOrigin:startPoint]; [fSrcTimeEndEncodingField setFrameOrigin:endPoint]; [fSrcFrameStartEncodingField setFrameOrigin:startPoint]; [fSrcFrameEndEncodingField setFrameOrigin:endPoint]; /* Destination box*/ NSMenuItem *menuItem; [fDstFormatPopUp removeAllItems]; for (const hb_container_t *container = hb_container_get_next(NULL); container != NULL; container = hb_container_get_next(container)) { NSString *title = nil; if (container->format & HB_MUX_MASK_MP4) { title = @"MP4 File"; } else if (container->format & HB_MUX_MASK_MKV) { title = @"MKV File"; } else { title = [NSString stringWithUTF8String:container->name]; } menuItem = [[fDstFormatPopUp menu] addItemWithTitle:title action:nil keyEquivalent:@""]; [menuItem setTag:container->format]; } // select the first container [fDstFormatPopUp selectItemAtIndex:0]; [self formatPopUpChanged:nil]; [fDstFile2Field setStringValue:[NSString stringWithFormat:@"%@/Desktop/Movie.mp4", NSHomeDirectory()]]; /* Bottom */ [fStatusField setStringValue: @""]; /* Register HBController's Window as a receiver for files/folders drag & drop operations */ [fWindow registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; // Set up the preset drawer fPresetsView = [[HBPresetsViewController alloc] initWithPresetManager:presetManager]; [fPresetDrawer setContentView:[fPresetsView view]]; fPresetsView.delegate = self; [[fPresetDrawer contentView] setAutoresizingMask:( NSViewWidthSizable | NSViewHeightSizable )]; // Set up the chapters title view fChapterTitlesController = [[HBChapterTitlesController alloc] init]; [fChaptersTitlesTab setView:[fChapterTitlesController view]]; // setup the subtitles view fSubtitlesViewController = [[HBSubtitlesController alloc] init]; [fSubtitlesTab setView:[fSubtitlesViewController view]]; // setup the audio controller fAudioController = [[HBAudioController alloc] init]; [fAudioTab setView:[fAudioController view]]; // setup the advanced view controller fAdvancedOptions = [[HBAdvancedController alloc] init]; [fAdvancedTab setView:[fAdvancedOptions view]]; // setup the video view controller fVideoController = [[HBVideoController alloc] initWithAdvancedController:fAdvancedOptions]; [fVideoTab setView:[fVideoController view]]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.HBShowAdvancedTab" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:NULL]; [fWindow recalculateKeyViewLoop]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == NULL) { if ([keyPath isEqualToString:@"values.HBShowAdvancedTab"]) { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HBShowAdvancedTab"]) { if (![[fMainTabView tabViewItems] containsObject:fAdvancedTab]) { [fMainTabView insertTabViewItem:fAdvancedTab atIndex:3]; [fAdvancedTab release]; } } else { [fAdvancedTab retain]; [fMainTabView removeTabViewItem:fAdvancedTab]; } } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void) enableUI: (BOOL) b { NSControl * controls[] = { fSrcTitleField, fSrcTitlePopUp, fSrcChapterStartPopUp, fSrcChapterToField, fSrcChapterEndPopUp, fSrcDuration1Field, fSrcDuration2Field, fDstFormatField, fDstFormatPopUp, fDstFile1Field, fDstFile2Field, fDstBrowseButton, fSrcAngleLabel, fSrcAnglePopUp, fDstMp4HttpOptFileCheck, fDstMp4iPodFileCheck, fEncodeStartStopPopUp, fSrcTimeStartEncodingField, fSrcTimeEndEncodingField, fSrcFrameStartEncodingField, fSrcFrameEndEncodingField, }; for (unsigned i = 0; i < (sizeof(controls) / sizeof(NSControl*)); i++) { if ([[controls[i] className] isEqualToString: @"NSTextField"]) { NSTextField *tf = (NSTextField*)controls[i]; if (![tf isBezeled]) { [tf setTextColor: (b ? [NSColor controlTextColor] : [NSColor disabledControlTextColor])]; continue; } } [controls[i] setEnabled: b]; } fPresetsView.enabled = b; fVideoController.enabled = b; fAudioController.enabled = b; fSubtitlesViewController.enabled = b; fChapterTitlesController.enabled = b; } /** * Registers the observers to the scan core notifications */ - (void)registerScanCoreNotifications { NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanningNotification object:self.core queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.core.hb_state); #define p s.param.scanning if (p.preview_cur) { fSrcDVD2Field.stringValue = [NSString stringWithFormat: NSLocalizedString( @"Scanning title %d of %d, preview %d…", @"" ), p.title_cur, p.title_count, p.preview_cur]; } else { fSrcDVD2Field.stringValue = [NSString stringWithFormat: NSLocalizedString( @"Scanning title %d of %d…", @"" ), p.title_cur, p.title_count]; } fScanIndicator.hidden = NO; fScanHorizontalLine.hidden = YES; fScanIndicator.doubleValue = 100.0 * p.progress; #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanDoneNotification object:self.core queue:mainQueue usingBlock:^(NSNotification *note) { fScanHorizontalLine.hidden = NO; fScanIndicator.hidden = YES; fScanIndicator.indeterminate = NO; fScanIndicator.doubleValue = 0.0; [HBUtilities writeToActivityLog:"ScanDone state received from fHandle"]; [self showNewScan:nil]; [[fWindow toolbar] validateVisibleItems]; }]; } - (void)registerQueueCoreNotifications { NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanningNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.queueCore.hb_state); #define p s.param.scanning NSString *scan_status; if (p.preview_cur) { scan_status = [NSString stringWithFormat: NSLocalizedString( @"Queue Scanning title %d of %d, preview %d…", @"" ), p.title_cur, p.title_count, p.preview_cur]; } else { scan_status = [NSString stringWithFormat: NSLocalizedString( @"Queue Scanning title %d of %d…", @"" ), p.title_cur, p.title_count]; } fStatusField.stringValue = scan_status; // Set the status string in fQueueController as well [fQueueController setQueueStatusString: scan_status]; #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreScanDoneNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { [HBUtilities writeToActivityLog:"ScanDone state received from fQueueEncodeLibhb"]; [self processNewQueueEncode]; [[fWindow toolbar] validateVisibleItems]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreSearchingNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.queueCore.hb_state); #define p s.param.working NSMutableString *string = [NSMutableString stringWithFormat: NSLocalizedString(@"Searching for start point… : %.2f %%", @""), 100.0 * p.progress]; if (p.seconds > -1) { [string appendFormat:NSLocalizedString(@" (ETA %02dh%02dm%02ds)", @"" ), p.hours, p.minutes, p.seconds]; } fStatusField.stringValue = string; // Set the status string in fQueueController as well [fQueueController setQueueStatusString: string]; // Update slider CGFloat progress_total = (p.progress + p.job_cur - 1) / p.job_count; fRipIndicator.indeterminate = NO; fRipIndicator.doubleValue = 100.0 * progress_total; // If progress bar hasn't been revealed at the bottom of the window, do // that now. This code used to be in doRip. I moved it to here to handle // the case where hb_start is called by HBQueueController and not from // HBController. if (!fRipIndicatorShown) { NSRect frame = [fWindow frame]; if (frame.size.width <= 591) frame.size.width = 591; frame.size.height += 36; frame.origin.y -= 36; [fWindow setFrame:frame display:YES animate:YES]; fRipIndicatorShown = YES; } #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreWorkingNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { hb_state_t s = *(self.queueCore.hb_state); #define p s.param.working // Update text field NSString *pass_desc; if (p.job_cur == 1 && p.job_count > 1) { if (QueueFileArray[currentQueueEncodeIndex][@"SubtitleList"] && [[QueueFileArray[currentQueueEncodeIndex][@"SubtitleList"] firstObject][keySubTrackIndex] intValue] == -1) { pass_desc = NSLocalizedString(@"(subtitle scan)", @""); } else { pass_desc = @""; } } else { pass_desc = @""; } NSMutableString *string; if (pass_desc.length) { string = [NSMutableString stringWithFormat: NSLocalizedString(@"Encoding: %@ \nPass %d %@ of %d, %.2f %%", @""), currentQueueEncodeNameString, p.job_cur, pass_desc, p.job_count, 100.0 * p.progress]; } else { string = [NSMutableString stringWithFormat: NSLocalizedString(@"Encoding: %@ \nPass %d of %d, %.2f %%", @""), currentQueueEncodeNameString, p.job_cur, p.job_count, 100.0 * p.progress]; } if (p.seconds > -1) { if (p.rate_cur > 0.0) { [string appendFormat: NSLocalizedString(@" (%.2f fps, avg %.2f fps, ETA %02dh%02dm%02ds)", @""), p.rate_cur, p.rate_avg, p.hours, p.minutes, p.seconds]; } else { [string appendFormat: NSLocalizedString(@" (ETA %02dh%02dm%02ds)", @""), p.hours, p.minutes, p.seconds]; } } fStatusField.stringValue = string; [fQueueController setQueueStatusString:string]; // Update slider CGFloat progress_total = (p.progress + p.job_cur - 1) / p.job_count; fRipIndicator.indeterminate = NO; fRipIndicator.doubleValue = 100.0 * progress_total; // If progress bar hasn't been revealed at the bottom of the window, do // that now. This code used to be in doRip. I moved it to here to handle // the case where hb_start is called by HBQueueController and not from // HBController. if (!fRipIndicatorShown) { NSRect frame = [fWindow frame]; if (frame.size.width <= 591) frame.size.width = 591; frame.size.height += 36; frame.origin.y -= 36; [fWindow setFrame:frame display:YES animate:YES]; fRipIndicatorShown = YES; } /* Update dock icon */ if (dockIconProgress < 100.0 * progress_total) { // ETA format is [XX]X:XX:XX when ETA is greater than one hour // [X]X:XX when ETA is greater than 0 (minutes or seconds) // When these conditions doesn't applied (eg. when ETA is undefined) // we show just a tilde (~) NSString *etaStr = @""; if (p.hours > 0) etaStr = [NSString stringWithFormat:@"%d:%02d:%02d", p.hours, p.minutes, p.seconds]; else if (p.minutes > 0 || p.seconds > 0) etaStr = [NSString stringWithFormat:@"%d:%02d", p.minutes, p.seconds]; else etaStr = @"~"; [dockTile updateDockIcon:progress_total withETA:etaStr]; dockIconProgress += dockTileUpdateFrequency; } #undef p }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreMuxingNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { // Update text field fStatusField.stringValue = NSLocalizedString(@"Muxing…", @""); // Set the status string in fQueueController as well [fQueueController setQueueStatusString:NSLocalizedString(@"Muxing…", @"")]; // Update slider fRipIndicator.indeterminate = YES; [fRipIndicator startAnimation: nil]; // Update dock icon [dockTile updateDockIcon:1.0 withETA:@""]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCorePausedNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { NSString *paused = NSLocalizedString(@"Paused", @""); fStatusField.stringValue = paused; [fQueueController setQueueStatusString:paused]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:HBCoreWorkDoneNotification object:self.queueCore queue:mainQueue usingBlock:^(NSNotification *note) { fStatusField.stringValue = NSLocalizedString(@"Encode Finished.", @""); // Set the status string in fQueueController as well [fQueueController setQueueStatusString:NSLocalizedString(@"Encode Finished.", @"")]; fRipIndicator.indeterminate = NO; fRipIndicator.doubleValue = 0.0; [fRipIndicator setIndeterminate: NO]; [[fWindow toolbar] validateVisibleItems]; // Restore dock icon [dockTile updateDockIcon:-1.0 withETA:@""]; dockIconProgress = 0; if (fRipIndicatorShown) { NSRect frame = [fWindow frame]; if( frame.size.width <= 591 ) frame.size.width = 591; frame.size.height += -36; frame.origin.y -= -36; [fWindow setFrame:frame display:YES animate:YES]; fRipIndicatorShown = NO; } // Since we are done with this encode, tell output to stop writing to the // individual encode log. [outputPanel endEncodeLog]; // Check to see if the encode state has not been cancelled // to determine if we should check for encode done notifications. if (fEncodeState != 2) { NSString *pathOfFinishedEncode; // Get the output file name for the finished encode pathOfFinishedEncode = [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"DestinationPath"]; // Both the Growl Alert and Sending to tagger can be done as encodes roll off the queue if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Growl Notification"] || [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Alert Window And Growl"]) { // If Play System Alert has been selected in Preferences if ([[NSUserDefaults standardUserDefaults] boolForKey:@"AlertWhenDoneSound"] == YES) { NSBeep(); } [self showGrowlDoneNotification:pathOfFinishedEncode]; } // Send to tagger [self sendToMetaX:pathOfFinishedEncode]; // since we have successfully completed an encode, we increment the queue counter [self incrementQueueItemDone:currentQueueEncodeIndex]; } }]; } #pragma mark - #pragma mark Toolbar - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem { SEL action = toolbarItem.action; if (self.core) { if (self.core.state == HBStateScanning) { if (action == @selector(browseSources:)) { [toolbarItem setImage: [NSImage imageNamed: @"stopencode"]]; [toolbarItem setLabel: @"Cancel Scan"]; [toolbarItem setPaletteLabel: @"Cancel Scanning"]; [toolbarItem setToolTip: @"Cancel Scanning Source"]; return YES; } if (action == @selector(Rip:) || action == @selector(addToQueue:)) return NO; } else { if (action == @selector(browseSources:)) { [toolbarItem setImage: [NSImage imageNamed: @"source"]]; [toolbarItem setLabel: @"Source"]; [toolbarItem setPaletteLabel: @"Source"]; [toolbarItem setToolTip: @"Choose Video Source"]; return YES; } } HBState queueState = self.queueCore.state; if (queueState == HBStateWorking || queueState == HBStateSearching || queueState == HBStateMuxing) { if (action == @selector(Rip:)) { [toolbarItem setImage: [NSImage imageNamed: @"stopencode"]]; [toolbarItem setLabel: @"Stop"]; [toolbarItem setPaletteLabel: @"Stop"]; [toolbarItem setToolTip: @"Stop Encoding"]; return YES; } if (action == @selector(Pause:)) { [toolbarItem setImage: [NSImage imageNamed: @"pauseencode"]]; [toolbarItem setLabel: @"Pause"]; [toolbarItem setPaletteLabel: @"Pause Encoding"]; [toolbarItem setToolTip: @"Pause Encoding"]; return YES; } if (self.job) { if (action == @selector(addToQueue:)) return YES; if (action == @selector(showPicturePanel:)) return YES; if (action == @selector(showPreviewWindow:)) return YES; } } else if (queueState == HBStatePaused) { if (action == @selector(Pause:)) { [toolbarItem setImage: [NSImage imageNamed: @"encode"]]; [toolbarItem setLabel: @"Resume"]; [toolbarItem setPaletteLabel: @"Resume Encoding"]; [toolbarItem setToolTip: @"Resume Encoding"]; return YES; } if (action == @selector(Rip:)) return YES; if (action == @selector(addToQueue:)) return YES; if (action == @selector(showPicturePanel:)) return YES; if (action == @selector(showPreviewWindow:)) return YES; } else if (queueState == HBStateScanning) { return NO; } else if (queueState == HBStateWorkDone || queueState == HBStateScanDone || self.job) { if (action == @selector(Rip:)) { [toolbarItem setImage: [NSImage imageNamed: @"encode"]]; if (hb_count(self.core.hb_handle) > 0) [toolbarItem setLabel: @"Start Queue"]; else [toolbarItem setLabel: @"Start"]; [toolbarItem setPaletteLabel: @"Start Encoding"]; [toolbarItem setToolTip: @"Start Encoding"]; return YES; } if (action == @selector(addToQueue:)) return YES; if (action == @selector(showPicturePanel:)) return YES; if (action == @selector(showPreviewWindow:)) return YES; } } /* If there are any pending queue items, make sure the start/stop button is active */ if (action == @selector(Rip:) && fPendingCount > 0) return YES; if (action == @selector(showQueueWindow:)) return YES; if (action == @selector(toggleDrawer:)) return YES; if (action == @selector(browseSources:)) return YES; if (action == @selector(showDebugOutputPanel:)) return YES; return NO; } - (BOOL) validateMenuItem: (NSMenuItem *) menuItem { SEL action = [menuItem action]; if (self.queueCore) { HBState queueState = self.queueCore.state; if (action == @selector(addToQueue:) || action == @selector(addAllTitlesToQueue:) || action == @selector(showPicturePanel:) || action == @selector(showAddPresetPanel:)) return self.job && [fWindow attachedSheet] == nil; if (action == @selector(selectDefaultPreset:)) return [fWindow attachedSheet] == nil; if (action == @selector(Pause:)) { if (queueState == HBStateWorking) { if(![[menuItem title] isEqualToString:@"Pause Encoding"]) [menuItem setTitle:@"Pause Encoding"]; return YES; } else if (queueState == HBStatePaused) { if(![[menuItem title] isEqualToString:@"Resume Encoding"]) [menuItem setTitle:@"Resume Encoding"]; return YES; } else return NO; } if (action == @selector(Rip:)) { if (queueState == HBStateWorking || queueState == HBStateMuxing || queueState == HBStatePaused) { if(![[menuItem title] isEqualToString:@"Stop Encoding"]) [menuItem setTitle:@"Stop Encoding"]; return YES; } else if (self.job) { if(![[menuItem title] isEqualToString:@"Start Encoding"]) [menuItem setTitle:@"Start Encoding"]; return [fWindow attachedSheet] == nil; } else return NO; } if (action == @selector(browseSources:)) { if (self.core.state == HBStateScanning) return NO; else return [fWindow attachedSheet] == nil; } if (action == @selector(selectPresetFromMenu:)) { if (!self.customPreset && [menuItem.representedObject isEqualTo:self.selectedPreset]) { [menuItem setState:NSOnState]; } else { [menuItem setState:NSOffState]; } } } return YES; } #pragma mark - #pragma mark Encode Done Actions // register a test notification and make // it enabled by default #define SERVICE_NAME @"Encode Done" - (NSDictionary *)registrationDictionaryForGrowl { NSDictionary *registrationDictionary = [NSDictionary dictionaryWithObjectsAndKeys: [NSArray arrayWithObjects:SERVICE_NAME,nil], GROWL_NOTIFICATIONS_ALL, [NSArray arrayWithObjects:SERVICE_NAME,nil], GROWL_NOTIFICATIONS_DEFAULT, nil]; return registrationDictionary; } -(void)showGrowlDoneNotification:(NSString *) filePath { /* This end of encode action is called as each encode rolls off of the queue */ /* Setup the Growl stuff */ NSString * finishedEncode = filePath; /* strip off the path to just show the file name */ finishedEncode = [finishedEncode lastPathComponent]; NSString * growlMssg = [NSString stringWithFormat: @"your HandBrake encode %@ is done!",finishedEncode]; [GrowlApplicationBridge notifyWithTitle:@"Put down that cocktail…" description:growlMssg notificationName:SERVICE_NAME iconData:nil priority:0 isSticky:1 clickContext:nil]; } -(void)sendToMetaX:(NSString *) filePath { /* This end of encode action is called as each encode rolls off of the queue */ if([[NSUserDefaults standardUserDefaults] boolForKey: @"sendToMetaX"] == YES) { NSString *sendToApp = [[NSUserDefaults standardUserDefaults] objectForKey: @"SendCompletedEncodeToApp"]; if (![sendToApp isEqualToString:@"None"]) { [HBUtilities writeToActivityLog: "trying to send encode to: %s", [sendToApp UTF8String]]; NSAppleScript *myScript = [[NSAppleScript alloc] initWithSource: [NSString stringWithFormat: @"%@%@%@%@%@", @"tell application \"",sendToApp,@"\" to open (POSIX file \"", filePath, @"\")"]]; [myScript executeAndReturnError: nil]; [myScript release]; } } } - (void) queueCompletedAlerts { /* If Play System Alert has been selected in Preferences */ if( [[NSUserDefaults standardUserDefaults] boolForKey:@"AlertWhenDoneSound"] == YES ) { NSBeep(); } /* If Alert Window or Window and Growl has been selected */ if( [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Alert Window"] || [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Alert Window And Growl"] ) { /*On Screen Notification*/ NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Put down that cocktail…"]; [alert setInformativeText:@"Your HandBrake queue is done!"]; [NSApp requestUserAttention:NSCriticalRequest]; [alert runModal]; [alert release]; } /* If sleep has been selected */ if( [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Put Computer To Sleep"] ) { /* Sleep */ NSDictionary *errorDict; NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithSource: @"tell application \"Finder\" to sleep"]; [scriptObject executeAndReturnError: &errorDict]; [scriptObject release]; } /* If Shutdown has been selected */ if( [[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Shut Down Computer"] ) { /* Shut Down */ NSDictionary* errorDict; NSAppleScript* scriptObject = [[NSAppleScript alloc] initWithSource: @"tell application \"Finder\" to shut down"]; [scriptObject executeAndReturnError: &errorDict]; [scriptObject release]; } } #pragma mark - #pragma mark Get New Source /*Opens the source browse window, called from Open Source widgets */ - (IBAction)browseSources:(id)sender { if (self.core.state == HBStateScanning) { [self.core cancelScan]; return; } NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setAllowsMultipleSelection:NO]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:YES]; NSURL *sourceDirectory; if ([[NSUserDefaults standardUserDefaults] URLForKey:@"LastSourceDirectoryURL"]) { sourceDirectory = [[NSUserDefaults standardUserDefaults] URLForKey:@"LastSourceDirectoryURL"]; } else { sourceDirectory = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"]; } [panel setDirectoryURL:sourceDirectory]; [panel beginSheetModalForWindow:fWindow completionHandler: ^(NSInteger result) { if (result == NSOKButton) { // Free display name allocated previously by this code [browsedSourceDisplayName release]; NSURL *scanURL = panel.URL; // we set the last searched source directory in the prefs here [[NSUserDefaults standardUserDefaults] setURL:scanURL.URLByDeletingLastPathComponent forKey:@"LastSourceDirectoryURL"]; // we order out sheet, which is the browse window as we need to open // the title selection sheet right away [panel orderOut:self]; if (sender == fOpenSourceTitleMMenu || [[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask) { // We put the chosen source path in the source display text field for the // source title selection sheet in which the user specifies the specific title to be // scanned as well as the short source name in fSrcDsplyNameTitleScan just for display // purposes in the title panel // Full Path [fScanSrcTitlePathField setStringValue:scanURL.path]; NSString *displayTitlescanSourceName; if ([scanURL.lastPathComponent isEqualToString: @"VIDEO_TS"]) { // If VIDEO_TS Folder is chosen, choose its parent folder for the source display name // we have to use the title->path value so we get the proper name of the volume if a physical dvd is the source displayTitlescanSourceName = scanURL.URLByDeletingLastPathComponent.lastPathComponent; } else { // if not the VIDEO_TS Folder, we can assume the chosen folder is the source name displayTitlescanSourceName = scanURL.lastPathComponent; } // we set the source display name in the title selection dialogue [fSrcDsplyNameTitleScan setStringValue:displayTitlescanSourceName]; // we set the attempted scans display name for main window to displayTitlescanSourceName browsedSourceDisplayName = [displayTitlescanSourceName retain]; // We show the actual sheet where the user specifies the title to be scanned // as we are going to do a title specific scan [self showSourceTitleScanPanel:nil]; } else { // We are just doing a standard full source scan, so we specify "0" to libhb NSURL *url = panel.URL; // We check to see if the chosen file at path is a package if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:url.path]) { [HBUtilities writeToActivityLog: "trying to open a package at: %s", url.path.UTF8String]; // We check to see if this is an .eyetv package if ([url.pathExtension isEqualToString: @"eyetv"]) { [HBUtilities writeToActivityLog:"trying to open eyetv package"]; // We're looking at an EyeTV package - try to open its enclosed .mpg media file browsedSourceDisplayName = [url.URLByDeletingPathExtension.lastPathComponent retain]; NSString *mpgname; NSUInteger n = [[[url path] stringByAppendingString: @"/"] completePathIntoString: &mpgname caseSensitive: YES matchesIntoArray: nil filterTypes: [NSArray arrayWithObject: @"mpg"]]; if (n > 0) { /* Found an mpeg inside the eyetv package, make it our scan path and call performScan on the enclosed mpeg */ [HBUtilities writeToActivityLog:"found mpeg in eyetv package"]; [self performScan:mpgname scanTitleNum:0]; } else { /* We did not find an mpeg file in our package, so we do not call performScan */ [HBUtilities writeToActivityLog:"no valid mpeg in eyetv package"]; } } /* We check to see if this is a .dvdmedia package */ else if ([[url pathExtension] isEqualToString: @"dvdmedia"]) { /* path IS a package - but dvdmedia packages can be treaded like normal directories */ browsedSourceDisplayName = [[[url URLByDeletingPathExtension] lastPathComponent] retain]; [HBUtilities writeToActivityLog:"trying to open dvdmedia package"]; [self performScan:[url path] scanTitleNum:0]; } else { /* The package is not an eyetv package, try to open it anyway */ browsedSourceDisplayName = [[url lastPathComponent] retain]; [HBUtilities writeToActivityLog:"not a known to package"]; [self performScan:[url path] scanTitleNum:0]; } } else // path is not a package, so we treat it as a dvd parent folder or VIDEO_TS folder { /* path is not a package, so we call perform scan directly on our file */ if ([[url lastPathComponent] isEqualToString: @"VIDEO_TS"]) { [HBUtilities writeToActivityLog:"trying to open video_ts folder (video_ts folder chosen)"]; /* If VIDEO_TS Folder is chosen, choose its parent folder for the source display name*/ browsedSourceDisplayName = [[[url URLByDeletingLastPathComponent] lastPathComponent] retain]; } else { [HBUtilities writeToActivityLog:"trying to open video_ts folder (parent directory chosen)"]; /* if not the VIDEO_TS Folder, we can assume the chosen folder is the source name */ /* make sure we remove any path extension as this can also be an '.mpg' file */ browsedSourceDisplayName = [[url lastPathComponent] retain]; } applyQueueToScan = NO; [self performScan:[url path] scanTitleNum:0]; } } } }]; } /* Here we open the title selection sheet where we can specify an exact title to be scanned */ - (IBAction) showSourceTitleScanPanel: (id) sender { /* We default the title number to be scanned to "0" which results in a full source scan, unless the * user changes it */ [fScanSrcTitleNumField setStringValue: @"0"]; /* Show the panel */ [NSApp beginSheet:fScanSrcTitlePanel modalForWindow:fWindow modalDelegate:nil didEndSelector:NULL contextInfo:NULL]; } - (IBAction) closeSourceTitleScanPanel: (id) sender { [NSApp endSheet: fScanSrcTitlePanel]; [fScanSrcTitlePanel orderOut: self]; if(sender == fScanSrcTitleOpenButton) { /* We setup the scan status in the main window to indicate a source title scan */ [fSrcDVD2Field setStringValue: @"Opening a new source title…"]; [fScanIndicator setHidden: NO]; [fScanHorizontalLine setHidden: YES]; [fScanIndicator setIndeterminate: YES]; [fScanIndicator startAnimation: nil]; /* We use the performScan method to actually perform the specified scan passing the path and the title * to be scanned */ applyQueueToScan = NO; [self performScan:[fScanSrcTitlePathField stringValue] scanTitleNum:[fScanSrcTitleNumField intValue]]; } } /* Here we actually tell hb_scan to perform the source scan, using the path to source and title number*/ - (void)performScan:(NSString *)scanPath scanTitleNum:(NSInteger)scanTitleNum { // Save the current settings if (self.job) { self.selectedPreset = [self createPresetFromCurrentSettings]; } // Notify anyone interested (audio/subtitles/chapters controller) that there's no title fPictureController.picture = nil; fPictureController.filters = nil; fPreviewController.job = nil; fAudioController.job = nil; fSubtitlesViewController.job = nil; fVideoController.video = nil; fChapterTitlesController.job = nil; self.job = nil; [self enableUI: NO]; NSError *outError = NULL; NSURL *fileURL = [NSURL fileURLWithPath:scanPath]; BOOL suppressWarning = [[NSUserDefaults standardUserDefaults] boolForKey:@"suppresslibdvdcss"]; // Check if we can scan the source and if there is any warning. BOOL canScan = [self.core canScan:fileURL error:&outError]; // Notify the user that we don't support removal of copy proteciton. if (canScan && [outError code] == 101 && !suppressWarning) { // Only show the user this warning once. They may be using a solution we don't know about. Notifying them each time is annoying. [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"suppresslibdvdcss"]; // Compatible libdvdcss not found [HBUtilities writeToActivityLog: "libdvdcss.2.dylib not found for decrypting physical dvd"]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Please note that HandBrake does not support the removal of copy-protection from DVD Discs. You can if you wish install libdvdcss or any other 3rd party software for this function."]; [alert setInformativeText:@"Videolan.org provides libdvdcss if you are not currently using another solution."]; [alert addButtonWithTitle:@"Get libdvdcss.pkg"]; [alert addButtonWithTitle:@"Cancel Scan"]; [alert addButtonWithTitle:@"Attempt Scan Anyway"]; [NSApp requestUserAttention:NSCriticalRequest]; NSInteger status = [alert runModal]; [alert release]; if (status == NSAlertFirstButtonReturn) { /* User chose to go download vlc (as they rightfully should) so we send them to the vlc site */ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://download.videolan.org/libdvdcss/1.2.12/macosx/"]]; canScan = NO; } else if (status == NSAlertSecondButtonReturn) { /* User chose to cancel the scan */ [HBUtilities writeToActivityLog: "Cannot open physical dvd, scan cancelled"]; canScan = NO; } else { /* User chose to override our warning and scan the physical dvd anyway, at their own peril. on an encrypted dvd this produces massive log files and fails */ [HBUtilities writeToActivityLog:"User overrode copy-protection warning - trying to open physical dvd without decryption"]; } } if (canScan) { int hb_num_previews = [[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewsNumber"] intValue]; int min_title_duration_seconds = [[[NSUserDefaults standardUserDefaults] objectForKey:@"MinTitleScanSeconds"] intValue]; [self.core scan:fileURL titleNum:scanTitleNum previewsNum:hb_num_previews minTitleDuration:min_title_duration_seconds]; } } - (IBAction) showNewScan:(id)sender { if (!self.core.titles.count) { // We display a message if a valid source was not chosen [fSrcDVD2Field setStringValue: @"No Valid Source Found"]; } else { if (applyQueueToScan == YES) { // we are a rescan of an existing queue item and need to apply the queued settings to the scan [HBUtilities writeToActivityLog: "showNewScan: This is a queued item rescan"]; } else { [HBUtilities writeToActivityLog: "showNewScan: This is a new source item scan"]; } [fSrcTitlePopUp removeAllItems]; for (HBTitle *title in self.core.titles) { // Set Source Name at top of window with the browsedSourceDisplayName grokked right before -performScan if (!browsedSourceDisplayName) { browsedSourceDisplayName = @"NoNameDetected"; } [fSrcDVD2Field setStringValue:browsedSourceDisplayName]; // use the correct extension based on the container int videoContainer = (int)[[fDstFormatPopUp selectedItem] tag]; const char *ext = hb_container_get_default_extension(videoContainer); // If its a queue rescan for edit, get the queue item output path // if not, its a new source scan. // Check to see if the last destination has been set,use if so, if not, use Desktop if (applyQueueToScan == YES) { [fDstFile2Field setStringValue: [NSString stringWithFormat:@"%@", QueueFileArray[fqueueEditRescanItemNum][@"DestinationPath"]]]; } else if ([[NSUserDefaults standardUserDefaults] stringForKey:@"LastDestinationDirectory"]) { [fDstFile2Field setStringValue: [NSString stringWithFormat: @"%@/%@.%s", [[NSUserDefaults standardUserDefaults] stringForKey:@"LastDestinationDirectory"],[browsedSourceDisplayName stringByDeletingPathExtension],ext]]; } else { [fDstFile2Field setStringValue: [NSString stringWithFormat: @"%@/Desktop/%@.%s", NSHomeDirectory(),[browsedSourceDisplayName stringByDeletingPathExtension],ext]]; } // set m4v extension if necessary - do not override user-specified .mp4 extension if ((videoContainer & HB_MUX_MASK_MP4) && (applyQueueToScan != YES)) { [self autoSetM4vExtension:sender]; } [fSrcTitlePopUp addItemWithTitle:title.description]; // See if this is the main feature according if (title.isFeatured) { [fSrcTitlePopUp selectItemWithTitle:title.description]; } } // Select the first item is nothing i selected if (!fSrcTitlePopUp.selectedItem) { [fSrcTitlePopUp selectItemAtIndex:0]; } // Updates the main window ui [self enableUI:YES]; [self titlePopUpChanged:nil]; [self encodeStartStopPopUpChanged:nil]; // Open preview window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"PreviewWindowIsOpen"]) [self showPreviewWindow:nil]; // Open picture sizing window now if it was visible when HB was closed if ([[NSUserDefaults standardUserDefaults] boolForKey:@"PictureSizeWindowIsOpen"]) [self showPicturePanel:nil]; if (applyQueueToScan == YES) { // we are a rescan of an existing queue item and need to apply the queued settings to the scan [HBUtilities writeToActivityLog: "showNewScan: calling applyQueueSettingsToMainWindow"]; [self applyQueueSettingsToMainWindow:nil]; } } } #pragma mark - #pragma mark New Output Destination - (IBAction)browseFile:(id)sender { // Open a panel to let the user choose and update the text field NSSavePanel * panel = [NSSavePanel savePanel]; // We get the current file name and path from the destination field here NSString *destinationDirectory = fDstFile2Field.stringValue.stringByDeletingLastPathComponent; [panel setDirectoryURL:[NSURL fileURLWithPath:destinationDirectory]]; [panel setNameFieldStringValue:[[fDstFile2Field stringValue] lastPathComponent]]; [panel beginSheetModalForWindow:fWindow completionHandler:^(NSInteger result) { if (result == NSFileHandlingPanelOKButton) { fDstFile2Field.stringValue = panel.URL.path; // Save this path to the prefs so that on next browse destination window it opens there NSString *destinationDirectory = [[fDstFile2Field stringValue] stringByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setObject:destinationDirectory forKey:@"LastDestinationDirectory"]; } }]; } #pragma mark - #pragma mark Main Window Control - (IBAction) openMainWindow: (id) sender { [fWindow makeKeyAndOrderFront:nil]; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag { if( !flag ) { [fWindow makeKeyAndOrderFront:nil]; } return YES; } - (NSSize) drawerWillResizeContents:(NSDrawer *) drawer toSize:(NSSize) contentSize { [[NSUserDefaults standardUserDefaults] setObject:NSStringFromSize( contentSize ) forKey:@"HBDrawerSize"]; return contentSize; } #pragma mark - #pragma mark Queue File static void queueFSEventStreamCallback( ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]) { HBController *hb = (HBController *)clientCallBackInfo; [hb reloadQueue]; } - (void)initQueueFSEvent { /* Define variables and create a CFArray object containing CFString objects containing paths to watch. */ CFStringRef mypath = (CFStringRef) [[HBUtilities appSupportPath] stringByAppendingPathComponent:@"Queue"]; CFArrayRef pathsToWatch = CFArrayCreate(NULL, (const void **)&mypath, 1, NULL); FSEventStreamContext callbackCtx; callbackCtx.version = 0; callbackCtx.info = self; callbackCtx.retain = NULL; callbackCtx.release = NULL; callbackCtx.copyDescription = NULL; CFAbsoluteTime latency = 0.5; /* Latency in seconds */ /* Create the stream, passing in a callback */ QueueStream = FSEventStreamCreate(NULL, &queueFSEventStreamCallback, &callbackCtx, pathsToWatch, kFSEventStreamEventIdSinceNow, latency, kFSEventStreamCreateFlagIgnoreSelf ); CFRelease(pathsToWatch); /* Create the stream before calling this. */ FSEventStreamScheduleWithRunLoop(QueueStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); FSEventStreamStart(QueueStream); } - (void)closeQueueFSEvent { FSEventStreamStop(QueueStream); FSEventStreamInvalidate(QueueStream); FSEventStreamRelease(QueueStream); } - (void)loadQueueFile { /* We declare the default NSFileManager into fileManager */ NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *appSupportPath = [HBUtilities appSupportPath]; /* We define the location of the user presets file */ QueueFile = [[appSupportPath stringByAppendingPathComponent:@"Queue/Queue.plist"] retain]; /* We check for the Queue.plist */ if( ![fileManager fileExistsAtPath:QueueFile] ) { if( ![fileManager fileExistsAtPath:[appSupportPath stringByAppendingPathComponent:@"Queue"]] ) { [fileManager createDirectoryAtPath:[appSupportPath stringByAppendingPathComponent:@"Queue"] withIntermediateDirectories:YES attributes:nil error:NULL]; } [fileManager createFileAtPath:QueueFile contents:nil attributes:nil]; } QueueFileArray = [[NSMutableArray alloc] initWithContentsOfFile:QueueFile]; /* lets check to see if there is anything in the queue file .plist */ if( QueueFileArray == nil ) { /* if not, then lets initialize an empty array */ QueueFileArray = [[NSMutableArray alloc] init]; } else { /* ONLY clear out encoded items if we are single instance */ if( hbInstanceNum == 1 ) { [self clearQueueEncodedItems]; } } } - (void)reloadQueue { [HBUtilities writeToActivityLog:"Queue reloaded"]; NSMutableArray * tempQueueArray = [[NSMutableArray alloc] initWithContentsOfFile:QueueFile]; [QueueFileArray setArray:tempQueueArray]; [tempQueueArray release]; /* Send Fresh QueueFileArray to fQueueController to update queue window */ [fQueueController setQueueArray: QueueFileArray]; [self getQueueStats]; } - (void)addQueueFileItem { [QueueFileArray addObject:[self createQueueFileItem]]; [self saveQueueFileItem]; } - (void) removeQueueFileItem:(NSUInteger) queueItemToRemove { [QueueFileArray removeObjectAtIndex:queueItemToRemove]; [self saveQueueFileItem]; } - (void)saveQueueFileItem { [QueueFileArray writeToFile:QueueFile atomically:YES]; [fQueueController setQueueArray: QueueFileArray]; [self getQueueStats]; } /** * Updates the queue status label on the main window. */ - (void)getQueueStats { /* lets get the stats on the status of the queue array */ fPendingCount = 0; fWorkingCount = 0; /* We use a number system to set the encode status of the queue item * in controller.mm * 0 == already encoded * 1 == is being encoded * 2 == is yet to be encoded * 3 == cancelled */ int i = 0; for (NSDictionary *thisQueueDict in QueueFileArray) { if ([[thisQueueDict objectForKey:@"Status"] intValue] == 1) // being encoded { fWorkingCount++; /* check to see if we are the instance doing this encoding */ if ([thisQueueDict objectForKey:@"EncodingPID"] && [[thisQueueDict objectForKey:@"EncodingPID"] intValue] == pidNum) { currentQueueEncodeIndex = i; } } if ([[thisQueueDict objectForKey:@"Status"] intValue] == 2) // pending { fPendingCount++; } i++; } /* Set the queue status field in the main window */ NSString *string; if (fPendingCount == 0) { string = NSLocalizedString( @"No encode pending", @""); } else if (fPendingCount == 1) { string = [NSString stringWithFormat: NSLocalizedString( @"%d encode pending", @"" ), fPendingCount]; } else { string = [NSString stringWithFormat: NSLocalizedString( @"%d encodes pending", @"" ), fPendingCount]; } [fQueueStatus setStringValue:string]; } /* Used to get the next pending queue item index and return it if found */ - (NSInteger)getNextPendingQueueIndex { /* initialize nextPendingIndex to -1, this value tells incrementQueueItemDone that there are no pending items in the queue */ NSInteger nextPendingIndex = -1; BOOL nextPendingFound = NO; for (id tempObject in QueueFileArray) { NSDictionary *thisQueueDict = tempObject; if ([[thisQueueDict objectForKey:@"Status"] intValue] == 2 && nextPendingFound == NO) // pending { nextPendingFound = YES; nextPendingIndex = [QueueFileArray indexOfObject: tempObject]; [HBUtilities writeToActivityLog: "getNextPendingQueueIndex next pending encode index is:%d", nextPendingIndex]; } } return nextPendingIndex; } /* This method will set any item marked as encoding back to pending * currently used right after a queue reload */ - (void) setQueueEncodingItemsAsPending { NSMutableArray *tempArray; tempArray = [NSMutableArray array]; /* we look here to see if the preset is we move on to the next one */ for (id tempObject in QueueFileArray) { /* We want to keep any queue item that is pending or was previously being encoded */ if ([[tempObject objectForKey:@"Status"] intValue] == 1 || [[tempObject objectForKey:@"Status"] intValue] == 2) { /* If the queue item is marked as "encoding" (1) * then change its status back to pending (2) which effectively * puts it back into the queue to be encoded */ if ([[tempObject objectForKey:@"Status"] intValue] == 1) { [tempObject setObject:[NSNumber numberWithInt: 2] forKey:@"Status"]; } [tempArray addObject:tempObject]; } } [QueueFileArray setArray:tempArray]; [self saveQueueFileItem]; } /* This method will clear the queue of any encodes that are not still pending * this includes both successfully completed encodes as well as cancelled encodes */ - (void) clearQueueEncodedItems { NSMutableArray *tempArray; tempArray = [NSMutableArray array]; /* we look here to see if the preset is we move on to the next one */ for (id tempObject in QueueFileArray) { /* If the queue item is either completed (0) or cancelled (3) from the * last session, then we put it in tempArray to be deleted from QueueFileArray. * NOTE: this means we retain pending (2) and also an item that is marked as * still encoding (1). If the queue has an item that is still marked as encoding * from a previous session, we can conlude that HB was either shutdown, or crashed * during the encodes so we keep it and tell the user in the "Load Queue Alert" */ if ([[tempObject objectForKey:@"Status"] intValue] == 0 || [[tempObject objectForKey:@"Status"] intValue] == 3) { [tempArray addObject:tempObject]; } } [QueueFileArray removeObjectsInArray:tempArray]; [self saveQueueFileItem]; } /* This method will clear the queue of all encodes. effectively creating an empty queue */ - (void) clearQueueAllItems { NSMutableArray *tempArray; tempArray = [NSMutableArray array]; /* we look here to see if the preset is we move on to the next one */ for (id tempObject in QueueFileArray) { [tempArray addObject:tempObject]; } [QueueFileArray removeObjectsInArray:tempArray]; [self saveQueueFileItem]; } /* This method will duplicate prepareJob however into the * queue .plist instead of into the job structure so it can * be recalled later */ - (NSDictionary *)createQueueFileItem { NSMutableDictionary *queueFileJob = [[NSMutableDictionary alloc] init]; hb_list_t * list = hb_get_titles(self.core.hb_handle); hb_title_t * title = (hb_title_t *) hb_list_item( list, (int)[fSrcTitlePopUp indexOfSelectedItem] ); /* We use a number system to set the encode status of the queue item * 0 == already encoded * 1 == is being encoded * 2 == is yet to be encoded * 3 == cancelled */ [queueFileJob setObject:[NSNumber numberWithInt:2] forKey:@"Status"]; /* Source and Destination Information */ [queueFileJob setObject:[NSString stringWithUTF8String: title->path] forKey:@"SourcePath"]; [queueFileJob setObject:[[fSrcDVD2Field stringValue] lastPathComponent] forKey:@"SourceName"]; [queueFileJob setObject:[NSNumber numberWithInt:title->index] forKey:@"TitleNumber"]; [queueFileJob setObject:[NSNumber numberWithInteger:[fSrcAnglePopUp indexOfSelectedItem] + 1] forKey:@"TitleAngle"]; /* Determine and set a variable to tell hb what start and stop times to use (chapters, seconds or frames) */ if( [fEncodeStartStopPopUp indexOfSelectedItem] == 0 ) { [queueFileJob setObject:[NSNumber numberWithInt:0] forKey:@"fEncodeStartStop"]; } else if ([fEncodeStartStopPopUp indexOfSelectedItem] == 1) { [queueFileJob setObject:[NSNumber numberWithInt:1] forKey:@"fEncodeStartStop"]; } else if ([fEncodeStartStopPopUp indexOfSelectedItem] == 2) { [queueFileJob setObject:[NSNumber numberWithInt:2] forKey:@"fEncodeStartStop"]; } /* Chapter encode info */ [queueFileJob setObject:[NSNumber numberWithInteger:[fSrcChapterStartPopUp indexOfSelectedItem] + 1] forKey:@"ChapterStart"]; [queueFileJob setObject:[NSNumber numberWithInteger:[fSrcChapterEndPopUp indexOfSelectedItem] + 1] forKey:@"ChapterEnd"]; /* Time (pts) encode info */ [queueFileJob setObject:[NSNumber numberWithInt:[fSrcTimeStartEncodingField intValue]] forKey:@"StartSeconds"]; [queueFileJob setObject:[NSNumber numberWithInt:[fSrcTimeEndEncodingField intValue] - [fSrcTimeStartEncodingField intValue]] forKey:@"StopSeconds"]; /* Frame number encode info */ [queueFileJob setObject:[NSNumber numberWithInt:[fSrcFrameStartEncodingField intValue]] forKey:@"StartFrame"]; [queueFileJob setObject:[NSNumber numberWithInt:[fSrcFrameEndEncodingField intValue] - [fSrcFrameStartEncodingField intValue]] forKey:@"StopFrame"]; /* The number of seek points equals the number of seconds announced in the title as that is our current granularity */ int title_duration_seconds = (title->hours * 3600) + (title->minutes * 60) + (title->seconds); [queueFileJob setObject:[NSNumber numberWithInt:title_duration_seconds] forKey:@"SourceTotalSeconds"]; [queueFileJob setObject:[fDstFile2Field stringValue] forKey:@"DestinationPath"]; /* Lets get the preset info if there is any */ [queueFileJob setObject:[fPresetSelectedDisplay stringValue] forKey:@"PresetName"]; [queueFileJob setObject:[fDstFormatPopUp titleOfSelectedItem] forKey:@"FileFormat"]; /* Chapter Markers*/ /* If we are encoding by chapters and we have only one chapter or a title without chapters, set chapter markers to off. Leave them on if we're doing point to point encoding, as libhb supports chapters when doing p2p. */ if ([fEncodeStartStopPopUp indexOfSelectedItem] == 0 && [fSrcChapterStartPopUp indexOfSelectedItem] == [fSrcChapterEndPopUp indexOfSelectedItem]) { [queueFileJob setObject:[NSNumber numberWithInt:0] forKey:@"ChapterMarkers"]; } else { [queueFileJob setObject:@(fChapterTitlesController.createChapterMarkers) forKey:@"ChapterMarkers"]; } /* We need to get the list of chapter names to put into an array and store * in our queue, so they can be reapplied in prepareJob when this queue * item comes up if Chapter Markers is set to on. */ [queueFileJob setObject:fChapterTitlesController.chapterTitlesArray forKey:@"ChapterNames"]; /* Mux mp4 with http optimization */ [queueFileJob setObject:[NSNumber numberWithInteger:[fDstMp4HttpOptFileCheck state]] forKey:@"Mp4HttpOptimize"]; /* Add iPod uuid atom */ [queueFileJob setObject:[NSNumber numberWithInteger:[fDstMp4iPodFileCheck state]] forKey:@"Mp4iPodCompatible"]; /* Codecs */ /* Video encoder */ [self.job.video prepareVideoForQueueFileJob:queueFileJob]; /* Picture Sizing */ [self.job.picture preparePictureForQueueFileJob:queueFileJob]; /* Text summaries of various settings */ [queueFileJob setObject:[NSString stringWithString:[self pictureSettingsSummary]] forKey:@"PictureSettingsSummary"]; [queueFileJob setObject:self.job.filters.summary forKey:@"PictureFiltersSummary"]; [queueFileJob setObject:[NSString stringWithString:[self muxerOptionsSummary]] forKey:@"MuxerOptionsSummary"]; /* Picture Filters */ HBFilters *filters = self.job.filters; queueFileJob[@"PictureDetelecine"] = @(filters.detelecine); queueFileJob[@"PictureDetelecineCustom"] = filters.detelecineCustomString; queueFileJob[@"PictureDecombDeinterlace"] = @(filters.useDecomb); queueFileJob[@"PictureDecomb"] = @(filters.decomb); queueFileJob[@"PictureDecombCustom"] = filters.decombCustomString; queueFileJob[@"PictureDeinterlace"] = @(filters.deinterlace); queueFileJob[@"PictureDeinterlaceCustom"] = filters.deinterlaceCustomString; queueFileJob[@"PictureDenoise"] = filters.denoise; queueFileJob[@"PictureDenoisePreset"] = filters.denoisePreset; queueFileJob[@"PictureDenoiseTune"] = filters.denoiseTune; queueFileJob[@"PictureDenoiseCustom"] = filters.denoiseCustomString; queueFileJob[@"PictureDeblock"] = [NSString stringWithFormat:@"%ld",(long)filters.deblock]; queueFileJob[@"VideoGrayScale"] = [NSString stringWithFormat:@"%ld",(long)filters.grayscale]; /* Audio Defaults */ NSMutableDictionary *audioDefaults = [NSMutableDictionary dictionary]; [self.job.audioDefaults prepareAudioDefaultsForPreset:audioDefaults]; queueFileJob[@"AudioDefaults"] = audioDefaults; /* Audio */ NSMutableArray *audioArray = [[NSMutableArray alloc] initWithArray:[fAudioController audioTracks] copyItems:YES]; [queueFileJob setObject:audioArray forKey:@"AudioList"]; [audioArray release]; /* Subtitles Defaults */ NSMutableDictionary *subtitlesDefaults = [NSMutableDictionary dictionary]; [self.job.subtitlesDefaults prepareSubtitlesDefaultsForPreset:subtitlesDefaults]; queueFileJob[@"SubtitlesDefaults"] = subtitlesDefaults; /* Subtitles */ NSMutableArray *subtitlesArray = [[NSMutableArray alloc] initWithArray:[fSubtitlesViewController subtitles] copyItems:YES]; [queueFileJob setObject:subtitlesArray forKey:@"SubtitleList"]; [subtitlesArray release]; /* Now we go ahead and set the "job->values in the plist for passing right to fQueueEncodeLibhb */ [queueFileJob setObject:[NSNumber numberWithInteger:[[fDstFormatPopUp selectedItem] tag]] forKey:@"JobFileFormatMux"]; /* Codecs */ /* Framerate */ [queueFileJob setObject:[NSNumber numberWithInt:title->vrate.num] forKey:@"JobVrate"]; [queueFileJob setObject:[NSNumber numberWithInt:title->vrate.den] forKey:@"JobVrateBase"]; /* we need to auto relase the queueFileJob and return it */ [queueFileJob autorelease]; return queueFileJob; } /* this is actually called from the queue controller to modify the queue array and return it back to the queue controller */ - (void)moveObjectsInQueueArray:(NSMutableArray *)array fromIndexes:(NSIndexSet *)indexSet toIndex:(NSUInteger)insertIndex { NSUInteger index = [indexSet lastIndex]; NSUInteger aboveInsertIndexCount = 0; NSUInteger removeIndex; if (index >= insertIndex) { removeIndex = index + aboveInsertIndexCount; aboveInsertIndexCount++; } else { removeIndex = index; insertIndex--; } id object = [[QueueFileArray objectAtIndex:removeIndex] retain]; [QueueFileArray removeObjectAtIndex:removeIndex]; [QueueFileArray insertObject:object atIndex:insertIndex]; [object release]; /* We save all of the Queue data here * and it also gets sent back to the queue controller*/ [self saveQueueFileItem]; } #pragma mark - #pragma mark Queue Job Processing - (void) incrementQueueItemDone:(NSInteger) queueItemDoneIndexNum { /* Mark the encode just finished as done (status 0)*/ [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] setObject:[NSNumber numberWithInt:0] forKey:@"Status"]; /* We save all of the Queue data here */ [self saveQueueFileItem]; /* Since we have now marked a queue item as done * we can go ahead and increment currentQueueEncodeIndex * so that if there is anything left in the queue we can * go ahead and move to the next item if we want to */ NSInteger queueItems = [QueueFileArray count]; /* Check to see if there are any more pending items in the queue */ NSInteger newQueueItemIndex = [self getNextPendingQueueIndex]; /* If we still have more pending items in our queue, lets go to the next one */ if (newQueueItemIndex >= 0 && newQueueItemIndex < queueItems) { /*Set our currentQueueEncodeIndex now to the newly found Pending encode as we own it */ currentQueueEncodeIndex = newQueueItemIndex; /* now we mark the queue item as Status = 1 ( being encoded ) so another instance can not come along and try to scan it while we are scanning */ [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] setObject:[NSNumber numberWithInt:1] forKey:@"Status"]; [HBUtilities writeToActivityLog: "incrementQueueItemDone new pending items found: %d", currentQueueEncodeIndex]; [self saveQueueFileItem]; /* now we can go ahead and scan the new pending queue item */ [self performNewQueueScan:[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"SourcePath"] scanTitleNum:[[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"TitleNumber"]intValue]]; } else { [HBUtilities writeToActivityLog: "incrementQueueItemDone there are no more pending encodes"]; /* Done encoding, allow system sleep for the encode handle */ /* * Since there are no more items to encode, go to queueCompletedAlerts * for user specified alerts after queue completed */ [self queueCompletedAlerts]; } } /* Here we actually tell hb_scan to perform the source scan, using the path to source and title number*/ - (void) performNewQueueScan:(NSString *) scanPath scanTitleNum: (NSInteger) scanTitleNum { /* Tell HB to output a new activity log file for this encode */ [outputPanel startEncodeLog:[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"DestinationPath"]]; /* We now flag the queue item as being owned by this instance of HB using the PID */ [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] setObject:[NSNumber numberWithInt:pidNum] forKey:@"EncodingPID"]; /* Get the currentQueueEncodeNameString from the queue item to display in the status field */ currentQueueEncodeNameString = [[[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"DestinationPath"] lastPathComponent]retain]; /* We save all of the Queue data here */ [self saveQueueFileItem]; /* * Only scan 10 previews before an encode - additional previews are * only useful for autocrop and static previews, which are already taken * care of at this point */ NSURL *fileURL = [NSURL fileURLWithPath:scanPath]; [self.queueCore scan:fileURL titleNum:scanTitleNum previewsNum:10 minTitleDuration:0]; } /* This assumes that we have re-scanned and loaded up a new queue item to send to libhb as fQueueEncodeLibhb */ - (void) processNewQueueEncode { hb_list_t * list = hb_get_titles( self.queueCore.hb_handle ); hb_title_t * title = (hb_title_t *) hb_list_item( list,0 ); // is always zero since now its a single title scan hb_job_t * job = hb_job_init(title); if( !hb_list_count( list ) ) { [HBUtilities writeToActivityLog: "processNewQueueEncode WARNING nothing found in the title list"]; } NSMutableDictionary * queueToApply = [QueueFileArray objectAtIndex:currentQueueEncodeIndex]; [HBUtilities writeToActivityLog: "Preset: %s", [[queueToApply objectForKey:@"PresetName"] UTF8String]]; [HBUtilities writeToActivityLog: "processNewQueueEncode number of passes expected is: %d", ([[queueToApply objectForKey:@"VideoTwoPass"] intValue] + 1)]; hb_job_set_file(job, [[queueToApply objectForKey:@"DestinationPath"] UTF8String]); [self prepareJob:job]; /* * If scanning we need to do some extra setup of the job. */ if (job->indepth_scan == 1) { char *encoder_preset_tmp = job->encoder_preset != NULL ? strdup(job->encoder_preset) : NULL; char *encoder_tune_tmp = job->encoder_tune != NULL ? strdup(job->encoder_tune) : NULL; char *encoder_options_tmp = job->encoder_options != NULL ? strdup(job->encoder_options) : NULL; char *encoder_profile_tmp = job->encoder_profile != NULL ? strdup(job->encoder_profile) : NULL; char *encoder_level_tmp = job->encoder_level != NULL ? strdup(job->encoder_level) : NULL; /* * When subtitle scan is enabled do a fast pre-scan job * which will determine which subtitles to enable, if any. */ hb_job_set_encoder_preset (job, NULL); hb_job_set_encoder_tune (job, NULL); hb_job_set_encoder_options(job, NULL); hb_job_set_encoder_profile(job, NULL); hb_job_set_encoder_level (job, NULL); job->pass = -1; hb_add(self.queueCore.hb_handle, job); /* * reset the advanced settings */ hb_job_set_encoder_preset (job, encoder_preset_tmp); hb_job_set_encoder_tune (job, encoder_tune_tmp); hb_job_set_encoder_options(job, encoder_options_tmp); hb_job_set_encoder_profile(job, encoder_profile_tmp); hb_job_set_encoder_level (job, encoder_level_tmp); free(encoder_preset_tmp); free(encoder_tune_tmp); free(encoder_options_tmp); free(encoder_profile_tmp); free(encoder_level_tmp); } if ([[queueToApply objectForKey:@"VideoTwoPass"] intValue] == 1) { job->indepth_scan = 0; job->pass = 1; hb_add(self.queueCore.hb_handle, job); job->pass = 2; hb_add(self.queueCore.hb_handle, job); } else { job->indepth_scan = 0; job->pass = 0; hb_add(self.queueCore.hb_handle, job); } NSString *destinationDirectory = [[queueToApply objectForKey:@"DestinationPath"] stringByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setObject:destinationDirectory forKey:@"LastDestinationDirectory"]; /* Lets mark our new encode as 1 or "Encoding" */ [queueToApply setObject:[NSNumber numberWithInt:1] forKey:@"Status"]; [self saveQueueFileItem]; /* libhb makes a copy of the job. So we need to free any resource * that were allocated in construction of the job. This empties * the audio, subtitle, and filter lists */ hb_job_close(&job); /* We should be all setup so let 'er rip */ [self doRip]; } #pragma mark - #pragma mark Queue Item Editing /* Rescans the chosen queue item back into the main window */ - (void)rescanQueueItemToMainWindow:(NSString *) scanPath scanTitleNum: (NSUInteger) scanTitleNum selectedQueueItem: (NSUInteger) selectedQueueItem { fqueueEditRescanItemNum = selectedQueueItem; [HBUtilities writeToActivityLog: "rescanQueueItemToMainWindow: Re-scanning queue item at index:%d",fqueueEditRescanItemNum]; applyQueueToScan = YES; /* Make sure we release the display name before reassigning it */ [browsedSourceDisplayName release]; /* Set the browsedSourceDisplayName for showNewScan */ browsedSourceDisplayName = [[[QueueFileArray objectAtIndex:fqueueEditRescanItemNum] objectForKey:@"SourceName"] retain]; [self performScan:scanPath scanTitleNum:scanTitleNum]; } /* We use this method after a queue item rescan for edit. * it largely mirrors -selectPreset in terms of structure. * Assumes that a queue item has been reloaded into the main window. */ - (IBAction)applyQueueSettingsToMainWindow:(id)sender { NSDictionary *queueToApply = [QueueFileArray objectAtIndex:fqueueEditRescanItemNum]; if (queueToApply) { [HBUtilities writeToActivityLog: "applyQueueSettingsToMainWindow: queue item found"]; } /* Set title number and chapters */ /* since the queue only scans a single title, its already been selected in showNewScan so do not try to reset it here. However if we do decide to do full source scans on a queue edit rescan, we would need it. So leaving in for now but commenting out. */ //[fSrcTitlePopUp selectItemAtIndex: [[queueToApply objectForKey:@"TitleNumber"] intValue] - 1]; [fSrcChapterStartPopUp selectItemAtIndex: [[queueToApply objectForKey:@"ChapterStart"] intValue] - 1]; [fSrcChapterEndPopUp selectItemAtIndex: [[queueToApply objectForKey:@"ChapterEnd"] intValue] - 1]; /* File Format */ [fDstFormatPopUp selectItemWithTag:[queueToApply[@"JobFileFormatMux"] integerValue]]; [self formatPopUpChanged:nil]; /* Chapter Markers*/ fChapterTitlesController.createChapterMarkers = [[queueToApply objectForKey:@"ChapterMarkers"] boolValue]; [fChapterTitlesController addChaptersFromQueue:[queueToApply objectForKey:@"ChapterNames"]]; /* Mux mp4 with http optimization */ [fDstMp4HttpOptFileCheck setState:[[queueToApply objectForKey:@"Mp4HttpOptimize"] intValue]]; /* Set the state of ipod compatible with Mp4iPodCompatible. Only for x264*/ [fDstMp4iPodFileCheck setState:[[queueToApply objectForKey:@"Mp4iPodCompatible"] intValue]]; /* video encoder */ [self.job.video applyVideoSettingsFromQueue:queueToApply]; /* Audio Defaults */ [self.job.audioDefaults applySettingsFromPreset:queueToApply[@"AudioDefaults"]]; /* Audio */ /* Now lets add our new tracks to the audio list here */ [fAudioController addTracksFromQueue:[queueToApply objectForKey:@"AudioList"]]; /* Subtitles Defaults */ [self.job.subtitlesDefaults applySettingsFromPreset:queueToApply[@"SubtitlesDefaults"]]; /* Subtitles */ [fSubtitlesViewController addTracksFromQueue:[queueToApply objectForKey:@"SubtitleList"]]; /* Picture Settings */ [self.job.picture applyPictureSettingsFromQueue:queueToApply]; /* Filters */ HBFilters *filters = self.job.filters; /* We only allow *either* Decomb or Deinterlace. So check for the PictureDecombDeinterlace key. */ filters.useDecomb = YES; filters.decomb = 0; filters.deinterlace = 0; if ([queueToApply[@"PictureDecombDeinterlace"] intValue] == 1) { /* we are using decomb */ /* Decomb */ if ([queueToApply[@"PictureDecomb"] intValue] > 0) { filters.decomb = [queueToApply[@"PictureDecomb"] intValue]; /* if we are using "Custom" in the decomb setting, also set the custom string*/ if ([queueToApply[@"PictureDecomb"] intValue] == 1) { filters.decombCustomString = queueToApply[@"PictureDecombCustom"]; } } } else { /* We are using Deinterlace */ /* Deinterlace */ if ([queueToApply[@"PictureDeinterlace"] intValue] > 0) { filters.useDecomb = NO; filters.deinterlace = [queueToApply[@"PictureDeinterlace"] intValue]; /* if we are using "Custom" in the deinterlace setting, also set the custom string*/ if ([queueToApply[@"PictureDeinterlace"] intValue] == 1) { filters.deinterlaceCustomString = queueToApply[@"PictureDeinterlaceCustom"]; } } } /* Detelecine */ if ([queueToApply[@"PictureDetelecine"] intValue] > 0) { filters.detelecine = [queueToApply[@"PictureDetelecine"] intValue]; /* if we are using "Custom" in the detelecine setting, also set the custom string*/ if ([queueToApply[@"PictureDetelecine"] intValue] == 1) { filters.detelecineCustomString = queueToApply[@"PictureDetelecineCustom"]; } } else { filters.detelecine = 0; } /* Denoise */ filters.denoise = queueToApply[@"PictureDenoise"]; filters.denoisePreset = queueToApply[@"PictureDenoisePreset"]; filters.denoiseTune = queueToApply[@"PictureDenoiseTune"]; filters.denoiseCustomString = queueToApply[@"PictureDenoiseCustom"]; /* Deblock */ if ([queueToApply[@"PictureDeblock"] intValue] == 1) { /* if its a one, then its the old on/off deblock, set on to 5*/ filters.deblock = 5; } else { /* use the settings intValue */ filters.deblock = [queueToApply[@"PictureDeblock"] intValue]; } filters.grayscale = [queueToApply[@"VideoGrayScale"] boolValue]; // Updates the previews window and summary strings [self pictureSettingsDidChange]; [fPresetSelectedDisplay setStringValue:queueToApply[@"PresetName"]]; [fPresetsView deselect]; /* Now that source is loaded and settings applied, delete the queue item from the queue */ [HBUtilities writeToActivityLog: "applyQueueSettingsToMainWindow: deleting queue item:%d",fqueueEditRescanItemNum]; [self removeQueueFileItem:fqueueEditRescanItemNum]; } #pragma mark - #pragma mark Job Handling - (void) prepareJob:(hb_job_t *)job { NSDictionary * queueToApply = [QueueFileArray objectAtIndex:currentQueueEncodeIndex]; hb_list_t * list = hb_get_titles(self.queueCore.hb_handle); hb_title_t * title = (hb_title_t *) hb_list_item( list,0 ); // is always zero since now its a single title scan hb_audio_config_t * audio; hb_filter_object_t * filter; /* Title Angle for dvdnav */ job->angle = [[queueToApply objectForKey:@"TitleAngle"] intValue]; if([[queueToApply objectForKey:@"fEncodeStartStop"] intValue] == 0) { /* Chapter selection */ [HBUtilities writeToActivityLog: "Start / Stop set to chapters"]; job->chapter_start = [[queueToApply objectForKey:@"ChapterStart"] intValue]; job->chapter_end = [[queueToApply objectForKey:@"ChapterEnd"] intValue]; } else if ([[queueToApply objectForKey:@"fEncodeStartStop"] intValue] == 1) { /* we are pts based start / stop */ [HBUtilities writeToActivityLog: "Start / Stop set to seconds…"]; /* Point A to Point B. Time to time in seconds.*/ /* get the start seconds from the start seconds field */ int start_seconds = [[queueToApply objectForKey:@"StartSeconds"] intValue]; job->pts_to_start = start_seconds * 90000LL; /* Stop seconds is actually the duration of encode, so subtract the end seconds from the start seconds */ int stop_seconds = [[queueToApply objectForKey:@"StopSeconds"] intValue]; job->pts_to_stop = stop_seconds * 90000LL; } else if ([[queueToApply objectForKey:@"fEncodeStartStop"] intValue] == 2) { /* we are frame based start / stop */ [HBUtilities writeToActivityLog: "Start / Stop set to frames…"]; /* Point A to Point B. Frame to frame */ /* get the start frame from the start frame field */ int start_frame = [[queueToApply objectForKey:@"StartFrame"] intValue]; job->frame_to_start = start_frame; /* get the frame to stop on from the end frame field */ int stop_frame = [[queueToApply objectForKey:@"StopFrame"] intValue]; job->frame_to_stop = stop_frame; } /* Format (Muxer) and Video Encoder */ job->mux = [[queueToApply objectForKey:@"JobFileFormatMux"] intValue]; job->vcodec = [[queueToApply objectForKey:@"JobVideoEncoderVcodec"] intValue]; /* We set http optimized mp4 here */ if( [[queueToApply objectForKey:@"Mp4HttpOptimize"] intValue] == 1 ) { job->mp4_optimize = 1; } else { job->mp4_optimize = 0; } /* We set the chapter marker extraction here based on the format being mpeg4 or mkv and the checkbox being checked */ if ([[queueToApply objectForKey:@"ChapterMarkers"] intValue] == 1) { job->chapter_markers = 1; /* now lets get our saved chapter names out the array in the queue file * and insert them back into the title chapter list. We have it here, * because unless we are inserting chapter markers there is no need to * spend the overhead of iterating through the chapter names array imo * Also, note that if for some reason we don't apply chapter names, the * chapters just come out 001, 002, etc. etc. */ NSArray *chapterNamesArray = [queueToApply objectForKey:@"ChapterNames"]; int i = 0; for (id tempObject in chapterNamesArray) { hb_chapter_t *chapter = (hb_chapter_t *) hb_list_item( job->list_chapter, i ); if( chapter != NULL ) { hb_chapter_set_title( chapter, [tempObject UTF8String] ); } i++; } } else { job->chapter_markers = 0; } if (job->vcodec == HB_VCODEC_X264 || job->vcodec == HB_VCODEC_X265) { /* iPod 5G atom */ job->ipod_atom = ([[queueToApply objectForKey:@"Mp4iPodCompatible"] intValue] == 1); /* set fastfirstpass if 2-pass and Turbo are enabled */ if ([[queueToApply objectForKey:@"VideoTwoPass"] intValue] == 1) { job->fastfirstpass = ([[queueToApply objectForKey:@"VideoTurboTwoPass"] intValue] == 1); } /* advanced x264 options */ NSString *tmpString; // translate zero-length strings to NULL for libhb const char *encoder_preset = NULL; const char *encoder_tune = NULL; const char *encoder_options = NULL; const char *encoder_profile = NULL; const char *encoder_level = NULL; if ([[queueToApply objectForKey:@"x264UseAdvancedOptions"] intValue]) { // we are using the advanced panel if ([(tmpString = [queueToApply objectForKey:@"x264Option"]) length]) { encoder_options = [tmpString UTF8String]; } } else { // we are using the x264 preset system if ([(tmpString = [queueToApply objectForKey:@"VideoTune"]) length]) { encoder_tune = [tmpString UTF8String]; } if ([(tmpString = [queueToApply objectForKey:@"VideoOptionExtra"]) length]) { encoder_options = [tmpString UTF8String]; } if ([(tmpString = [queueToApply objectForKey:@"VideoProfile"]) length]) { encoder_profile = [tmpString UTF8String]; } if ([(tmpString = [queueToApply objectForKey:@"VideoLevel"]) length]) { encoder_level = [tmpString UTF8String]; } encoder_preset = [[queueToApply objectForKey:@"VideoPreset"] UTF8String]; } hb_job_set_encoder_preset (job, encoder_preset); hb_job_set_encoder_tune (job, encoder_tune); hb_job_set_encoder_options(job, encoder_options); hb_job_set_encoder_profile(job, encoder_profile); hb_job_set_encoder_level (job, encoder_level); } else if (job->vcodec & HB_VCODEC_FFMPEG_MASK) { hb_job_set_encoder_options(job, [[queueToApply objectForKey:@"VideoOptionExtra"] UTF8String]); } /* Picture Size Settings */ job->par.num = [[queueToApply objectForKey:@"PicturePARPixelWidth"] intValue]; job->par.den = [[queueToApply objectForKey:@"PicturePARPixelHeight"] intValue]; /* Video settings */ /* Framerate */ int fps_mode, fps_num, fps_den; if ([[queueToApply objectForKey:@"JobIndexVideoFramerate"] intValue] > 0) { /* a specific framerate has been chosen */ fps_num = 27000000; fps_den = (int)[[queueToApply objectForKey:@"JobIndexVideoFramerate"] intValue]; if ([[queueToApply objectForKey:@"VideoFramerateMode"] isEqualToString:@"cfr"]) { // CFR fps_mode = 1; } else { // PFR fps_mode = 2; } } else { /* same as source */ fps_num = [[queueToApply objectForKey:@"JobVrate"] intValue]; fps_den = [[queueToApply objectForKey:@"JobVrateBase"] intValue]; if ([[queueToApply objectForKey:@"VideoFramerateMode"] isEqualToString:@"cfr"]) { // CFR fps_mode = 1; } else { // VFR fps_mode = 0; } } if ( [[queueToApply objectForKey:@"VideoQualityType"] intValue] != 2 ) { job->vquality = -1.0; job->vbitrate = [[queueToApply objectForKey:@"VideoAvgBitrate"] intValue]; } if ( [[queueToApply objectForKey:@"VideoQualityType"] intValue] == 2 ) { job->vquality = [[queueToApply objectForKey:@"VideoQualitySlider"] floatValue]; job->vbitrate = 0; } job->grayscale = [[queueToApply objectForKey:@"VideoGrayScale"] intValue]; // Map the settings in the dictionaries for the SubtitleList array to match title->list_subtitle BOOL one_burned = NO; int i = 0; NSArray *subtitles = [queueToApply objectForKey:@"SubtitleList"]; for (id subtitleDict in subtitles) { int subtitle = [subtitleDict[keySubTrackIndex] intValue]; int force = [subtitleDict[keySubTrackForced] intValue]; int burned = [subtitleDict[keySubTrackBurned] intValue]; int def = [subtitleDict[keySubTrackDefault] intValue]; // if i is 0, then we are in the first item of the subtitles which we need to // check for the "Foreign Audio Search" which would be keySubTrackIndex of -1 // if we are on the first track and using "Foreign Audio Search" if (i == 0 && subtitle == -1) { [HBUtilities writeToActivityLog: "Foreign Language Search: %d", 1]; job->indepth_scan = 1; if (burned != 1) { job->select_subtitle_config.dest = PASSTHRUSUB; } else { job->select_subtitle_config.dest = RENDERSUB; } job->select_subtitle_config.force = force; job->select_subtitle_config.default_track = def; } else { // if we are getting the subtitles from an external srt file if ([subtitleDict[keySubTrackType] intValue] == SRTSUB) { hb_subtitle_config_t sub_config; sub_config.offset = [subtitleDict[keySubTrackSrtOffset] intValue]; // we need to srncpy file name and codeset strncpy(sub_config.src_filename, [subtitleDict[keySubTrackSrtFilePath] UTF8String], 255); sub_config.src_filename[255] = 0; strncpy(sub_config.src_codeset, [subtitleDict[keySubTrackSrtCharCode] UTF8String], 39); sub_config.src_codeset[39] = 0; if( !burned && hb_subtitle_can_pass( SRTSUB, job->mux ) ) { sub_config.dest = PASSTHRUSUB; } else if( hb_subtitle_can_burn( SRTSUB ) ) { // Only allow one subtitle to be burned into the video if( one_burned ) continue; one_burned = TRUE; sub_config.dest = RENDERSUB; } sub_config.force = 0; sub_config.default_track = def; hb_srt_add( job, &sub_config, [subtitleDict[keySubTrackLanguageIsoCode] UTF8String]); continue; } /* We are setting a source subtitle so access the source subtitle info */ hb_subtitle_t * subt = (hb_subtitle_t *) hb_list_item( title->list_subtitle, subtitle ); if( subt != NULL ) { hb_subtitle_config_t sub_config = subt->config; if( !burned && hb_subtitle_can_pass( subt->source, job->mux ) ) { sub_config.dest = PASSTHRUSUB; } else if( hb_subtitle_can_burn( subt->source ) ) { // Only allow one subtitle to be burned into the video if( one_burned ) continue; one_burned = TRUE; sub_config.dest = RENDERSUB; } sub_config.force = force; sub_config.default_track = def; hb_subtitle_add( job, &sub_config, subtitle ); } } i++; } if( one_burned ) { filter = hb_filter_init( HB_FILTER_RENDER_SUB ); hb_add_filter( job, filter, [[NSString stringWithFormat:@"%d:%d:%d:%d", job->crop[0], job->crop[1], job->crop[2], job->crop[3]] UTF8String] ); } /* Auto Defaults */ NSDictionary *audioDefaults = queueToApply[@"AudioDefaults"]; job->acodec_copy_mask = 0; if ([audioDefaults[@"AudioAllowAACPass"] boolValue]) { job->acodec_copy_mask |= HB_ACODEC_FFAAC; } if ([audioDefaults[@"AudioAllowAC3Pass"] boolValue]) { job->acodec_copy_mask |= HB_ACODEC_AC3; } if ([audioDefaults[@"AudioAllowDTSHDPass"] boolValue]) { job->acodec_copy_mask |= HB_ACODEC_DCA_HD; } if ([audioDefaults[@"AudioAllowDTSPass"] boolValue]) { job->acodec_copy_mask |= HB_ACODEC_DCA; } if ([audioDefaults[@"AudioAllowMP3Pass"] boolValue]) { job->acodec_copy_mask |= HB_ACODEC_MP3; } job->acodec_fallback = hb_audio_encoder_get_from_name([audioDefaults[@"AudioEncoderFallback"] UTF8String]); /* Audio tracks and mixdowns */ /* Now lets add our new tracks to the audio list here */ for (NSDictionary *audioDict in [queueToApply objectForKey:@"AudioList"]) { audio = (hb_audio_config_t *)calloc(1, sizeof(*audio)); hb_audio_config_init(audio); audio->in.track = [audioDict[@"Track"] intValue]; /* We go ahead and assign values to our audio->out. */ audio->out.track = audio->in.track; audio->out.codec = [audioDict[@"JobEncoder"] intValue]; audio->out.compression_level = hb_audio_compression_get_default(audio->out.codec); audio->out.mixdown = [audioDict[@"JobMixdown"] intValue]; audio->out.normalize_mix_level = 0; audio->out.bitrate = [audioDict[@"JobBitrate"] intValue]; audio->out.samplerate = [audioDict[@"JobSamplerate"] intValue]; audio->out.dynamic_range_compression = [audioDict[@"TrackDRCSlider"] doubleValue]; audio->out.gain = [audioDict[@"TrackGainSlider"] doubleValue]; audio->out.dither_method = hb_audio_dither_get_default(); hb_audio_add(job, audio); free(audio); } /* Now lets call the filters if applicable. * The order of the filters is critical */ /* Detelecine */ filter = hb_filter_init( HB_FILTER_DETELECINE ); if ([[queueToApply objectForKey:@"PictureDetelecine"] intValue] == 1) { /* use a custom detelecine string */ hb_add_filter( job, filter, [[queueToApply objectForKey:@"PictureDetelecineCustom"] UTF8String] ); } else if ([[queueToApply objectForKey:@"PictureDetelecine"] intValue] == 2) { /* Use libhb's default values */ hb_add_filter( job, filter, NULL ); } if ([[queueToApply objectForKey:@"PictureDecombDeinterlace"] intValue] == 1) { /* Decomb */ filter = hb_filter_init( HB_FILTER_DECOMB ); if ([[queueToApply objectForKey:@"PictureDecomb"] intValue] == 1) { /* use a custom decomb string */ hb_add_filter( job, filter, [[queueToApply objectForKey:@"PictureDecombCustom"] UTF8String] ); } else if ([[queueToApply objectForKey:@"PictureDecomb"] intValue] == 2) { /* use libhb defaults */ hb_add_filter( job, filter, NULL ); } else if ([[queueToApply objectForKey:@"PictureDecomb"] intValue] == 3) { /* use old defaults (decomb fast) */ hb_add_filter( job, filter, "7:2:6:9:1:80" ); } else if ([[queueToApply objectForKey:@"PictureDecomb"] intValue] == 4) { /* decomb 3 with bobbing enabled */ hb_add_filter( job, filter, "455" ); } } else { /* Deinterlace */ filter = hb_filter_init( HB_FILTER_DEINTERLACE ); if ([[queueToApply objectForKey:@"PictureDeinterlace"] intValue] == 1) { /* we add the custom string if present */ hb_add_filter( job, filter, [[queueToApply objectForKey:@"PictureDeinterlaceCustom"] UTF8String] ); } else if ([[queueToApply objectForKey:@"PictureDeinterlace"] intValue] == 2) { /* Run old deinterlacer fd by default */ hb_add_filter( job, filter, "0" ); } else if ([[queueToApply objectForKey:@"PictureDeinterlace"] intValue] == 3) { /* Yadif mode 0 (without spatial deinterlacing) */ hb_add_filter( job, filter, "1" ); } else if ([[queueToApply objectForKey:@"PictureDeinterlace"] intValue] == 4) { /* Yadif (with spatial deinterlacing) */ hb_add_filter( job, filter, "3" ); } else if ([[queueToApply objectForKey:@"PictureDeinterlace"] intValue] == 5) { /* Yadif (with spatial deinterlacing and bobbing) */ hb_add_filter( job, filter, "15" ); } } /* Denoise */ if (![queueToApply[@"PictureDenoise"] isEqualToString:@"off"]) { int filter_id = HB_FILTER_HQDN3D; if ([queueToApply[@"PictureDenoise"] isEqualToString:@"nlmeans"]) filter_id = HB_FILTER_NLMEANS; if ([queueToApply[@"PictureDenoisePreset"] isEqualToString:@"none"]) { const char *filter_str; filter_str = [queueToApply[@"PictureDenoiseCustom"] UTF8String]; filter = hb_filter_init(filter_id); hb_add_filter(job, filter, filter_str); } else { const char *filter_str, *preset, *tune; preset = [queueToApply[@"PictureDenoisePreset"] UTF8String]; tune = [queueToApply[@"PictureDenoiseTune"] UTF8String]; filter_str = hb_generate_filter_settings(filter_id, preset, tune); filter = hb_filter_init(filter_id); hb_add_filter(job, filter, filter_str); } } /* Deblock (uses pp7 default) */ /* NOTE: even though there is a valid deblock setting of 0 for the filter, for * the macgui's purposes a value of 0 actually means to not even use the filter * current hb_filter_deblock.settings valid ranges are from 5 - 15 */ filter = hb_filter_init( HB_FILTER_DEBLOCK ); if ([[queueToApply objectForKey:@"PictureDeblock"] intValue] != 0) { hb_add_filter( job, filter, [[queueToApply objectForKey:@"PictureDeblock"] UTF8String] ); } /* Add Crop/Scale filter */ filter = hb_filter_init(HB_FILTER_CROP_SCALE); hb_add_filter( job, filter, [[NSString stringWithFormat:@"%d:%d:%d:%d:%d:%d", [queueToApply[@"PictureWidth"] intValue], [queueToApply[@"PictureHeight"] intValue], [queueToApply[@"PictureTopCrop"] intValue], [queueToApply[@"PictureBottomCrop"] intValue], [queueToApply[@"PictureLeftCrop"] intValue], [queueToApply[@"PictureRightCrop"] intValue]] UTF8String] ); /* Add framerate shaping filter */ filter = hb_filter_init(HB_FILTER_VFR); hb_add_filter(job, filter, [[NSString stringWithFormat:@"%d:%d:%d", fps_mode, fps_num, fps_den] UTF8String]); [HBUtilities writeToActivityLog: "prepareJob exiting"]; } /* addToQueue: puts up an alert before ultimately calling doAddToQueue */ - (IBAction) addToQueue: (id) sender { /* We get the destination directory from the destination field here */ NSString *destinationDirectory = [[fDstFile2Field stringValue] stringByDeletingLastPathComponent]; /* We check for a valid destination here */ if ([[NSFileManager defaultManager] fileExistsAtPath:destinationDirectory] == 0) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Warning!"]; [alert setInformativeText:@"This is not a valid destination directory!"]; [alert runModal]; [alert release]; return; } BOOL fileExists; fileExists = NO; BOOL fileExistsInQueue; fileExistsInQueue = NO; /* We check for and existing file here */ if([[NSFileManager defaultManager] fileExistsAtPath: [fDstFile2Field stringValue]]) { fileExists = YES; } /* We now run through the queue and make sure we are not overwriting an exisiting queue item */ for (id tempObject in QueueFileArray) { NSDictionary *thisQueueDict = tempObject; if ([[thisQueueDict objectForKey:@"DestinationPath"] isEqualToString: [fDstFile2Field stringValue]]) { fileExistsInQueue = YES; } } if(fileExists == YES) { NSAlert *alert = [NSAlert alertWithMessageText:@"File already exists." defaultButton:@"Cancel" alternateButton:@"Overwrite" otherButton:nil informativeTextWithFormat:@"Do you want to overwrite %@?", [fDstFile2Field stringValue]]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( overwriteAddToQueueAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; } else if (fileExistsInQueue == YES) { NSAlert *alert = [NSAlert alertWithMessageText:@"There is already a queue item for this destination." defaultButton:@"Cancel" alternateButton:@"Overwrite" otherButton:nil informativeTextWithFormat:@"Do you want to overwrite %@?", [fDstFile2Field stringValue]]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( overwriteAddToQueueAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; } else { [self doAddToQueue]; } } /* overwriteAddToQueueAlertDone: called from the alert posted by addToQueue that asks the user if they want to overwrite an exiting movie file. */ - (void) overwriteAddToQueueAlertDone: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo { if( returnCode == NSAlertAlternateReturn ) [self doAddToQueue]; } - (void) doAddToQueue { [self addQueueFileItem ]; } /* Rip: puts up an alert before ultimately calling doRip */ - (IBAction) Rip: (id) sender { [HBUtilities writeToActivityLog: "Rip: Pending queue count is %d", fPendingCount]; /* Rip or Cancel ? */ if (self.queueCore.state == HBStateWorking || self.queueCore.state == HBStatePaused) { [self Cancel: sender]; return; } /* We check to see if we need to warn the user that the computer will go to sleep or shut down when encoding is finished */ [self remindUserOfSleepOrShutdown]; // If there are pending jobs in the queue, then this is a rip the queue if (fPendingCount > 0) { currentQueueEncodeIndex = [self getNextPendingQueueIndex]; /* here lets start the queue with the first pending item */ [self performNewQueueScan:[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"SourcePath"] scanTitleNum:[[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"TitleNumber"]intValue]]; return; } // Before adding jobs to the queue, check for a valid destination. NSString *destinationDirectory = [[fDstFile2Field stringValue] stringByDeletingLastPathComponent]; if ([[NSFileManager defaultManager] fileExistsAtPath:destinationDirectory] == 0) { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Warning!"]; [alert setInformativeText:@"This is not a valid destination directory!"]; [alert runModal]; [alert release]; return; } /* We check for duplicate name here */ if( [[NSFileManager defaultManager] fileExistsAtPath:[fDstFile2Field stringValue]] ) { NSAlert *alert = [NSAlert alertWithMessageText:@"File already exists." defaultButton:@"Cancel" alternateButton:@"Overwrite" otherButton:nil informativeTextWithFormat:@"Do you want to overwrite %@?", [fDstFile2Field stringValue]]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( overWriteAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; // overWriteAlertDone: will be called when the alert is dismissed. It will call doRip. } else { /* if there are no pending jobs in the queue, then add this one to the queue and rip otherwise, just rip the queue */ if(fPendingCount == 0) { [self doAddToQueue]; } /* go right to processing the new queue encode */ currentQueueEncodeIndex = [self getNextPendingQueueIndex]; [self performNewQueueScan:[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"SourcePath"] scanTitleNum:[[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"TitleNumber"]intValue]]; } } /* overWriteAlertDone: called from the alert posted by Rip: that asks the user if they want to overwrite an exiting movie file. */ - (void) overWriteAlertDone: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo { if( returnCode == NSAlertAlternateReturn ) { /* if there are no jobs in the queue, then add this one to the queue and rip otherwise, just rip the queue */ if( fPendingCount == 0 ) { [self doAddToQueue]; } NSString *destinationDirectory = [[fDstFile2Field stringValue] stringByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setObject:destinationDirectory forKey:@"LastDestinationDirectory"]; currentQueueEncodeIndex = [self getNextPendingQueueIndex]; [self performNewQueueScan:[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"SourcePath"] scanTitleNum:[[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"TitleNumber"]intValue]]; } } - (void) remindUserOfSleepOrShutdown { if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Put Computer To Sleep"]) { /*Warn that computer will sleep after encoding*/ NSBeep(); [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"The computer will sleep after encoding is done."]; [alert setInformativeText:@"You have selected to sleep the computer after encoding. To turn off sleeping, go to the HandBrake preferences."]; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Preferences…"]; NSInteger reminduser = [alert runModal]; [alert release]; if (reminduser == NSAlertSecondButtonReturn) { [self showPreferencesWindow:nil]; } } else if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"AlertWhenDone"] isEqualToString: @"Shut Down Computer"]) { /*Warn that computer will shut down after encoding*/ NSBeep(); [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"The computer will shut down after encoding is done."]; [alert setInformativeText:@"You have selected to shut down the computer after encoding. To turn off shut down, go to the HandBrake preferences."]; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Preferences…"]; NSInteger reminduser = [alert runModal]; if (reminduser == NSAlertSecondButtonReturn) { [self showPreferencesWindow:nil]; } [alert release]; } } - (void) doRip { /* Let libhb do the job */ [self.queueCore start]; /* set the fEncodeState State */ fEncodeState = 1; } //------------------------------------------------------------------------------------ // Displays an alert asking user if the want to cancel encoding of current job. // Cancel: returns immediately after posting the alert. Later, when the user // acknowledges the alert, doCancelCurrentJob is called. //------------------------------------------------------------------------------------ - (IBAction)Cancel: (id)sender { if (!fQueueController) return; /* * No need to allow system sleep here as we'll either call Cancel: * (which will take care of it) or resume right away */ [self.queueCore pause]; // Which window to attach the sheet to? NSWindow * docWindow; if ([sender respondsToSelector: @selector(window)]) { docWindow = [sender window]; } else { docWindow = fWindow; } NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"You are currently encoding. What would you like to do ?", nil)]; [alert setInformativeText:NSLocalizedString(@"Your encode will be cancelled if you don't continue encoding.", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Continue Encoding", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel Current and Stop", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel Current and Continue", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:docWindow modalDelegate:self didEndSelector:@selector(didDimissCancel:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } - (void) didDimissCancel: (NSWindow *)sheet returnCode: (int)returnCode contextInfo: (void *)contextInfo { /* No need to prevent system sleep here as we didn't allow it in Cancel: */ [self.queueCore resume]; if (returnCode == NSAlertSecondButtonReturn) { [self doCancelCurrentJobAndStop]; } else if (returnCode == NSAlertThirdButtonReturn) { [self doCancelCurrentJob]; // <- this also stops libhb } } //------------------------------------------------------------------------------------ // Cancels and deletes the current job and stops libhb from processing the remaining // encodes. //------------------------------------------------------------------------------------ - (void) doCancelCurrentJob { // Stop the current job. [self.queueCore stop]; fEncodeState = 2; // don't alert at end of processing since this was a cancel // now that we've stopped the currently encoding job, lets mark it as cancelled [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] setObject:[NSNumber numberWithInt:3] forKey:@"Status"]; // and as always, save it in Queue.plist /* We save all of the Queue data here */ [self saveQueueFileItem]; // and see if there are more items left in our queue NSInteger queueItems = [QueueFileArray count]; /* If we still have more items in our queue, lets go to the next one */ /* Check to see if there are any more pending items in the queue */ NSInteger newQueueItemIndex = [self getNextPendingQueueIndex]; /* If we still have more pending items in our queue, lets go to the next one */ if (newQueueItemIndex >= 0 && newQueueItemIndex < queueItems) { /*Set our currentQueueEncodeIndex now to the newly found Pending encode as we own it */ currentQueueEncodeIndex = newQueueItemIndex; /* now we mark the queue item as Status = 1 ( being encoded ) so another instance can not come along and try to scan it while we are scanning */ [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] setObject:[NSNumber numberWithInt:1] forKey:@"Status"]; [HBUtilities writeToActivityLog: "incrementQueueItemDone new pending items found: %d", currentQueueEncodeIndex]; [self saveQueueFileItem]; /* now we can go ahead and scan the new pending queue item */ [self performNewQueueScan:[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"SourcePath"] scanTitleNum:[[[QueueFileArray objectAtIndex:currentQueueEncodeIndex] objectForKey:@"TitleNumber"]intValue]]; } else { [HBUtilities writeToActivityLog: "incrementQueueItemDone there are no more pending encodes"]; } } - (void) doCancelCurrentJobAndStop { [self.queueCore stop]; fEncodeState = 2; // don't alert at end of processing since this was a cancel // now that we've stopped the currently encoding job, lets mark it as cancelled [[QueueFileArray objectAtIndex:currentQueueEncodeIndex] setObject:[NSNumber numberWithInt:3] forKey:@"Status"]; // and as always, save it in Queue.plist /* We save all of the Queue data here */ [self saveQueueFileItem]; // so now lets move to currentQueueEncodeIndex++ ; [HBUtilities writeToActivityLog: "cancelling current job and stopping the queue"]; } - (IBAction) Pause: (id) sender { if (self.queueCore.state == HBStatePaused) { [self.queueCore resume]; } else { [self.queueCore pause]; } } #pragma mark - #pragma mark Batch Queue Titles Methods - (IBAction) addAllTitlesToQueue: (id) sender { NSAlert *alert = [NSAlert alertWithMessageText:@"You are about to add ALL titles to the queue!" defaultButton:@"Cancel" alternateButton:@"Yes, I want to add all titles to the queue" otherButton:nil informativeTextWithFormat:@"Current preset will be applied to all %ld titles. Are you sure you want to do this?", (long)[fSrcTitlePopUp numberOfItems]]; [alert setAlertStyle:NSCriticalAlertStyle]; [alert beginSheetModalForWindow:fWindow modalDelegate:self didEndSelector:@selector( addAllTitlesToQueueAlertDone:returnCode:contextInfo: ) contextInfo:NULL]; } - (void) addAllTitlesToQueueAlertDone: (NSWindow *) sheet returnCode: (int) returnCode contextInfo: (void *) contextInfo { if( returnCode == NSAlertAlternateReturn ) [self doAddAllTitlesToQueue]; } - (void) doAddAllTitlesToQueue { /* first get the currently selected index so we can choose it again after cycling through the available titles. */ NSInteger currentlySelectedTitle = [fSrcTitlePopUp indexOfSelectedItem]; /* For each title in the fSrcTitlePopUp, select it */ for( int i = 0; i < [fSrcTitlePopUp numberOfItems]; i++ ) { [fSrcTitlePopUp selectItemAtIndex:i]; /* Now call titlePopUpChanged to load it up */ [self titlePopUpChanged:nil]; /* now add the title to the queue */ [self addToQueue:nil]; } /* Now that we are done, reselect the previously selected title.*/ [fSrcTitlePopUp selectItemAtIndex: currentlySelectedTitle]; /* Now call titlePopUpChanged to load it up */ [self titlePopUpChanged:nil]; } #pragma mark - #pragma mark GUI Controls Changed Methods - (void)updateFileName { if (!self.job) { return; } HBTitle *title = self.job.title; // Generate a new file name NSString *fileName = [HBUtilities automaticNameForSource:title.name title:title.hb_title->index chapters:NSMakeRange([fSrcChapterStartPopUp indexOfSelectedItem] + 1, [fSrcChapterEndPopUp indexOfSelectedItem] + 1) quality:self.job.video.qualityType ? self.job.video.quality : 0 bitrate:!self.job.video.qualityType ? self.job.video.avgBitrate : 0 videoCodec:self.job.video.encoder]; // Swap the old one with the new one [fDstFile2Field setStringValue: [NSString stringWithFormat:@"%@/%@.%@", [[fDstFile2Field stringValue] stringByDeletingLastPathComponent], fileName, [[fDstFile2Field stringValue] pathExtension]]]; } - (IBAction) titlePopUpChanged: (id) sender { // If there is already a title load, save the current settings to a preset if (self.job) { self.selectedPreset = [self createPresetFromCurrentSettings]; } HBTitle *hbtitle = [self.core.titles objectAtIndex:fSrcTitlePopUp.indexOfSelectedItem]; self.job = [[[HBJob alloc] initWithTitle:hbtitle url:[NSURL fileURLWithPath:fSrcDVD2Field.stringValue] andPreset:self.selectedPreset] autorelease]; hb_title_t *title = hbtitle.hb_title; /* If we are a stream type and a batch scan, grok the output file name from title->name upon title change */ if ((title->type == HB_STREAM_TYPE || title->type == HB_FF_STREAM_TYPE) && self.core.titles.count > 1) { /* we set the default name according to the new title->name */ [fDstFile2Field setStringValue: [NSString stringWithFormat: @"%@/%@.%@", [[fDstFile2Field stringValue] stringByDeletingLastPathComponent], [NSString stringWithUTF8String: title->name], [[fDstFile2Field stringValue] pathExtension]]]; /* Change the source to read out the parent folder also */ [fSrcDVD2Field setStringValue:[NSString stringWithFormat:@"%@/%@", browsedSourceDisplayName,[NSString stringWithUTF8String: title->name]]]; } /* For point a to point b pts encoding, set the start and end fields to 0 and the title duration in seconds respectively */ int duration = (title->hours * 3600) + (title->minutes * 60) + (title->seconds); [fSrcTimeStartEncodingField setStringValue: [NSString stringWithFormat: @"%d", 0]]; [fSrcTimeEndEncodingField setStringValue: [NSString stringWithFormat: @"%d", duration]]; /* For point a to point b frame encoding, set the start and end fields to 0 and the title duration * announced fps in seconds respectively */ [fSrcFrameStartEncodingField setStringValue: [NSString stringWithFormat: @"%d", 1]]; //[fSrcFrameEndEncodingField setStringValue: [NSString stringWithFormat: @"%d", ((title->hours * 3600) + (title->minutes * 60) + (title->seconds)) * 24]]; [fSrcFrameEndEncodingField setStringValue: [NSString stringWithFormat: @"%d", duration * (title->vrate.num / title->vrate.den)]]; /* Update encode start / stop variables */ /* Update chapter popups */ [fSrcChapterStartPopUp removeAllItems]; [fSrcChapterEndPopUp removeAllItems]; for( int i = 0; i < hb_list_count( title->list_chapter ); i++ ) { [fSrcChapterStartPopUp addItemWithTitle: [NSString stringWithFormat: @"%d", i + 1]]; [fSrcChapterEndPopUp addItemWithTitle: [NSString stringWithFormat: @"%d", i + 1]]; } [fSrcChapterStartPopUp selectItemAtIndex: 0]; [fSrcChapterEndPopUp selectItemAtIndex: hb_list_count( title->list_chapter ) - 1]; [self chapterPopUpChanged:nil]; [fSrcAnglePopUp removeAllItems]; for( int i = 0; i < title->angle_count; i++ ) { [fSrcAnglePopUp addItemWithTitle: [NSString stringWithFormat: @"%d", i + 1]]; } [fSrcAnglePopUp selectItemAtIndex: 0]; // Set the jobs info to the view controllers fPictureController.picture = self.job.picture; fPictureController.filters = self.job.filters; fPreviewController.job = self.job; fVideoController.video = self.job.video; fAudioController.job = self.job; fSubtitlesViewController.job = self.job; fChapterTitlesController.job = self.job; // Set Auto Crop to on upon selecting a new title self.job.picture.autocrop = YES; /* If Auto Naming is on. We create an output filename of dvd name - title number */ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DefaultAutoNaming"]) { [self updateFileName]; } /* apply the current preset */ [self applyPreset:self.selectedPreset]; } - (IBAction) encodeStartStopPopUpChanged: (id) sender; { if( [fEncodeStartStopPopUp isEnabled] ) { /* We are chapters */ if( [fEncodeStartStopPopUp indexOfSelectedItem] == 0 ) { [fSrcChapterStartPopUp setHidden: NO]; [fSrcChapterEndPopUp setHidden: NO]; [fSrcTimeStartEncodingField setHidden: YES]; [fSrcTimeEndEncodingField setHidden: YES]; [fSrcFrameStartEncodingField setHidden: YES]; [fSrcFrameEndEncodingField setHidden: YES]; [self chapterPopUpChanged:nil]; } /* We are time based (seconds) */ else if ([fEncodeStartStopPopUp indexOfSelectedItem] == 1) { [fSrcChapterStartPopUp setHidden: YES]; [fSrcChapterEndPopUp setHidden: YES]; [fSrcTimeStartEncodingField setHidden: NO]; [fSrcTimeEndEncodingField setHidden: NO]; [fSrcFrameStartEncodingField setHidden: YES]; [fSrcFrameEndEncodingField setHidden: YES]; [self startEndSecValueChanged:nil]; } /* We are frame based */ else if ([fEncodeStartStopPopUp indexOfSelectedItem] == 2) { [fSrcChapterStartPopUp setHidden: YES]; [fSrcChapterEndPopUp setHidden: YES]; [fSrcTimeStartEncodingField setHidden: YES]; [fSrcTimeEndEncodingField setHidden: YES]; [fSrcFrameStartEncodingField setHidden: NO]; [fSrcFrameEndEncodingField setHidden: NO]; [self startEndFrameValueChanged:nil]; } } } - (IBAction) chapterPopUpChanged: (id) sender { /* If start chapter popup is greater than end chapter popup, we set the end chapter popup to the same as start chapter popup */ if ([fSrcChapterStartPopUp indexOfSelectedItem] > [fSrcChapterEndPopUp indexOfSelectedItem]) { [fSrcChapterEndPopUp selectItemAtIndex: [fSrcChapterStartPopUp indexOfSelectedItem]]; } hb_list_t * list = hb_get_titles( self.core.hb_handle); hb_title_t * title = (hb_title_t *) hb_list_item( list, (int)[fSrcTitlePopUp indexOfSelectedItem] ); hb_chapter_t * chapter; int64_t duration = 0; for( NSInteger i = [fSrcChapterStartPopUp indexOfSelectedItem]; i <= [fSrcChapterEndPopUp indexOfSelectedItem]; i++ ) { chapter = (hb_chapter_t *) hb_list_item( title->list_chapter, (int)i ); duration += chapter->duration; } duration /= 90000; /* pts -> seconds */ [fSrcDuration2Field setStringValue: [NSString stringWithFormat: @"%02lld:%02lld:%02lld", duration / 3600, ( duration / 60 ) % 60, duration % 60]]; /* We're changing the chapter range - we may need to flip the m4v/mp4 extension */ if ([[fDstFormatPopUp selectedItem] tag] & HB_MUX_MASK_MP4) { [self autoSetM4vExtension:sender]; } /* If Auto Naming is on it might need to be update if it includes the chapters range */ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DefaultAutoNaming"]) { [self updateFileName]; } } - (IBAction) startEndSecValueChanged: (id) sender { int duration = [fSrcTimeEndEncodingField intValue] - [fSrcTimeStartEncodingField intValue]; [fSrcDuration2Field setStringValue: [NSString stringWithFormat: @"%02d:%02d:%02d", duration / 3600, ( duration / 60 ) % 60, duration % 60]]; } - (IBAction) startEndFrameValueChanged: (id) sender { hb_list_t * list = hb_get_titles(self.core.hb_handle); hb_title_t * title = (hb_title_t*) hb_list_item( list, (int)[fSrcTitlePopUp indexOfSelectedItem] ); int duration = ([fSrcFrameEndEncodingField intValue] - [fSrcFrameStartEncodingField intValue]) / (title->vrate.num / title->vrate.den); [fSrcDuration2Field setStringValue: [NSString stringWithFormat: @"%02d:%02d:%02d", duration / 3600, ( duration / 60 ) % 60, duration % 60]]; } - (IBAction) formatPopUpChanged: (id) sender { NSString *string = [fDstFile2Field stringValue]; int videoContainer = (int)[[fDstFormatPopUp selectedItem] tag]; const char *ext = NULL; // enable chapter markers and hide muxer-specific options [fDstMp4HttpOptFileCheck setHidden:YES]; [fDstMp4iPodFileCheck setHidden:YES]; switch (videoContainer) { case HB_MUX_AV_MP4: [fDstMp4HttpOptFileCheck setHidden:NO]; [fDstMp4iPodFileCheck setHidden:NO]; break; default: break; } // set the file extension ext = hb_container_get_default_extension(videoContainer); [fDstFile2Field setStringValue:[NSString stringWithFormat:@"%@.%s", [string stringByDeletingPathExtension], ext]]; if (videoContainer & HB_MUX_MASK_MP4) { [self autoSetM4vExtension:sender]; } /* post a notification for any interested observers to indicate that our video container has changed */ [[NSNotificationCenter defaultCenter] postNotification: [NSNotification notificationWithName:HBContainerChangedNotification object:self userInfo:[NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:videoContainer], keyContainerTag, nil]]]; [self customSettingUsed:sender]; } - (void) autoSetM4vExtension:(NSNotification *)notification { if (!([[fDstFormatPopUp selectedItem] tag] & HB_MUX_MASK_MP4)) return; NSString * extension = @"mp4"; BOOL anyCodecAC3 = [fAudioController anyCodecMatches: HB_ACODEC_AC3] || [fAudioController anyCodecMatches: HB_ACODEC_AC3_PASS]; /* Chapter markers are enabled if the checkbox is ticked and we are doing p2p or we have > 1 chapter */ BOOL chapterMarkers = (fChapterTitlesController.createChapterMarkers) && ([fEncodeStartStopPopUp indexOfSelectedItem] != 0 || [fSrcChapterStartPopUp indexOfSelectedItem] < [fSrcChapterEndPopUp indexOfSelectedItem]); if ([[[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultMpegExtension"] isEqualToString: @".m4v"] || ((YES == anyCodecAC3 || YES == chapterMarkers) && [[[NSUserDefaults standardUserDefaults] objectForKey:@"DefaultMpegExtension"] isEqualToString: @"Auto"] )) { extension = @"m4v"; } if( [extension isEqualTo: [[fDstFile2Field stringValue] pathExtension]] ) return; else [fDstFile2Field setStringValue: [NSString stringWithFormat:@"%@.%@", [[fDstFile2Field stringValue] stringByDeletingPathExtension], extension]]; } - (void)updateMp4Checkboxes:(NSNotification *)notification { if (self.job.video.encoder != HB_VCODEC_X264) { /* We set the iPod atom checkbox to disabled and uncheck it as its only for x264 in the mp4 * container. Format is taken care of in formatPopUpChanged method by hiding and unchecking * anything other than MP4. */ [fDstMp4iPodFileCheck setEnabled: NO]; [fDstMp4iPodFileCheck setState: NSOffState]; } else { [fDstMp4iPodFileCheck setEnabled: YES]; } } /* Method to determine if we should change the UI To reflect whether or not a Preset is being used or if the user is using "Custom" settings by determining the sender*/ - (IBAction) customSettingUsed: (id) sender { if ([sender stringValue]) { /* Deselect the currently selected Preset if there is one*/ [fPresetsView deselect]; /* Change UI to show "Custom" settings are being used */ [fPresetSelectedDisplay setStringValue: @"Custom"]; self.customPreset = YES; } /* If Auto Naming is on it might need to be update if it includes the quality token */ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DefaultAutoNaming"]) { [self updateFileName]; } } #pragma mark - #pragma mark - Picture /** * Registers changes made in the Picture Settings Window. */ - (void)pictureSettingsDidChange { // align picture settings and video filters in the UI using tabs fVideoController.pictureSettings = [self pictureSettingsSummary]; fVideoController.pictureFilters = self.job.filters.summary; /* Store storage resolution for unparse */ fVideoController.video.widthForUnparse = self.job.picture.width; fVideoController.video.heightForUnparse = self.job.picture.height; [fPreviewController reloadPreviews]; } #pragma mark - #pragma mark - Text Summaries - (NSString *)pictureSettingsSummary { NSMutableString *summary = [NSMutableString stringWithString:@""]; if (self.job.picture) { HBPicture *pict = self.job.picture; [summary appendString:pict.info]; if (pict.anamorphicMode != HB_ANAMORPHIC_STRICT) { // anamorphic is not Strict, show the modulus [summary appendFormat:@", Modulus: %d", pict.modulus]; } [summary appendFormat:@", Crop: %s %d/%d/%d/%d", pict.autocrop ? "Auto" : "Custom", pict.cropTop, pict.cropBottom, pict.cropLeft, pict.cropRight]; } return [NSString stringWithString:summary]; } - (NSString*) muxerOptionsSummary { NSMutableString *summary = [NSMutableString stringWithString:@""]; if ([fDstMp4HttpOptFileCheck isHidden] == NO && [fDstMp4HttpOptFileCheck isEnabled] == YES && [fDstMp4HttpOptFileCheck state] == NSOnState) { [summary appendString:@" - Web optimized"]; } if ([fDstMp4iPodFileCheck isHidden] == NO && [fDstMp4iPodFileCheck isEnabled] == YES && [fDstMp4iPodFileCheck state] == NSOnState) { [summary appendString:@" - iPod 5G support"]; } if ([summary hasPrefix:@" - "]) { [summary deleteCharactersInRange:NSMakeRange(0, 3)]; } return [NSString stringWithString:summary]; } #pragma mark - #pragma mark Open New Windows - (IBAction) openHomepage: (id) sender { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"http://handbrake.fr/"]]; } - (IBAction) openForums: (id) sender { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"http://forum.handbrake.fr/"]]; } - (IBAction) openUserGuide: (id) sender { [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"http://trac.handbrake.fr/wiki/HandBrakeGuide"]]; } /** * Shows debug output window. */ - (IBAction)showDebugOutputPanel:(id)sender { [outputPanel showOutputPanel:sender]; } /** * Shows preferences window. */ - (IBAction) showPreferencesWindow: (id) sender { if (fPreferencesController == nil) { fPreferencesController = [[HBPreferencesController alloc] init]; } NSWindow *window = [fPreferencesController window]; if (![window isVisible]) [window center]; [window makeKeyAndOrderFront: nil]; } /** * Shows queue window. */ - (IBAction) showQueueWindow:(id)sender { [fQueueController showQueueWindow:sender]; } - (IBAction) toggleDrawer:(id)sender { if ([fPresetDrawer state] == NSDrawerClosedState) { [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"HBDefaultPresetsDrawerShow"]; } else { [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"HBDefaultPresetsDrawerShow"]; } [fPresetDrawer toggle:self]; } /** * Shows Picture Settings Window. */ - (IBAction) showPicturePanel: (id) sender { [fPictureController showPictureWindow]; } - (IBAction) showPreviewWindow: (id) sender { [fPreviewController showWindow:sender]; } #pragma mark - Preset Methods - (void)applyPreset:(HBPreset *)preset { if (preset != nil && self.job) { self.selectedPreset = preset; self.customPreset = NO; NSDictionary *chosenPreset = preset.content; [fPresetSelectedDisplay setStringValue:[chosenPreset objectForKey:@"PresetName"]]; if ([[chosenPreset objectForKey:@"Default"] intValue] == 1) { [fPresetSelectedDisplay setStringValue:[NSString stringWithFormat:@"%@ (Default)", [chosenPreset objectForKey:@"PresetName"]]]; } else { [fPresetSelectedDisplay setStringValue:[chosenPreset objectForKey:@"PresetName"]]; } /* File Format */ /* map legacy container names via libhb */ int format = hb_container_get_from_name(hb_container_sanitize_name([chosenPreset[@"FileFormat"] UTF8String])); [fDstFormatPopUp selectItemWithTag:format]; [self formatPopUpChanged:nil]; /* Chapter Markers*/ fChapterTitlesController.createChapterMarkers = [[chosenPreset objectForKey:@"ChapterMarkers"] boolValue]; /* check to see if we have only one chapter */ [self chapterPopUpChanged:nil]; /* Mux mp4 with http optimization */ [fDstMp4HttpOptFileCheck setState:[[chosenPreset objectForKey:@"Mp4HttpOptimize"] intValue]]; /* Lets run through the following functions to get variables set there */ /* Set the state of ipod compatible with Mp4iPodCompatible. Only for x264*/ [fDstMp4iPodFileCheck setState:[[chosenPreset objectForKey:@"Mp4iPodCompatible"] intValue]]; /* Audio */ [fAudioController applySettingsFromPreset: chosenPreset]; /*Subtitles*/ [fSubtitlesViewController applySettingsFromPreset:chosenPreset]; // Apply the preset to the current job [self.job applyPreset:preset]; [self pictureSettingsDidChange]; } } #pragma mark - Presets View Controller Delegate - (void)selectionDidChange { [self applyPreset:fPresetsView.selectedPreset]; } #pragma mark - #pragma mark Manage Presets - (void) checkBuiltInsForUpdates { /* if we have built in presets to update, then do so AlertBuiltInPresetUpdate*/ if ([presetManager checkBuiltInsForUpdates]) { if( [[NSUserDefaults standardUserDefaults] boolForKey:@"AlertBuiltInPresetUpdate"] == YES) { /* Show an alert window that built in presets will be updated */ /*On Screen Notification*/ [NSApp requestUserAttention:NSCriticalRequest]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"HandBrake has determined your built in presets are out of date…"]; [alert setInformativeText:@"HandBrake will now update your built-in presets."]; [alert runModal]; [alert release]; } /* when alert is dismissed, go ahead and update the built in presets */ [presetManager generateBuiltInPresets]; } } - (IBAction)showAddPresetPanel:(id)sender { /* Show the add panel */ HBAddPresetController *addPresetController = [[HBAddPresetController alloc] initWithPreset:[self createPresetFromCurrentSettings] videoSize:NSMakeSize(self.job.picture.width, self.job.picture.height)]; [NSApp beginSheet:addPresetController.window modalForWindow:fWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:addPresetController]; } - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { HBAddPresetController *addPresetController = (HBAddPresetController *)contextInfo; if (returnCode == NSModalResponseContinue) { [presetManager addPreset:addPresetController.preset]; } [addPresetController release]; } - (HBPreset *)createPresetFromCurrentSettings { NSMutableDictionary *preset = [NSMutableDictionary dictionary]; NSDictionary *currentPreset = self.selectedPreset.content; preset[@"PresetBuildNumber"] = [NSString stringWithFormat: @"%d", [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] intValue]]; preset[@"PresetName"] = fPresetSelectedDisplay.stringValue; preset[@"Folder"] = @NO; // Set whether or not this is a user preset or factory 0 is factory, 1 is user preset[@"Type"] = @1; preset[@"Default"] = @0; // Get the whether or not to apply pic Size and Cropping (includes Anamorphic) preset[@"UsesPictureSettings"] = currentPreset[@"UsesPictureSettings"]; // Get whether or not to use the current Picture Filter settings for the preset preset[@"UsesPictureFilters"] = currentPreset[@"UsesPictureFilters"]; preset[@"PresetDescription"] = currentPreset[@"PresetDescription"]; preset[@"FileFormat"] = fDstFormatPopUp.titleOfSelectedItem; preset[@"ChapterMarkers"] = @(fChapterTitlesController.createChapterMarkers); // Mux mp4 with http optimization preset[@"Mp4HttpOptimize"] = @(fDstMp4HttpOptFileCheck.state); // Add iPod uuid atom preset[@"Mp4iPodCompatible"] = @(fDstMp4iPodFileCheck.state); // Video encoder [self.job.video prepareVideoForPreset:preset]; preset[@"PictureWidth"] = currentPreset[@"PictureWidth"]; preset[@"PictureHeight"] = currentPreset[@"PictureHeight"]; // Picture Filters [self.job.filters prepareFiltersForPreset:preset]; // Picture Size [self.job.picture preparePictureForPreset:preset]; // Audio [self.job.audioDefaults prepareAudioDefaultsForPreset:preset]; // Subtitles [self.job.subtitlesDefaults prepareSubtitlesDefaultsForPreset:preset]; return [[[HBPreset alloc] initWithName:preset[@"PresetName"] content:preset builtIn:NO] autorelease]; } #pragma mark - #pragma mark Import Export Preset(s) - (IBAction) browseExportPresetFile: (id) sender { /* Open a panel to let the user choose where and how to save the export file */ NSSavePanel *panel = [NSSavePanel savePanel]; /* We get the current file name and path from the destination field here */ NSString *defaultExportDirectory = [NSString stringWithFormat: @"%@/Desktop/", NSHomeDirectory()]; [panel setDirectoryURL:[NSURL fileURLWithPath:defaultExportDirectory]]; [panel setNameFieldStringValue:@"HB_Export.plist"]; [panel beginSheetModalForWindow:fWindow completionHandler:^(NSInteger result) { if( result == NSOKButton ) { NSURL *exportPresetsFile = [panel URL]; NSURL *presetExportDirectory = [exportPresetsFile URLByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setURL:presetExportDirectory forKey:@"LastPresetExportDirectoryURL"]; /* We check for the presets.plist */ if ([[NSFileManager defaultManager] fileExistsAtPath:[exportPresetsFile path]] == 0) { [[NSFileManager defaultManager] createFileAtPath:[exportPresetsFile path] contents:nil attributes:nil]; } NSMutableArray *presetsToExport = [[[NSMutableArray alloc] initWithContentsOfURL:exportPresetsFile] autorelease]; if (presetsToExport == nil) { presetsToExport = [[NSMutableArray alloc] init]; /* now get and add selected presets to export */ } if (fPresetsView.selectedPreset != nil) { [presetsToExport addObject:[fPresetsView.selectedPreset content]]; [presetsToExport writeToURL:exportPresetsFile atomically:YES]; } } }]; } - (IBAction)browseImportPresetFile:(id)sender { NSOpenPanel *panel; panel = [NSOpenPanel openPanel]; [panel setAllowsMultipleSelection:NO]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:NO]; [panel setAllowedFileTypes:@[@"plist", @"xml"]]; NSURL *sourceDirectory; if ([[NSUserDefaults standardUserDefaults] URLForKey:@"LastPresetImportDirectoryURL"]) { sourceDirectory = [[NSUserDefaults standardUserDefaults] URLForKey:@"LastPresetImportDirectoryURL"]; } else { sourceDirectory = [[NSURL fileURLWithPath:NSHomeDirectory()] URLByAppendingPathComponent:@"Desktop"]; } /* we open up the browse sources sheet here and call for browseSourcesDone after the sheet is closed * to evaluate whether we want to specify a title, we pass the sender in the contextInfo variable */ /* set this for allowed file types, not sure if we should allow xml or not */ [panel setDirectoryURL:sourceDirectory]; [panel beginSheetModalForWindow:fWindow completionHandler:^(NSInteger result) { NSURL *importPresetsFile = [panel URL]; NSURL *importPresetsDirectory = nil;//[importPresetsFile URLByDeletingLastPathComponent]; [[NSUserDefaults standardUserDefaults] setURL:importPresetsDirectory forKey:@"LastPresetImportDirectoryURL"]; /* NOTE: here we need to do some sanity checking to verify we do not hose up our presets file */ NSMutableArray *presetsToImport = [[NSMutableArray alloc] initWithContentsOfURL:importPresetsFile]; /* iterate though the new array of presets to import and add them to our presets array */ for (NSMutableDictionary *tempObject in presetsToImport) { /* make any changes to the incoming preset we see fit */ /* make sure the incoming preset is not tagged as default */ [tempObject setObject:[NSNumber numberWithInt:0] forKey:@"Default"]; /* prepend "(imported) to the name of the incoming preset for clarification since it can be changed */ NSString *prependedName = [@"(import) " stringByAppendingString:[tempObject objectForKey:@"PresetName"]] ; [tempObject setObject:prependedName forKey:@"PresetName"]; /* actually add the new preset to our presets array */ [presetManager addPresetFromDictionary:tempObject]; } [presetsToImport autorelease]; }]; } #pragma mark - #pragma mark Preset Menu - (IBAction)selectDefaultPreset:(id)sender { [self applyPreset:presetManager.defaultPreset]; [fPresetsView setSelection:_selectedPreset]; } - (IBAction)insertFolder:(id)sender { [fPresetsView insertFolder:sender]; } - (IBAction)selectPresetFromMenu:(id)sender { // Retrieve the preset stored in the NSMenuItem HBPreset *preset = [sender representedObject]; [self applyPreset:preset]; [fPresetsView setSelection:preset]; } /** * Adds the presets list to the menu. */ - (void)buildPresetsMenu { // First we remove all the preset menu items // inserted previosly NSArray *menuItems = [presetsMenu.itemArray copy]; for (NSMenuItem *item in menuItems) { if (item.tag != -1) { [presetsMenu removeItem:item]; } } [menuItems release]; __block NSUInteger i = 0; __block BOOL builtInEnded = NO; [presetManager.root enumerateObjectsUsingBlock:^(id obj, NSIndexPath *idx, BOOL *stop) { if (idx.length) { NSMenuItem *item = [[NSMenuItem alloc] init]; item.title = [obj name]; item.tag = i++; // Set an action only to the actual presets, // not on the folders. if ([obj isLeaf]) { item.action = @selector(selectPresetFromMenu:); item.representedObject = obj; } // Make the default preset font bold. if ([obj isDefault]) { NSAttributedString *newTitle = [[NSAttributedString alloc] initWithString:[obj name] attributes:@{NSFontAttributeName: [NSFont boldSystemFontOfSize:14]}]; [item setAttributedTitle:newTitle]; [newTitle release]; } // Add a separator line after the last builtIn preset if ([obj isBuiltIn] == NO && builtInEnded == NO) { [presetsMenu addItem:[NSMenuItem separatorItem]]; builtInEnded = YES; } item.indentationLevel = idx.length - 1; [presetsMenu addItem:item]; [item release]; } }]; } /** * We use this method to recreate new, updated factory presets */ - (IBAction)addFactoryPresets:(id)sender { [presetManager generateBuiltInPresets]; } @end