Quartz Composer app creates files that can be opened by QuickTime Player. QTMovie can also open these files to play in a QTMovieView. One thing you should understand before jumping into creating “qtz” files is the concept of a “Rendering Destination”. Quartz Composer files define image compositions that are a function of time, input parameters and environment variables. These compositions are rendered dynamically into a “destination”. The destination’s exact dimensions are not a known constant. Instead it is up to you to create a Quartz Composer file that can adapt to variable dimensions (e.g. preserving an aspect ratio or centering an image). When you open a “qtz” file with QTMovie, you can define the “Rendering Destination” and the duration of the movie. This is done by changing the Quartz Composer environment. There are three user defaults that define the destination’s size and movie duration. If you would like to define the state that a Quartz composition uses when rendering you must define this environment before you initialize a QTMovie with a “qtz” file. After the movie has been initialized you can change this environment for another movie.
static void SetQuartzComposerState(int width, int height, float duration) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:[NSNumber numberWithInt:width] forKey:@”QuartzComposerDefaultMovieWidth”]; [defaults setObject:[NSNumber numberWithInt:height] forKey:@”QuartzComposerDefaultMovieHeight”]; [defaults setObject:[NSNumber numberWithFloat:time] forKey:@”QuartzComposerDefaultMovieDuration”]; }
(void)openQuartzComposerFileAtPath:(NSString *)file {
if (file pathExtension] isEqualToString:@”qtz”]) { // set Quartz Composer environment [[SetQuartzComposerState(320, 240, 60.0f); // open quarts movie QTMovie *movie = [QTMovie movieWithFile:file error:nil]; // set movie (movieView is a QTMovieView) [movieView setMovie:movie]; } }
The default destination size is 640x480 and the default duration is 30 seconds (the minimum duration is 3 seconds).
Below is sample code and the basic steps to open and export a “qtz” file using QTKit.
*Open XCode *Create a new “Cocoa Document-based Application” project (File->New Project->Cocoa Document-based Application) *Edit the “MyDocument.m” file so it looks like the code below. *Add the QTKit framework to the doc-app target. *Build/Run.
There are four movies involved in this example:
*Movie A - quicktime movie with 30 fps video (this can be any movie) *Movie B - QTMovie initialized with a “qtz” file that uses “Movie A” as source material *Movie C - CustomMovie object used to create and export a new movie from frames rendered by “Movie B” *Movie D - QTMovie initialized with a reference to “Movie A” to get unique frame times since Quartz Composer movies (e.g. Movie B) do not have discrete frame times.
The macro definition “OpenNewFilesWithSharedWorkspace” can be used to view the two resource files generated by this code. Just set “OpenNewFilesWithSharedWorkspace” to 1 if you would like “Quartz Composer” and “QuickTime Player” to automatically open these files for you. The “qtz” file and source movie are located in the newly created app’s resource directory (hopefully you already know how to view an app’s package contents with the Finder).
A “qtz” file is just a property list. You can edit it in “Property List Editor” or change input parameters directly by editing an NSMutableDictionary initialized with the contents of a “qtz” file and then writing this file back to disk.
This sample code generates a generic “qtz” file for you, but once you become familiar with QuartzComposer and “qtz” files you can create movies with more advanced filters and effects. This code also shows you how to render specific frames by setting the Quartz movie’s current time and creating NSImage objects using QTMovie’s instance method currentFrameImage. Once you have access to individual frame images, the possibilities are endless.
This is only a starting point and there are many performance optimizations that can be pursued. For example, you could set the context, that the Quartz movie draws into, to a pixel buffer so you can pull frames faster. Or, you could figure out the lower level QuickTime calls needed to add raw image sample data directly to the export movie to avoid the performance hit caused by using addImage:forDuration:withAttributes:.
–zootbobbalu
#import “MyDocument.h” #import <QTKit/QTKit.h>
#ifndef PostError #define PostError(n, …) {error = [NSString stringWithFormat:n, ## VA_ARGS]; goto ERROR;} #endif
#define OpenNewFilesWithSharedWorkspace 0
@interface CustomMovie : QTMovie { DataHandler outputHandler; Handle dataHandle; NSString *tempPath; QTVisualContextRef context; unsigned cntxWidth, cntxHeight; }
@interface MyDocument (Private)
@end
////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// //// //// MyDocument //// ////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation MyDocument
(id)init {
if (self = [super init]) {
} return self;
}
}
(void)savePanelDidEnd:(NSOpenPanel *)openPanel returnCode:(int)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSOKButton) { NSArray *filenames = [openPanel filenames]; if ([filenames count] == 1) { NSString *path = [filenames lastObject]; if (!path pathExtension] isEqualToString:@”mov”]) path = [path stringByAppendingPathExtension:@”mov”];
BOOL isDir;
[[NSFileManager *manager = [NSFileManager defaultManager];
if ([manager fileExistsAtPath:path isDirectory:&isDir] && !isDir)
[manager removeFileAtPath:path handler:nil];
NSString *resDir = [self resourceDirectory];
[self exportMovieWithQuartzPath:[resDir stringByAppendingPathComponent:@"test.qtz"]
sourcePath:[resDir stringByAppendingPathComponent:@"source.mov"]
exportPath:path];
} else {
int result =
NSRunAlertPanel(@"Only one file can be exported at a time", nil, @"OK", @"Cancel", nil);
if (result == NSAlertDefaultReturn)
[self export];
} } }
(void)windowControllerDidLoadNib:(NSWindowController *)aController {
static BOOL isFirstLoad = YES;
[super windowControllerDidLoadNib:aController]; QTMovieView *movieView = [self setupWindow:[aController window]];
NSString *resourceDirectory = [self resourceDirectory];
if (resourceDirectory && ![movieView movie]) {
NSString *qtzPath = [resourceDirectory stringByAppendingPathComponent:@"test.qtz"];
NSString *moviePath = [resourceDirectory stringByAppendingPathComponent:@"source.mov"];
if (isFirstLoad) {
isFirstLoad = NO;
NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:moviePath])
[self createMovieAtPath:moviePath];
#if OpenNewFilesWithSharedWorkspace == 1 NSWorkspace *sws = [NSWorkspace sharedWorkspace]; [sws openFile:moviePath withApplication:@”QuickTime Player”]; #endif
[manager removeFileAtPath:qtzPath handler:nil];
[self createQuartzCompositionAtPath:qtzPath moviePath:moviePath];
}
[movieView setMovie:[QTMovie movieWithFile:qtzPath error:nil]];
[self performSelector:@selector(export) withObject:nil afterDelay:1.0f];
}
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// //// //// MyDocument (Private) //// ////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation MyDocument (Private)
<plist version="1.0">\n\
\n
</plist>”;
char *xmlUTF8 = (char *)[xmlString UTF8String];
NSPropertyListFormat format = NSPropertyListXMLFormat_v1_0;
return [NSPropertyListSerialization propertyListFromData:[NSData dataWithBytes:xmlUTF8 length:strlen(xmlUTF8)]
mutabilityOption:NSPropertyListMutableContainersAndLeaves
format:&format
errorDescription:(void *)NULL];
}
(void)createMovieAtPath:(NSString *)path {
NSString *cachePath = [path stringByAppendingPathExtension:@”cache”]; [[NSFileManager defaultManager] removeFileAtPath:cachePath handler:nil]; CustomMovie *mov = [[CustomMovie alloc] initWithCacheDirectory:[path stringByDeletingLastPathComponent] width:360 height:240]; NSRect r = NSMakeRect(0.0f, 0.0f, 360.0f, 240.0f); NSImage *img = [[NSImage alloc] initWithSize:r.size]; NSDictionary *att = [NSDictionary dictionaryWithObjectsAndKeys: [NSFont fontWithName:@”Courier” size:22.0f], NSFontAttributeName, [NSColor whiteColor], NSForegroundColorAttributeName, nil];
NSDictionary *movieAtt = [NSDictionary dictionaryWithObjectsAndKeys: @”mp4v”, QTAddImageCodecType, [NSNumber numberWithLong:codecHighQuality], QTAddImageCodecQuality, nil]; int i; for (i = 0; i < 90; i++) { NSAutoreleasePool *innerPool = [[NSAutoreleasePool alloc] init]; [img lockFocus]; [[NSColor blueColor] set]; NSRectFill(r); NSString *num = [NSString stringWithFormat:@”%i”, i]; NSSize size = [num sizeWithAttributes:att]; NSRect numRect = NSInsetRect(r, (NSWidth(r) - size.width) / 2.0f, (NSHeight(r) - size.height) / 2.0f); [num drawInRect:numRect withAttributes:att]; [img unlockFocus]; [mov addImage:img forDuration:QTMakeTime(20, 600) withAttributes:movieAtt]; [innerPool release]; if ((i % 30) == 0) NSLog(@”<%p>%s: frame: %i”, self, PRETTY_FUNCTION, i); }
[img release];
[mov writeToFile:path atomically:YES]; [mov release];
}
(void)createQuartzCompositionAtPath:(NSString *)path moviePath:(NSString *)moviePath {
NSMutableDictionary *quartzComposition = [NSMutableDictionary dictionary]; [quartzComposition setValue:@”617 572 512 430 0 0 1280 1002 “ forKey:@”editorViewerWindow”]; [quartzComposition setValue:[NSDictionary dictionaryWithObject:moviePath forKey:@”Movie_Path”] forKey:@”inputParameters”]; [quartzComposition setValue:[self rootPatch] forKey:@”rootPatch”]; NSData *xmlData = [NSPropertyListSerialization dataFromPropertyList:quartzComposition format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil]; [xmlData writeToFile:path atomically:YES];
#if OpenNewFilesWithSharedWorkspace == 1 NSWorkspace *sws = [NSWorkspace sharedWorkspace]; [sws openFile:path withApplication:@”Quartz Composer”]; #endif
}
(NSString *)resourceDirectory { NSString *resourcePath = [[NSBundle mainBundle] resourcePath]; NSArray *comps = [resourcePath pathComponents]; unsigned cmp_cnt = [comps count]; if ((cmp_cnt > 3) && [comps objectAtIndex:cmp_cnt - 3] pathExtension] isEqualToString:@”app”]) return resourcePath; return nil; }
([[QTMovieView *)setupWindow:(NSWindow *)window { NSView *contentView = [window contentView]; QTMovieView *movieView = [[QTMovieView alloc] initWithFrame:[contentView frame]]; [window setContentView:movieView]; [movieView release]; [movieView setAutoresizingMask:18]; [movieView setControllerVisible:YES]; [movieView setPreservesAspectRatio:YES]; return movieView; }
(void)nextFrame:(NSMutableDictionary *)info {
QTMovie *quartzMovie = [info objectForKey:@”quartzMovie”]; QTMovie *sourceMovie = [info objectForKey:@”sourceMovie”]; CustomMovie *exportMovie = [info objectForKey:@”exportMovie”]; QTTime lastTime = info objectForKey:@”lastTime”] [[QTTimeValue]; int duplicateCount = info objectForKey:@”duplicateCount”] intValue]; [[NSDictionary *exportAttributes = [info objectForKey:@”exportAttributes”]; NSWindow *window = [self windowForSheet];
NSImage *img = [quartzMovie currentFrameImage]; QTTime sourceDuration = [sourceMovie duration];
[sourceMovie stepForward]; QTTime time = [sourceMovie currentTime]; [quartzMovie setCurrentTime:time];
QTTime frameDuration = time; frameDuration.timeValue = time.timeValue - lastTime.timeValue; [exportMovie addImage:img forDuration:frameDuration withAttributes:exportAttributes];
if (QTTimeCompare(time, lastTime) == NSOrderedSame) duplicateCount++;
if (duplicateCount > 2) { NSString *exportPath = [info objectForKey:@”exportPath”]; [exportMovie writeToFile:exportPath atomically:YES]; [window setTitle:@”Done”];
#if OpenNewFilesWithSharedWorkspace == 1 NSWorkspace *sws = [NSWorkspace sharedWorkspace]; [sws openFile:exportPath withApplication:@”QuickTime Player”]; #endif
} else {
float percentDone = (float)time.timeValue / (float)sourceDuration.timeValue * 100.0f;
[window setTitle:[NSString stringWithFormat:@"%.2f Percent Done", percentDone]];
[info setValue:[NSNumber numberWithInt:duplicateCount] forKey:@"duplicateCount"];
[info setValue:[NSValue valueWithQTTime:time] forKey:@"lastTime"];
[self performSelector:@selector(nextFrame:) withObject:info afterDelay:0.0f];
}
}
(void)exportMovieWithQuartzPath:(NSString *)qtzPath sourcePath:(NSString *)moviePath exportPath:(NSString *)exportPath {
[NSApp activateIgnoringOtherApps:YES]; QTMovie *quartzMovie = [QTMovie movieWithFile:qtzPath error:nil]; QTMovie *sourceMovie = [QTMovie movieWithFile:moviePath error:nil]; CustomMovie *exportMovie = [[CustomMovie alloc] initWithCacheDirectory:[exportPath stringByDeletingLastPathComponent] width:360 height:240]; NSDictionary *exportAttributes = [NSDictionary dictionaryWithObjectsAndKeys: @”mp4v”, QTAddImageCodecType, [NSNumber numberWithLong:codecHighQuality], QTAddImageCodecQuality, nil];
if (quartzMovie && sourceMovie && exportMovie && exportAttributes) { [quartzMovie setCurrentTime:QTZeroTime]; [sourceMovie setCurrentTime:QTZeroTime]; QTTime time = [sourceMovie currentTime]; NSMutableDictionary *info = [NSMutableDictionary dictionaryWithObjectsAndKeys: exportAttributes, @”exportAttributes”, [NSValue valueWithQTTime:time], @”lastTime”, [NSNumber numberWithInt:0], @”duplicateCount”, exportPath, @”exportPath”, quartzMovie, @”quartzMovie”, sourceMovie, @”sourceMovie”, exportMovie, @”exportMovie”, nil]; [self performSelector:@selector(nextFrame:) withObject:info afterDelay:0.0f];
} [exportMovie release];
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////// //// //// CustomMovie //// ////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
static OSStatus CreatePixelBufferContext(SInt32 format, SInt32 width, SInt32 height, QTVisualContextRef *contextPtr, NSString **errorStringPtr) { QTVisualContextRef context = NULL; CFDictionaryRef pixelBufferOptions = NULL; CFDictionaryRef visualContextOptions = NULL; OSStatus err = noErr; NSString *error = nil; SInt32 alignment = 16; CFTypeRef vals[] = {NULL, NULL, NULL, NULL}; CFTypeRef keys[] = { kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelBufferHeightKey, kCVPixelBufferBytesPerRowAlignmentKey };
if (!contextPtr || !format || (width < 1) || (height < 1))
PostError(@"invalid input parameters: %i", err = paramErr);
if (vals[0] = CFNumberCreate(NULL, kCFNumberSInt32Type, (void *)&format)) {
if (vals[1] = CFNumberCreate(NULL, kCFNumberSInt32Type, (void *)&width)) {
if (vals[2] = CFNumberCreate(NULL, kCFNumberSInt32Type, (void *)&height)) {
if (vals[3] = CFNumberCreate(NULL, kCFNumberSInt32Type, (void *)&alignment)) {
pixelBufferOptions = CFDictionaryCreate(NULL,
(const void **)keys,
(const void **)vals,
(CFIndex)4,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFRelease((CFTypeRef)vals[3]);
}
CFRelease((CFTypeRef)vals[2]);
}
CFRelease((CFTypeRef)vals[1]);
}
CFRelease((CFTypeRef)vals[0]);
}
if (!pixelBufferOptions)
PostError(@"unable to create pixel buffer options: %i", err = coreFoundationUnknownErr);
visualContextOptions = CFDictionaryCreate(NULL,
(const void **)&kQTVisualContextPixelBufferAttributesKey,
(const void **)&pixelBufferOptions,
(CFIndex)1,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
if (!visualContextOptions)
PostError(@"unable to create visual context options: %i", err = coreFoundationUnknownErr);
if (err = QTPixelBufferContextCreate(kCFAllocatorDefault, visualContextOptions, &context))
PostError(@"unable to create pixel buffer context: %i", err);
*contextPtr = context;
context = NULL;
ERROR:
if (visualContextOptions) CFRelease(visualContextOptions);
if (pixelBufferOptions) CFRelease(pixelBufferOptions);
if (context) QTVisualContextRelease(context);
if (errorStringPtr)
*errorStringPtr = error;
return err; }
@implementation CustomMovie
(id)initWithCacheDirectory:(NSString *)dir width:(unsigned)w height:(unsigned)h {
CFStringRef temp = nil; NSString *error = nil; NSError *nsErr = nil; OSErr err = 0; OSType dt; Movie mov = nil;
BOOL isDir; if (!([[NSFileManager defaultManager] fileExistsAtPath:dir isDirectory:&isDir] && isDir)) PostError(@”cache directory path is not a valid directory: %@”, dir);
temp = (CFStringRef)[dir stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; if (!temp) PostError(@”unable to create temporary cache path”);
if (err = QTNewDataReferenceFromFullPathCFString(temp, kQTNativeDefaultPathStyle, 0, &dataHandle, &dt)) PostError(@”unable to create new data reference at path: %@”, temp);
if (err = CreateMovieStorage(dataHandle, dt, ‘TVOD’, smSystemScript, newMovieActive, &outputHandler, &mov)) PostError(@”unable to create movie with storage”);
if (self = [self initWithQuickTimeMovie:mov disposeWhenDone:YES error:&nsErr]) {
[self setAttribute:(id)kCFBooleanTrue forKey:QTMovieEditableAttribute];
if (w && h) {
if (err = CreatePixelBufferContext(k32ARGBPixelFormat, cntxWidth = w, cntxHeight = w, &context, &error))
goto ERROR;
}
SetMovieVisualContext(mov, context);
tempPath = (id)CFRetain(temp);
} else PostError(@”unable to create movie!! %@”, [nsErr localizedFailureReason]);
return self;
ERROR:;
NSLog(@"<%p>%s: ERROR!! (%i) %@", self, __PRETTY_FUNCTION__, err, error);
[self release];
return nil;
}
(void)dealloc {
if (tempPath) { [[NSFileManager defaultManager] removeFileAtPath:tempPath handler:nil]; [tempPath release]; }
if (outputHandler) CloseMovieStorage(outputHandler); if (dataHandle) DisposeHandle(dataHandle); if (context) CFRelease((CFTypeRef)context);
[super dealloc];
}
(BOOL)writeToFile:(NSString *)file atomically:(BOOL)atomically {
NSString *error = nil; NSString *atomicPath = nil; NSString *tempName = nil; NSFileManager *manager = [NSFileManager defaultManager];
if ([manager fileExistsAtPath:file]) PostError(@”file exists at path: %@”, file);
NSDictionary *att = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:QTMovieFlatten]; if (atomically) { atomicPath = [file stringByDeletingLastPathComponent]; tempName = [NSString stringWithFormat:@”%@.mov”, [[NSProcessInfo processInfo] globallyUniqueString]]; atomicPath = [atomicPath stringByAppendingPathComponent:tempName]; if ([self writeToFile:atomicPath withAttributes:att]) { if (![manager movePath:atomicPath toPath:file handler:nil]) PostError(@”unable to move temporary movie at path: %@ to path: %@”, atomicPath, file); } else PostError(@”unable to create temporary movie at path: %@”, atomicPath); } else if (![self writeToFile:file withAttributes:att]) PostError(@”unable to writeToFile: %@”, file);
return YES;
ERROR:;
NSLog(@"<%p>%s: ERROR: %@", self, __PRETTY_FUNCTION__, error);
BOOL isDir;
if (atomicPath &&
([manager fileExistsAtPath:atomicPath isDirectory:&isDir] && !isDir))
{
[manager removeFileAtPath:atomicPath handler:nil];
}
return NO; }
@end