1 /* $Id: HBPreviewController.mm,v 1.11 2005/08/01 15:10:44 titer Exp $
3 This file is part of the HandBrake source code.
4 Homepage: <http://handbrake.fr/>.
5 It may be used under the terms of the GNU General Public License. */
7 #import "HBPreviewController.h"
10 @implementation QTMovieView ( HBQTkitExt )
11 - (void) mouseMoved:(NSEvent *)theEvent
13 [super mouseMoved:theEvent];
19 @interface PreviewController (Private)
21 - (NSSize)optimalViewSizeForImageSize: (NSSize)imageSize;
22 - (void)resizeSheetForViewSize: (NSSize)viewSize;
23 - (void)setViewSize: (NSSize)viewSize;
24 - (BOOL)viewNeedsToResizeToSize: (NSSize)newSize;
28 @implementation PreviewController
32 if (self = [super initWithWindowNibName:@"PicturePreview"])
34 // NSWindowController likes to lazily load its window. However since
35 // this controller tries to set all sorts of outlets before the window
36 // is displayed, we need it to load immediately. The correct way to do
37 // this, according to the documentation, is simply to invoke the window
40 // If/when we switch a lot of this stuff to bindings, this can probably
44 fPicturePreviews = [[NSMutableDictionary dictionaryWithCapacity: HB_NUM_HBLIB_PICTURES] retain];
45 /* Init libhb with check for updates libhb style set to "0" so its ignored and lets sparkle take care of it */
46 int loggingLevel = [[[NSUserDefaults standardUserDefaults] objectForKey:@"LoggingLevel"] intValue];
47 fPreviewLibhb = hb_init(loggingLevel, 0);
56 //------------------------------------------------------------------------------------
57 // Displays and brings the picture window to the front
58 //------------------------------------------------------------------------------------
59 - (IBAction) showPreviewWindow: (id)sender
61 [self showWindow:sender];
62 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"PreviewWindowIsOpen"];
64 /* lets set the preview window to accept mouse moved events */
65 [fPreviewWindow setAcceptsMouseMovedEvents:YES];
67 [self pictureSliderChanged:nil];
68 [self startReceivingLibhbNotifications];
71 - (void)setHBController: (HBController *)controller
73 fHBController = controller;
78 [fPreviewWindow setDelegate:self];
79 if( ![[self window] setFrameUsingName:@"Preview"] )
80 [[self window] center];
81 [self setWindowFrameAutosaveName:@"Preview"];
82 [[self window] setExcludedFromWindowsMenu:YES];
84 /* lets set the preview window to accept mouse moved events */
85 [fPreviewWindow setAcceptsMouseMovedEvents:YES];
86 //[self pictureSliderChanged:nil];
87 [self startReceivingLibhbNotifications];
90 /* we set the progress indicator to not use threaded animation
91 * as it causes a conflict with the qtmovieview's controllerbar
93 [fMovieCreationProgressIndicator setUsesThreadedAnimation:NO];
95 /* Setup our layers for core animation */
96 [fPictureViewArea setWantsLayer:YES];
97 [fPictureView setWantsLayer:YES];
99 [fCancelPreviewMovieButton setWantsLayer:YES];
100 [fMovieCreationProgressIndicator setWantsLayer:YES];
102 [fPictureControlBox setWantsLayer:YES];
103 [fEncodingControlBox setWantsLayer:YES];
104 [fMovieView setWantsLayer:YES];
105 [fMovieView setHidden:YES];
106 [fMovieView setDelegate:self];
108 /* Since the xib has everything off center for easy acess
109 * we align our views and windows here we an align to anything
110 * since it will actually change later upon source load, but
111 * for convenience we will use the fPictureViewArea
114 /* Align the still preview image view to the picture box */
115 [fPictureView setFrameSize:[fPictureViewArea frame].size];
116 [fMovieView setFrameSize:[fPictureViewArea frame].size];
117 //[fPreviewWindow setFrameSize:[fPictureViewArea frame].size];
121 - (BOOL)acceptsMouseMovedEvents
126 - (void)windowWillClose:(NSNotification *)aNotification
128 /* Upon Closing the picture window, we make sure we clean up any
129 * preview movie that might be playing
131 hb_stop( fPreviewLibhb );
133 // Show the picture view
134 [fPictureView setHidden:NO];
135 [fMovieView pause:nil];
136 [fMovieTimer invalidate];
137 [fMovieTimer release];
138 [fMovieView setHidden:YES];
139 [fMovieView setMovie:nil];
142 [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"PreviewWindowIsOpen"];
145 - (BOOL)windowShouldClose:(id)fPictureWindow
152 hb_stop(fPreviewLibhb);
153 if (fPreviewMoviePath)
155 [[NSFileManager defaultManager] removeFileAtPath:fPreviewMoviePath handler:nil];
156 [fPreviewMoviePath release];
159 [fLibhbTimer invalidate];
160 [fLibhbTimer release];
162 [fHudTimer invalidate];
165 [fMovieTimer invalidate];
166 [fMovieTimer release];
168 [fPicturePreviews release];
169 [fFullScreenWindow release];
171 hb_close(&fPreviewLibhb);
173 [self removeMovieCallbacks];
178 - (void) SetHandle: (hb_handle_t *) handle
184 /* we set the preview length popup in seconds */
185 [fPreviewMovieLengthPopUp removeAllItems];
186 [fPreviewMovieLengthPopUp addItemWithTitle: @"5"];
187 [fPreviewMovieLengthPopUp addItemWithTitle: @"10"];
188 [fPreviewMovieLengthPopUp addItemWithTitle: @"15"];
189 [fPreviewMovieLengthPopUp addItemWithTitle: @"20"];
190 [fPreviewMovieLengthPopUp addItemWithTitle: @"25"];
191 [fPreviewMovieLengthPopUp addItemWithTitle: @"30"];
192 [fPreviewMovieLengthPopUp addItemWithTitle: @"35"];
193 [fPreviewMovieLengthPopUp addItemWithTitle: @"40"];
194 [fPreviewMovieLengthPopUp addItemWithTitle: @"45"];
195 [fPreviewMovieLengthPopUp addItemWithTitle: @"50"];
196 [fPreviewMovieLengthPopUp addItemWithTitle: @"55"];
197 [fPreviewMovieLengthPopUp addItemWithTitle: @"60"];
199 /* adjust the preview slider length */
200 /* We use our advance pref to determine how many previews we scanned */
201 int hb_num_previews = [[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewsNumber"] intValue];
202 [fPictureSlider setMaxValue: hb_num_previews - 1.0];
203 [fPictureSlider setNumberOfTickMarks: hb_num_previews];
205 if ([[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewLength"])
207 [fPreviewMovieLengthPopUp selectItemWithTitle:[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewLength"]];
211 /* currently hard set default to 10 seconds */
212 [fPreviewMovieLengthPopUp selectItemAtIndex: 1];
216 - (void) SetTitle: (hb_title_t *) title
218 hb_job_t * job = title->job;
222 MaxOutputWidth = title->width - job->crop[2] - job->crop[3];
223 MaxOutputHeight = title->height - job->crop[0] - job->crop[1];
225 [self SettingsChanged: nil];
227 /* set the top of the hud controller boxes centered vertically with the origin of our window */
228 NSPoint hudControlBoxOrigin = [fPictureControlBox frame].origin;
229 hudControlBoxOrigin.y = ([[self window] frame].size.height / 2) - [fPictureControlBox frame].size.height;
230 [fPictureControlBox setFrameOrigin:hudControlBoxOrigin];
231 [fEncodingControlBox setFrameOrigin:hudControlBoxOrigin];
232 [fMoviePlaybackControlBox setFrameOrigin:hudControlBoxOrigin];
238 // Adjusts the window to draw the current picture (fPicture) adjusting its size as
239 // necessary to display as much of the picture as possible.
240 - (void) displayPreview
242 hb_job_t * job = fTitle->job;
243 /* lets make sure that the still picture view is not hidden and that
244 * the movie preview is
247 [fMovieView pause:nil];
248 [fMovieView setHidden:YES];
249 [fMovieView setMovie:nil];
250 [fMovieCreationProgressIndicator stopAnimation: nil];
251 [fMovieCreationProgressIndicator setHidden: YES];
252 [fMoviePlaybackControlBox setHidden: YES];
253 [self stopMovieTimer];
254 [fPictureControlBox setHidden: NO];
256 [fPictureView setHidden:NO];
258 NSImage *fPreviewImage = [self imageForPicture: fPicture];
259 NSSize imageScaledSize = [fPreviewImage size];
260 [fPictureView setImage: fPreviewImage];
262 NSSize displaySize = NSMakeSize( ( CGFloat )fTitle->width, ( CGFloat )fTitle->height );
263 NSString *sizeInfoString;
264 /* Set the picture size display fields below the Preview Picture*/
265 if( fTitle->job->anamorphic.mode == 1 ) // Original PAR Implementation
267 output_width = fTitle->width-fTitle->job->crop[2]-fTitle->job->crop[3];
268 output_height = fTitle->height-fTitle->job->crop[0]-fTitle->job->crop[1];
269 display_width = output_width * fTitle->job->anamorphic.par_width / fTitle->job->anamorphic.par_height;
270 sizeInfoString = [NSString stringWithFormat:
271 @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d Strict",
272 fTitle->width, fTitle->height, output_width, output_height, display_width, output_height];
274 displaySize.width = display_width;
275 displaySize.height = fTitle->height;
276 imageScaledSize.width = display_width;
277 imageScaledSize.height = output_height;
279 else if (fTitle->job->anamorphic.mode == 2) // Loose Anamorphic
281 hb_set_anamorphic_size(job, &output_width, &output_height, &output_par_width, &output_par_height);
282 display_width = output_width * output_par_width / output_par_height;
283 sizeInfoString = [NSString stringWithFormat:
284 @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d Loose",
285 fTitle->width, fTitle->height, output_width, output_height, display_width, output_height];
287 displaySize.width = display_width;
288 displaySize.height = fTitle->height;
289 imageScaledSize.width = display_width;
290 imageScaledSize.height = output_height;
292 else if (fTitle->job->anamorphic.mode == 3) // Custom Anamorphic
294 hb_set_anamorphic_size(job, &output_width, &output_height, &output_par_width, &output_par_height);
295 display_width = output_width * output_par_width / output_par_height;
296 sizeInfoString = [NSString stringWithFormat:
297 @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d Custom",
298 fTitle->width, fTitle->height, output_width, output_height, fTitle->job->anamorphic.dar_width, fTitle->job->anamorphic.dar_height];
300 displaySize.width = fTitle->job->anamorphic.dar_width + fTitle->job->crop[2] + fTitle->job->crop[3] ;
301 displaySize.height = fTitle->job->anamorphic.dar_height + fTitle->job->crop[0] + fTitle->job->crop[1];
302 imageScaledSize.width = (int)fTitle->job->anamorphic.dar_width;
303 imageScaledSize.height = (int)fTitle->job->height;
305 else // No Anamorphic
307 sizeInfoString = [NSString stringWithFormat:
308 @"Source: %dx%d, Output: %dx%d", fTitle->width, fTitle->height,
309 fTitle->job->width, fTitle->job->height];
311 displaySize.width = fTitle->width;
312 displaySize.height = fTitle->height;
313 imageScaledSize.width = fTitle->job->width;
314 imageScaledSize.height = fTitle->job->height;
319 NSSize viewSize = [self optimalViewSizeForImageSize:displaySize];
320 [self resizeSheetForViewSize:viewSize];
322 NSSize windowSize = [[self window] frame].size;
324 if (scaleToScreen == YES)
326 /* Note: this should probably become a utility function */
327 /* We are in Scale To Screen mode so, we have to get the ratio for height and width against the window
328 *size so we can scale from there.
330 CGFloat deltaWidth = imageScaledSize.width / displaySize.width;
331 CGFloat deltaHeight = imageScaledSize.height /displaySize.height;
332 NSSize windowSize = [[self window] frame].size;
333 CGFloat pictureAspectRatio = imageScaledSize.width / imageScaledSize.height;
335 /* Set our min size to the storage size */
337 minSize.width = fTitle->width;
338 minSize.height = fTitle->height;
340 /* Set delta's based on minimum size */
341 if (imageScaledSize.width < minSize.width)
343 deltaWidth = imageScaledSize.width / minSize.width;
350 if (imageScaledSize.height < minSize.height)
352 deltaHeight = imageScaledSize.height / minSize.height;
359 /* Now apply our deltas to the full screen view */
360 if (pictureAspectRatio > 1.0) // we are wider than taller, so expand the width to fill the area and scale the height
362 viewSize.width = windowSize.width * deltaWidth;
363 viewSize.height = viewSize.width / pictureAspectRatio;
368 viewSize.height = windowSize.height * deltaHeight;
369 viewSize.width = viewSize.height * pictureAspectRatio;
375 viewSize.width = viewSize.width - (viewSize.width - imageScaledSize.width);
376 viewSize.height = viewSize.height - (viewSize.height - imageScaledSize.height);
378 if (fTitle->width > windowSize.width || fTitle->height > windowSize.height)
380 CGFloat viewSizeAspect = viewSize.width / viewSize.height;
381 if (viewSizeAspect > 1.0) // we are wider than taller, so expand the width to fill the area and scale the height
383 viewSize.width = viewSize.width * (windowSize.width / fTitle->width) ;
384 viewSize.height = viewSize.width / viewSizeAspect;
388 viewSize.height = viewSize.height * (windowSize.height / fTitle->height);
389 viewSize.width = viewSize.height * viewSizeAspect;
395 [self setViewSize:viewSize];
397 NSString *scaleString;
398 CGFloat scale = ( ( CGFloat )[fPictureView frame].size.width) / ( ( CGFloat )imageScaledSize.width);
399 if (scale * 100.0 != 100)
401 scaleString = [NSString stringWithFormat:
402 NSLocalizedString( @" (%.0f%% actual size)",
403 @"String shown when a preview is scaled" ), scale * 100.0];
407 scaleString = @"(Actual size)";
410 if (scaleToScreen == YES)
412 scaleString = [scaleString stringByAppendingString:@" Scaled To Screen"];
414 /* Set the info fields in the hud controller */
415 [fInfoField setStringValue: [NSString stringWithFormat:
416 @"%@", sizeInfoString]];
418 [fscaleInfoField setStringValue: [NSString stringWithFormat:
419 @"%@", scaleString]];
420 /* Set the info field in the window title bar */
421 [[self window] setTitle:[NSString stringWithFormat: @"Preview - %@ %@",sizeInfoString, scaleString]];
424 - (IBAction) previewDurationPopUpChanged: (id) sender
427 [[NSUserDefaults standardUserDefaults] setObject:[fPreviewMovieLengthPopUp titleOfSelectedItem] forKey:@"PreviewLength"];
431 - (IBAction) SettingsChanged: (id) sender
433 // Purge the existing picture previews so they get recreated the next time
435 [self purgeImageCache];
436 [self pictureSliderChanged:nil];
439 - (IBAction) pictureSliderChanged: (id) sender
441 // Show the picture view
442 [fPictureView setHidden:NO];
443 [fMovieView pause:nil];
444 [fMovieView setHidden:YES];
445 [fMovieView setMovie:nil];
446 [fEncodingControlBox setHidden: YES];
448 int newPicture = [fPictureSlider intValue];
449 if (newPicture != fPicture)
451 fPicture = newPicture;
453 [self displayPreview];
457 - (IBAction)showPreviewPanel: (id)sender forTitle: (hb_title_t *)title
459 if ([fPreviewWindow isVisible])
461 [fPreviewWindow close];
465 [self showWindow:sender];
466 [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"PreviewWindowIsOpen"];
467 [fPreviewWindow setAcceptsMouseMovedEvents:YES];
469 [self pictureSliderChanged:nil];
474 - (NSString*) pictureSizeInfoString
476 return [fInfoField stringValue];
479 - (IBAction)showPictureSettings:(id)sender
481 [fHBController showPicturePanel:self];
484 #pragma mark Hud Control Overlay
485 - (void) mouseMoved:(NSEvent *)theEvent
487 [super mouseMoved:theEvent];
488 NSPoint mouseLoc = [theEvent locationInWindow];
490 /* Test for mouse location to show/hide hud controls */
491 if( isEncoding == NO )
493 /* Since we are not encoding, verify which control hud to show
494 * or hide based on aMovie ( aMovie indicates we need movie controls )
496 NSBox * hudBoxToShow;
497 if ( aMovie == nil ) // No movie loaded up
499 hudBoxToShow = fPictureControlBox;
501 else // We have a movie
503 hudBoxToShow = fMoviePlaybackControlBox;
506 if( NSPointInRect( mouseLoc, [fPictureControlBox frame] ) )
508 [[hudBoxToShow animator] setHidden: NO];
511 else if( NSPointInRect( mouseLoc, [fPictureViewArea frame] ) )
513 [[hudBoxToShow animator] setHidden: NO];
514 [self startHudTimer];
518 [[hudBoxToShow animator] setHidden: YES];
523 - (void) startHudTimer
526 [fHudTimer invalidate];
529 fHudTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(hudTimerFired:) userInfo:nil repeats:YES];
533 - (void) stopHudTimer
537 [fHudTimer invalidate];
544 - (void) hudTimerFired: (NSTimer*)theTimer
547 if( hudTimerSeconds >= 10 )
549 /* Regardless which control box is active, after the timer
550 * period we want either one to fade to hidden.
552 [[fPictureControlBox animator] setHidden: YES];
553 [[fMoviePlaybackControlBox animator] setHidden: YES];
560 - (IBAction)toggleScaleToScreen:(id)sender
562 if (scaleToScreen == YES)
565 /* make sure we are set to a still preview */
566 [self pictureSliderChanged:nil];
567 [fScaleToScreenToggleButton setTitle:@"Scale To Screen"];
572 /* make sure we are set to a still preview */
573 [self pictureSliderChanged:nil];
574 [fScaleToScreenToggleButton setTitle:@"Actual Scale"];
581 // Title-less windows normally don't receive key presses, override this
582 - (BOOL)canBecomeKeyWindow
587 // Title-less windows normally can't become main which means that another
588 // non-fullscreen window will have the "active" titlebar in expose. Bad, fix it.
589 - (BOOL)canBecomeMainWindow
595 - (IBAction)goWindowedScreen:(id)sender
598 /* Get the screen info to release the display but don't actually do
599 * it until the windowed screen is setup.
602 [self pictureSliderChanged:nil];
603 [fScaleToScreenToggleButton setTitle:@"<->"];
605 NSScreen* mainScreen = [NSScreen mainScreen];
606 NSDictionary* screenInfo = [mainScreen deviceDescription];
607 NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"];
608 CGDirectDisplayID displayID = (CGDirectDisplayID)[screenID longValue];
610 [fFullScreenWindow dealloc];
611 [fFullScreenWindow release];
614 [fPreviewWindow setContentView:fPictureViewArea];
615 [fPictureViewArea setNeedsDisplay:YES];
616 [self setWindow:fPreviewWindow];
619 [fPreviewWindow makeKeyAndOrderFront:self];
621 /* Set the window back to regular level */
622 [fPreviewWindow setLevel:NSNormalWindowLevel];
624 /* Set the isFullScreen flag back to NO */
627 /* make sure we are set to a still preview */
628 [self pictureSliderChanged:nil];
629 [self showPreviewWindow:nil];
631 /* Change the name of fFullScreenToggleButton appropriately */
632 //[fFullScreenToggleButton setTitle: @"Full Screen"];
633 // [fScaleToScreenToggleButton setHidden:YES];
634 /* set the picture settings pallete back to normal level */
635 [fHBController picturePanelWindowed];
637 /* Release the display now that the we are back in windowed mode */
638 CGDisplayRelease(displayID);
640 [fPreviewWindow setAcceptsMouseMovedEvents:YES];
641 //[fFullScreenWindow setAcceptsMouseMovedEvents:NO];
644 [self startHudTimer];
649 #pragma mark Still Preview Image Processing
652 // This function converts an image created by libhb (specified via pictureIndex) into
653 // an NSImage suitable for the GUI code to use. If removeBorders is YES,
654 // makeImageForPicture crops the image generated by libhb stripping off the gray
655 // border around the content. This is the low-level method that generates the image.
656 // -imageForPicture calls this function whenever it can't find an image in its cache.
657 + (NSImage *) makeImageForPicture: (int)pictureIndex
658 libhb:(hb_handle_t*)handle
659 title:(hb_title_t*)title
661 static uint8_t * buffer;
662 static int bufferSize;
664 // Make sure we have a big enough buffer to receive the image from libhb. libhb
665 int dstWidth = title->job->width;
666 int dstHeight = title->job->height;
669 newSize = dstWidth * dstHeight * 4;
670 if( bufferSize < newSize )
672 bufferSize = newSize;
673 buffer = (uint8_t *) realloc( buffer, bufferSize );
676 hb_get_preview( handle, title, pictureIndex, buffer );
678 // Create an NSBitmapImageRep and copy the libhb image into it, converting it from
679 // libhb's format to one suitable for NSImage. Along the way, we'll strip off the
680 // border around libhb's image.
682 // The image data returned by hb_get_preview is 4 bytes per pixel, BGRA format.
685 NSBitmapFormat bitmapFormat = (NSBitmapFormat)NSAlphaFirstBitmapFormat;
686 NSBitmapImageRep * imgrep = [[[NSBitmapImageRep alloc]
687 initWithBitmapDataPlanes:nil
691 samplesPerPixel:3 // ignore alpha
694 colorSpaceName:NSCalibratedRGBColorSpace
695 bitmapFormat:bitmapFormat
696 bytesPerRow:dstWidth * 4
697 bitsPerPixel:32] autorelease];
699 UInt32 * src = (UInt32 *)buffer;
700 UInt32 * dst = (UInt32 *)[imgrep bitmapData];
702 for (r = 0; r < dstHeight; r++)
704 for (c = 0; c < dstWidth; c++)
705 #if TARGET_RT_LITTLE_ENDIAN
706 *dst++ = Endian32_Swap(*src++);
712 NSImage * img = [[[NSImage alloc] initWithSize: NSMakeSize(dstWidth, dstHeight)] autorelease];
713 [img addRepresentation:imgrep];
718 // Returns the preview image for the specified index, retrieving it from its internal
719 // cache or by calling makeImageForPicture if it is not cached. Generally, you should
720 // use imageForPicture so that images are cached. Calling makeImageForPicture will
721 // always generate a new copy of the image.
722 - (NSImage *) imageForPicture: (int) pictureIndex
724 // The preview for the specified index may not currently exist, so this method
725 // generates it if necessary.
726 NSString * key = [NSString stringWithFormat:@"%d", pictureIndex];
727 NSImage * theImage = [fPicturePreviews objectForKey:key];
730 theImage = [PreviewController makeImageForPicture:pictureIndex libhb:fHandle title:fTitle];
731 [fPicturePreviews setObject:theImage forKey:key];
736 // Purges all images from the cache. The next call to imageForPicture will cause a new
737 // image to be generated.
738 - (void) purgeImageCache
740 [fPicturePreviews removeAllObjects];
745 #pragma mark Movie Preview
746 - (IBAction) createMoviePreview: (id) sender
750 /* Lets make sure the still picture previews are showing in case
751 * there is currently a movie showing */
752 [self pictureSliderChanged:nil];
754 /* Rip or Cancel ? */
756 hb_get_state2( fPreviewLibhb, &s );
758 if(sender == fCancelPreviewMovieButton && (s.state == HB_STATE_WORKING || s.state == HB_STATE_PAUSED))
760 hb_stop( fPreviewLibhb );
761 [fPictureView setHidden:NO];
762 [fMovieView pause:nil];
763 [fMovieView setHidden:YES];
764 [fMovieView setMovie:nil];
765 [fPictureSlider setHidden:NO];
772 /* we use controller.mm's prepareJobForPreview to go ahead and set all of our settings
773 * however, we want to use a temporary destination field of course
774 * so that we do not put our temp preview in the users chosen
777 hb_job_t * job = fTitle->job;
779 /* We run our current setting through prepeareJob in Controller.mm
780 * just as if it were a regular encode */
782 [fHBController prepareJobForPreview];
784 /* Destination file. We set this to our preview directory
785 * changing the extension appropriately.*/
786 if (fTitle->job->mux == HB_MUX_MP4) // MP4 file
788 /* we use .m4v for our mp4 files so that ac3 and chapters in mp4 will play properly */
789 fPreviewMoviePath = @"~/Library/Application Support/HandBrake/Previews/preview_temp.m4v";
791 else if (fTitle->job->mux == HB_MUX_MKV) // MKV file
793 fPreviewMoviePath = @"~/Library/Application Support/HandBrake/Previews/preview_temp.mkv";
795 else if (fTitle->job->mux == HB_MUX_AVI) // AVI file
797 fPreviewMoviePath = @"~/Library/Application Support/HandBrake/Previews/preview_temp.avi";
799 else if (fTitle->job->mux == HB_MUX_OGM) // OGM file
801 fPreviewMoviePath = @"~/Library/Application Support/HandBrake/Previews/preview_temp.ogm";
804 fPreviewMoviePath = [[fPreviewMoviePath stringByExpandingTildeInPath]retain];
806 /* See if there is an existing preview file, if so, delete it */
807 if( ![[NSFileManager defaultManager] fileExistsAtPath:fPreviewMoviePath] )
809 [[NSFileManager defaultManager] removeFileAtPath:fPreviewMoviePath
813 /* We now direct our preview encode to fPreviewMoviePath */
814 fTitle->job->file = [fPreviewMoviePath UTF8String];
816 /* We use our advance pref to determine how many previews to scan */
817 int hb_num_previews = [[[NSUserDefaults standardUserDefaults] objectForKey:@"PreviewsNumber"] intValue];
818 job->start_at_preview = fPicture + 1;
819 job->seek_points = hb_num_previews;
821 /* we use the preview duration popup to get the specified
822 * number of seconds for the preview encode.
825 job->pts_to_stop = [[fPreviewMovieLengthPopUp titleOfSelectedItem] intValue] * 90000LL;
827 /* lets go ahead and send it off to libhb
828 * Note: unlike a full encode, we only send 1 pass regardless if the final encode calls for 2 passes.
829 * this should suffice for a fairly accurate short preview and cuts our preview generation time in half.
830 * However we also need to take into account the indepth scan for subtitles.
833 * If scanning we need to do some extra setup of the job.
835 if( job->indepth_scan == 1 )
840 * When subtitle scan is enabled do a fast pre-scan job
841 * which will determine which subtitles to enable, if any.
844 x264opts_tmp = job->x264opts;
846 job->x264opts = NULL;
847 job->indepth_scan = 1;
849 * Add the pre-scan job
851 hb_add( fPreviewLibhb, job );
852 job->x264opts = x264opts_tmp;
854 /* Go ahead and perform the actual encoding preview scan */
855 job->indepth_scan = 0;
857 hb_add( fPreviewLibhb, job );
859 [fEncodingControlBox setHidden: NO];
860 [fPictureControlBox setHidden: YES];
862 [fMovieCreationProgressIndicator setHidden: NO];
863 [fPreviewMovieStatusField setHidden: NO];
867 /* Let fPreviewLibhb do the job */
868 hb_start( fPreviewLibhb );
872 - (void) startReceivingLibhbNotifications
876 fLibhbTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(libhbTimerFired:) userInfo:nil repeats:YES];
877 [fLibhbTimer retain];
881 - (void) stopReceivingLibhbNotifications
885 [fLibhbTimer invalidate];
886 [fLibhbTimer release];
890 - (void) libhbTimerFired: (NSTimer*)theTimer
893 hb_get_state( fPreviewLibhb, &s );
894 [self libhbStateChanged: s];
898 - (void) libhbStateChanged: (hb_state_t)state
900 switch( state.state )
903 case HB_STATE_SCANNING:
904 case HB_STATE_SCANDONE:
907 case HB_STATE_WORKING:
909 #define p state.param.working
911 NSMutableString * string;
912 /* Update text field */
913 string = [NSMutableString stringWithFormat: NSLocalizedString( @"Encoding preview: %.2f %%", @"" ), 100.0 * p.progress];
917 [string appendFormat:
918 NSLocalizedString( @" (%.2f fps, avg %.2f fps, ETA %02dh%02dm%02ds)", @"" ),
919 p.rate_cur, p.rate_avg, p.hours, p.minutes, p.seconds];
921 [fPreviewMovieStatusField setStringValue: string];
923 [fMovieCreationProgressIndicator setIndeterminate: NO];
925 [fMovieCreationProgressIndicator setDoubleValue: 100.0 * p.progress];
927 [fCreatePreviewMovieButton setTitle: @"Cancel Preview"];
934 #define p state.param.muxing
935 case HB_STATE_MUXING:
937 // Update fMovieCreationProgressIndicator
938 [fMovieCreationProgressIndicator setIndeterminate: YES];
939 [fMovieCreationProgressIndicator startAnimation: nil];
940 [fPreviewMovieStatusField setStringValue: [NSString stringWithFormat:
941 NSLocalizedString( @"Muxing Preview ...", @"" )]];
945 case HB_STATE_PAUSED:
946 [fMovieCreationProgressIndicator stopAnimation: nil];
949 case HB_STATE_WORKDONE:
951 // Delete all remaining jobs since libhb doesn't do this on its own.
953 while( ( job = hb_job(fPreviewLibhb, 0) ) )
954 hb_rem( fHandle, job );
956 [fPreviewMovieStatusField setStringValue: @""];
957 [fPreviewMovieStatusField setHidden: YES];
959 [fMovieCreationProgressIndicator stopAnimation: nil];
960 [fMovieCreationProgressIndicator setHidden: YES];
961 [fEncodingControlBox setHidden: YES];
962 [fPictureControlBox setHidden: YES];
965 // Show the movie view
966 [self showMoviePreview:fPreviewMoviePath];
967 [fCreatePreviewMovieButton setTitle: @"Live Preview"];
974 - (IBAction) toggleMoviePreviewPlayPause: (id) sender
976 /* make sure a movie is even loaded up */
979 /* For some stupid reason there is no "isPlaying" method for a QTMovie
980 * object, given that, we detect the rate to determine whether the movie
983 if ([aMovie rate] != 0) // we are playing
985 [fMovieView pause:aMovie];
986 [fPlayPauseButton setTitle: @">"];
988 else // we are paused or stopped
990 [fMovieView play:aMovie];
991 [fPlayPauseButton setTitle: @"||"];
997 - (IBAction) moviePlaybackGoToBeginning: (id) sender
999 /* make sure a movie is even loaded up */
1002 [fMovieView gotoBeginning:aMovie];
1007 - (IBAction) moviePlaybackGoToEnd: (id) sender
1009 /* make sure a movie is even loaded up */
1012 [fMovieView gotoEnd:aMovie];
1017 - (IBAction) moviePlaybackGoBackwardOneFrame: (id) sender
1019 /* make sure a movie is even loaded up */
1022 [fMovieView pause:aMovie]; // Pause the movie
1023 [fMovieView stepBackward:aMovie];
1028 - (IBAction) moviePlaybackGoForwardOneFrame: (id) sender
1030 /* make sure a movie is even loaded up */
1033 [fMovieView pause:aMovie]; // Pause the movie
1034 [fMovieView stepForward:aMovie];
1040 - (void) startMovieTimer
1043 [fMovieTimer invalidate];
1044 [fMovieTimer release];
1046 fMovieTimer = [NSTimer scheduledTimerWithTimeInterval:0.10 target:self selector:@selector(movieTimerFired:) userInfo:nil repeats:YES];
1047 [fMovieTimer retain];
1050 - (void) stopMovieTimer
1054 [fMovieTimer invalidate];
1055 [fMovieTimer release];
1060 - (void) movieTimerFired: (NSTimer*)theTimer
1064 [self adjustPreviewScrubberForCurrentMovieTime];
1065 [fMovieInfoField setStringValue: [NSString stringWithFormat:NSLocalizedString( @"%@", @"" ),[self calculatePlaybackSMTPETimecodeForDisplay]]];
1071 - (IBAction) showMoviePreview: (NSString *) path
1073 /* Since the gray background for the still images is part of
1074 * fPictureView, lets leave the picture view visible and postion
1075 * the fMovieView over the image portion of fPictureView so
1076 * we retain the gray cropping border we have already established
1077 * with the still previews
1080 /* Load the new movie into fMovieView */
1085 NSURL *movieUrl = [NSURL fileURLWithPath:path];
1086 NSDictionary *movieAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
1087 movieUrl, QTMovieURLAttribute,
1088 [NSNumber numberWithBool:NO], QTMovieAskUnresolvedDataRefsAttribute,
1089 [NSNumber numberWithBool:YES], @"QTMovieOpenForPlaybackAttribute",
1090 [NSNumber numberWithBool:NO], @"QTMovieOpenAsyncRequiredAttribute",
1091 [NSNumber numberWithBool:NO], @"QTMovieOpenAsyncOKAttribute",
1092 [NSNumber numberWithBool:YES], @"QTMovieIsSteppableAttribute",
1093 QTMovieApertureModeClean, QTMovieApertureModeAttribute,
1096 aMovie = [[[QTMovie alloc] initWithAttributes:movieAttributes error:&outError] autorelease];
1101 NSLog(@"Unable to open movie");
1106 /* we get some size information from the preview movie */
1107 NSSize movieSize= [[aMovie attributeForKey:QTMovieNaturalSizeAttribute] sizeValue];
1108 movieBounds = [fMovieView movieBounds];
1109 movieBounds.size.height = movieSize.height;
1110 /* We also get our view size to use for scaling fMovieView's size */
1111 NSSize scaledMovieViewSize = [fPictureView frame].size;
1112 [fMovieView setControllerVisible:FALSE];
1113 if ([fMovieView isControllerVisible])
1115 CGFloat controllerBarHeight = [fMovieView controllerBarHeight];
1116 if ( controllerBarHeight != 0 ) //Check if QTKit return a real value or not.
1118 movieBounds.size.height += controllerBarHeight;
1119 scaledMovieViewSize.height += controllerBarHeight;
1123 movieBounds.size.height += 15;
1124 scaledMovieViewSize.height += 15;
1128 movieBounds.size.width = movieSize.width;
1130 /* we need to account for an issue where the scaledMovieViewSize > the window size */
1131 if (scaledMovieViewSize.height > [[self window] frame].size.height)
1133 [fHBController writeToActivityLog: "showMoviePreview: Our window is not tall enough to show the controller bar ..."];
1138 /* Scale the fMovieView to scaledMovieViewSize */
1139 [fMovieView setFrameSize:scaledMovieViewSize];
1141 /*set our origin try using fPictureViewArea or fPictureView */
1142 NSPoint origin = [fPictureView frame].origin;
1143 origin.x += trunc( ( [fPictureView frame].size.width -
1144 [fMovieView frame].size.width ) / 2.0 );
1145 origin.y += trunc( ( ( [fPictureView frame].size.height -
1146 [fMovieView frame].size.height ) / 2.0 ) - 7.5 );
1148 [fMovieView setFrameOrigin:origin];
1149 [fMovieView setMovie:aMovie];
1150 [fMovieView setHidden:NO];
1151 [fMoviePlaybackControlBox setHidden: NO];
1152 [fPictureControlBox setHidden: YES];
1154 // to actually play the movie
1156 [self initPreviewScrubberForMovie];
1157 [self startMovieTimer];
1158 /* Install amovie notifications */
1159 [aMovie setDelegate:self];
1160 [self installMovieCallbacks];
1161 [fMovieView play:aMovie];
1167 #pragma mark *** Movie Playback Scrubber and time code methods ***
1169 /* Since MacOSX Leopard QTKit has taken over some responsibility for assessing movie playback
1170 * information from the old QuickTime carbon api ( time code information as well as fps, etc.).
1171 * However, the QTKit devs at apple were not really big on documentation and further ...
1172 * QuickTimes ability to playback HB's largely variable framerate output makes perfectly frame
1173 * accurate information at best convoluted. Still, for the purpose of a custom hud based custom
1174 * playback scrubber slider this has so far proven to be as accurate as I have found. To say it
1175 * could use some better accuracy is not understating it enough probably.
1176 * Most of this was gleaned from this obscure Apple Mail list thread:
1177 * http://www.mailinglistarchive.com/quicktime-api@lists.apple.com/msg05642.html
1178 * Now as we currently do not show a QTKit control bar with scrubber for display sizes > container
1179 * size, this seems to facilitate playback control from the HB custom HUD controller fairly close
1180 * to the built in controller bar.
1181 * Further work needs to be done to try to get accurate frame by frame playback display if we want it.
1182 * Note that the keyboard commands for frame by frame step through etc. work as always.
1185 // Returns a human readable string from the currentTime of movie playback
1186 - (NSString*) calculatePlaybackSMTPETimecodeForDisplay
1188 QTTime time = [aMovie currentTime];
1190 NSString *smtpeTimeCodeString;
1191 int days, hour, minute, second, frame;
1194 result = time.timeValue / time.timeScale; // second
1195 frame = (time.timeValue % time.timeScale) / 100;
1197 second = result % 60;
1199 result = result / 60; // minute
1200 minute = result % 60;
1202 result = result / 60; // hour
1206 smtpeTimeCodeString = [NSString stringWithFormat:@"Time: %02d:%02d:%02d", hour, minute, second]; // hh:mm:ss
1207 return smtpeTimeCodeString;
1212 // Initialize the preview scrubber min/max to appropriate values for the current movie
1213 -(void) initPreviewScrubberForMovie
1218 QTTime duration = [aMovie duration];
1219 float result = duration.timeValue / duration.timeScale;
1221 [fMovieScrubberSlider setMinValue:0.0];
1222 [fMovieScrubberSlider setMaxValue: (float)result];
1223 [fMovieScrubberSlider setFloatValue: 0.0];
1228 -(void) adjustPreviewScrubberForCurrentMovieTime
1232 QTTime time = [aMovie currentTime];
1234 float result = (float)time.timeValue / (float)time.timeScale;;
1235 [fMovieScrubberSlider setFloatValue:result];
1239 - (IBAction) previewScrubberChanged: (id) sender
1243 [fMovieView pause:aMovie]; // Pause the movie
1244 QTTime time = [aMovie currentTime];
1245 [self setTime: time.timeScale * [fMovieScrubberSlider floatValue]];
1246 [self calculatePlaybackSMTPETimecodeForDisplay];
1249 #pragma mark *** Movie Notifications ***
1251 - (void) installMovieCallbacks
1255 /*Notification for any time the movie rate changes */
1256 [[NSNotificationCenter defaultCenter] addObserver:self
1257 selector:@selector(movieRateDidChange:)
1258 name:@"QTMovieRateDidChangeNotification"
1260 /*Notification for when the movie ends */
1261 [[NSNotificationCenter defaultCenter] addObserver:self
1262 selector:@selector(movieDidEnd:)
1263 name:@"QTMovieDidEndNotification"
1267 - (void)removeMovieCallbacks
1271 /*Notification for any time the movie rate changes */
1272 [[NSNotificationCenter defaultCenter] removeObserver:self
1273 name:@"QTMovieRateDidChangeNotification"
1275 /*Notification for when the movie ends */
1276 [[NSNotificationCenter defaultCenter] removeObserver:self
1277 name:@"QTMovieDidEndNotification"
1282 - (void)movieRateDidChange:(NSNotification *)notification
1286 /* For some stupid reason there is no "isPlaying" method for a QTMovie
1287 * object, given that, we detect the rate to determine whether the movie
1288 * is playing or not.
1290 //[self adjustPreviewScrubberForCurrentMovieTime];
1291 if ([aMovie rate] != 0) // we are playing
1293 [fPlayPauseButton setTitle: @"||"];
1295 else // we are paused or stopped
1297 [fPlayPauseButton setTitle: @">"];
1301 /* This notification is not currently used. However we should keep it "just in case" as
1302 * live preview playback is enhanced.
1304 - (void)movieDidEnd:(NSNotification *)notification
1307 //[fHBController writeToActivityLog: "Movie DidEnd Notification Received"];
1311 #pragma mark *** QTTime Utilities ***
1313 // convert a time value (long) to a QTTime structure
1314 -(void)timeToQTTime:(long)timeValue resultTime:(QTTime *)aQTTime
1316 NSNumber *timeScaleObj;
1317 long timeScaleValue;
1319 timeScaleObj = [aMovie attributeForKey:QTMovieTimeScaleAttribute];
1320 timeScaleValue = [timeScaleObj longValue];
1322 *aQTTime = QTMakeTime(timeValue, timeScaleValue);
1325 // set the movie's current time
1326 -(void)setTime:(int)timeValue
1329 NSValue *valueForQTTime;
1331 [self timeToQTTime:timeValue resultTime:&movieQTTime];
1333 valueForQTTime = [NSValue valueWithQTTime:movieQTTime];
1335 [aMovie setAttribute:valueForQTTime forKey:QTMovieCurrentTimeAttribute];
1341 @implementation PreviewController (Private)
1344 // -[PictureController(Private) optimalViewSizeForImageSize:]
1346 // Given the size of the preview image to be shown, returns the best possible
1347 // size for the view.
1349 - (NSSize)optimalViewSizeForImageSize: (NSSize)imageSize
1351 // The min size is 480x360
1352 CGFloat minWidth = 480.0;
1353 CGFloat minHeight = 360.0;
1355 NSSize screenSize = [[[self window] screen] visibleFrame].size;
1356 NSSize sheetSize = [[self window] frame].size;
1357 NSSize viewAreaSize = [fPictureViewArea frame].size;
1358 CGFloat paddingX = 0.00;
1359 CGFloat paddingY = 0.00;
1361 if (fTitle->width > screenSize.width || fTitle->height > screenSize.height)
1363 if (scaleToScreen == YES)
1365 paddingX = screenSize.width - imageSize.width;
1366 paddingY = screenSize.height - imageSize.height;
1371 paddingX = sheetSize.width - viewAreaSize.width;
1372 paddingY = sheetSize.height - viewAreaSize.height;
1379 maxWidth = screenSize.width - paddingX;
1380 maxHeight = screenSize.height - paddingY;
1382 NSSize resultSize = imageSize;
1383 CGFloat resultPar = resultSize.width / resultSize.height;
1385 //note, a mbp 15" at 1440 x 900 is a 1.6 ar
1386 CGFloat screenAspect = screenSize.width / screenSize.height;
1387 // Note, a standard dvd will use 720 x 480 which is a 1.5
1388 CGFloat viewAreaAspect = viewAreaSize.width / viewAreaSize.height;
1390 if (scaleToScreen == YES)
1393 if (screenAspect < viewAreaAspect)
1395 resultSize.width = screenSize.width;
1396 resultSize.height = (screenSize.width / viewAreaAspect);
1400 resultSize.height = screenSize.height;
1401 resultSize.width = resultSize.height * viewAreaAspect;
1405 else if ( resultSize.width > maxWidth || resultSize.height > maxHeight )
1407 // Source is larger than screen in one or more dimensions
1408 if ( resultPar > screenAspect )
1410 // Source aspect wider than screen aspect, snap to max width and vary height
1411 resultSize.width = maxWidth;
1412 resultSize.height = (maxWidth / resultPar);
1416 // Source aspect narrower than screen aspect, snap to max height vary width
1417 resultSize.height = maxHeight;
1418 resultSize.width = (maxHeight * resultPar);
1422 // If necessary, grow to minimum dimensions to ensure controls overlay is not obstructed
1423 if ( resultSize.width < minWidth )
1425 resultSize.width = minWidth;
1427 if ( resultSize.height < minHeight )
1429 resultSize.height = minHeight;
1438 // -[PictureController(Private) resizePanelForViewSize:animate:]
1440 // Resizes the entire window to accomodate a view of a particular size.
1442 - (void)resizeSheetForViewSize: (NSSize)viewSize
1444 // Figure out the deltas for the new frame area
1445 NSSize currentSize = [fPictureViewArea frame].size;
1446 CGFloat deltaX = viewSize.width - currentSize.width;
1447 CGFloat deltaY = viewSize.height - currentSize.height;
1449 // Now resize the whole panel by those same deltas, but don't exceed the min
1450 NSRect frame = [[self window] frame];
1451 NSSize maxSize = [[[self window] screen] visibleFrame].size;
1452 /* if we are not Scale To Screen, put an 85% of visible screen on the window */
1453 if (scaleToScreen == NO )
1455 maxSize.width = maxSize.width * 0.85;
1456 maxSize.height = maxSize.height * 0.85;
1459 /* Set our min size to the storage size */
1461 minSize.width = fTitle->width;
1462 minSize.height = fTitle->height;
1464 frame.size.width += deltaX;
1465 frame.size.height += deltaY;
1466 if( frame.size.width < minSize.width )
1468 frame.size.width = minSize.width;
1471 if( frame.size.height < minSize.height )
1473 frame.size.height = minSize.height;
1475 /* compare frame to max size of screen */
1477 if( frame.size.width > maxSize.width )
1479 frame.size.width = maxSize.width;
1482 if( frame.size.height > maxSize.height )
1484 frame.size.height = maxSize.height;
1491 // But now the sheet is off-center, so also shift the origin to center it and
1492 // keep the top aligned.
1493 if( frame.size.width != [[self window] frame].size.width )
1494 frame.origin.x -= (deltaX / 2.0);
1497 /* Since upon launch we can open up the preview window if it was open
1498 * the last time we quit (and at the size it was) we want to make
1499 * sure that upon resize we do not have the window off the screen
1500 * So check the origin against the screen origin and adjust if
1503 NSSize screenSize = [[[self window] screen] visibleFrame].size;
1504 NSPoint screenOrigin = [[[self window] screen] frame].origin;
1505 if (screenSize.height < frame.size.height)
1507 frame.size.height = screenSize.height;
1509 if (screenSize.width < frame.size.width)
1511 frame.size.width = screenSize.width;
1515 /* our origin is off the screen to the left*/
1516 if (frame.origin.x < screenOrigin.x)
1518 /* so shift our origin to the right */
1519 frame.origin.x = screenOrigin.x;
1521 else if ((frame.origin.x + frame.size.width) > (screenOrigin.x + screenSize.width))
1523 /* the right side of the preview is off the screen, so shift to the left */
1524 frame.origin.x = (screenOrigin.x + screenSize.width) - frame.size.width;
1527 [[self window] setFrame:frame display:YES animate:YES];
1533 // -[PictureController(Private) setViewSize:]
1535 // Changes the view's size and centers it vertically inside of its area.
1536 // Assumes resizeSheetForViewSize: has already been called.
1538 - (void)setViewSize: (NSSize)viewSize
1541 /* special case for scaleToScreen */
1542 NSSize screenSize = [[[self window] screen] visibleFrame].size;
1543 NSSize areaSize = [fPictureViewArea frame].size;
1544 NSSize pictureSize = [fPictureView frame].size;
1545 CGFloat viewSizeAspect = viewSize.width / viewSize.height;
1547 if (viewSize.width > areaSize.width || viewSize.height > areaSize.height)
1550 if (viewSizeAspect > 1.0) // we are wider than taller, so expand the width to fill the area and scale the height
1552 viewSize.width = areaSize.width;
1553 viewSize.height = viewSize.width / viewSizeAspect;
1557 viewSize.height = areaSize.height;
1558 viewSize.width = viewSize.height * viewSizeAspect;
1563 [fPictureView setFrameSize:viewSize];
1564 NSSize newAreaSize = [fPictureViewArea frame].size;
1567 // center it vertically and horizontally
1568 NSPoint origin = [fPictureViewArea frame].origin;
1569 origin.y += ([fPictureViewArea frame].size.height -
1570 [fPictureView frame].size.height) / 2.0;
1572 origin.x += ([fPictureViewArea frame].size.width -
1573 [fPictureView frame].size.width) / 2.0;
1575 origin.x = floor( origin.x );
1576 origin.y = floor( origin.y );
1578 [fPictureView setFrameOrigin:origin];
1583 - (BOOL)viewNeedsToResizeToSize: (NSSize)newSize
1585 NSSize viewSize = [fPictureViewArea frame].size;
1586 return (newSize.width != viewSize.width || newSize.height != viewSize.height);