Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
fcpDocumentProcessor.m
/* |
File: fcpDocumentProcessor.m |
Abstract: Class implementation for an FCP XML document processor. |
This processor parses an XML document, returning |
a list of sequences present. For a particular |
sequence, a description can be requested, as well |
as an "edit list" (a tab delimited file containing |
specific fields from each clip), with that list |
containing only the fields requested by the user. |
Version: 0.5 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by |
Apple, Inc. ("Apple") in consideration of your agreement to the |
following terms, and your use, installation, modification or |
redistribution of this Apple software constitutes acceptance of these |
terms. If you do not agree with these terms, please do not use, |
install, modify or redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple, Inc. |
may be used to endorse or promote products derived from the Apple |
Software without specific prior written permission from Apple. Except |
as expressly stated in this notice, no other rights or licenses, express |
or implied, are granted by Apple herein, including but not limited to |
any patent rights that may be infringed by your derivative works or by |
other works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright © 2007 Apple, Inc., All Rights Reserved |
*/ |
#import "fcpDocumentProcessor.h" |
//======================================== |
// forward declarations of local utility functions |
//======================================== |
double FrameRateFromFrameBase(long framebase, BOOL isNTSC); |
NSString *FormatTimecode(long framebase, double framecount, NSString *format); |
id RetrieveFirstElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError); |
NSString * RetrieveFirstStringElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError); |
double RetrieveFirstDoubleElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError); |
BOOL RetrieveFirstBooleanElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError); |
//======================================== |
// processor class method implementations |
//======================================== |
@implementation FCPDocumentProcessor |
//---------------------------------------- |
- (id)init |
{ |
self = [super init]; |
if (self) { |
// Add your subclass-specific initialization here. |
// If an error occurs here, send a [self release] message and return nil. |
xmlDocumentData = NULL; |
cachedSequenceList = NULL; |
} |
return self; |
} |
//---------------------------------------- |
- (void)dealloc |
{ |
// free instance data |
[xmlDocumentData release]; |
xmlDocumentData = NULL; |
[cachedSequenceList release]; |
cachedSequenceList = NULL; |
// call superclass |
[super dealloc]; |
} |
//---------------------------------------- |
+ (FCPDocumentProcessor *)processorWithFile:(NSURL *) sourceFileURL |
{ |
// attempt to read data from URL |
NSData * rawReadData = [NSData dataWithContentsOfURL:sourceFileURL]; |
if (rawReadData) |
{ |
return ([FCPDocumentProcessor processorWithData:rawReadData]); |
} |
// fallthrough: return NULL on error |
return (NULL); |
} |
//---------------------------------------- |
+ (FCPDocumentProcessor *)processorWithData:(NSData *) sourceData |
{ |
// alloc an instance |
FCPDocumentProcessor * finalParser = [[[FCPDocumentProcessor alloc] init] retain]; |
if (finalParser != NULL) |
{ |
// attempt to parse the data |
NSError *localError = NULL; |
finalParser->xmlDocumentData = [[NSXMLDocument alloc] initWithData:sourceData options:0 error:&localError]; |
if (finalParser->xmlDocumentData) |
[finalParser->xmlDocumentData retain]; |
// build the cached sequence list now |
if ([finalParser getListOfSequences:NULL] == NO) |
{ |
[finalParser release]; |
finalParser = NULL; |
} |
} |
// return parser |
return (finalParser); |
} |
//---------------------------------------- |
- (BOOL)getListOfSequences:(NSArray **)resultList |
{ |
BOOL success = YES; |
// if we don't have a sequence list, build one now... |
if (cachedSequenceList == NULL) |
{ |
// ERROR: bail if we don't have a document |
if (xmlDocumentData == NULL) |
return (NO); |
NSXMLNode * curElement = NULL, *nameElement = NULL; |
NSEnumerator * seqEnumerator = NULL; |
NSArray * foundSequences = NULL; |
NSString *theSeqName = NULL; |
NSError *localError = NULL; |
// find each sequence, storing it's name in our list |
if ((foundSequences = [xmlDocumentData objectsForXQuery:@".//sequence" error:&localError]) != NULL) |
if ((seqEnumerator = [foundSequences objectEnumerator]) != NULL) |
{ |
// alloc a mutable array to store the names |
cachedSequenceList = [[NSMutableArray arrayWithCapacity:[foundSequences count]] retain]; |
// iterate over the sequences, retrieving their names |
while ((curElement = (NSXMLNode*) [seqEnumerator nextObject])) |
if ((nameElement = [[curElement objectsForXQuery:@"./name" error:&localError] objectAtIndex:0]) != NULL) |
if ((theSeqName = [nameElement stringValue]) != NULL) |
[cachedSequenceList addObject:theSeqName]; |
} |
} |
else if (resultList != NULL) |
{ |
// return a copy of the array |
if (((*resultList) = [NSArray arrayWithArray:cachedSequenceList]) == NULL) |
success = NO; |
} |
// return results |
return (success); |
} |
//---------------------------------------- |
- (NSString *)getDescriptionOfSequence:(NSString *)sequenceName |
{ |
NSString *resultString = NULL; |
NSError *localError = NULL; |
// find sequence |
if (sequenceName) |
{ |
NSString *seqSearchString = [NSString stringWithFormat:@".//sequence[name=\"%@\"]", sequenceName]; |
NSXMLElement *timecodeRate = NULL, *timecodeElement = NULL, *timecodeFrameCountElement = NULL; |
NSXMLElement *foundSequence = NULL, *sequenceRate = NULL; |
double seqDuration = 0.0, seqFrameRate = 0.0, tcFrameBase = 0.0, tcFrameCount = 0.0; |
NSString *nameString = NULL, *uuidString = NULL, *tcFormatID = NULL; |
NSMutableString *workingBufferString = NULL; |
// only continue if we found a sequence |
if ((foundSequence = RetrieveFirstElementFromQuery(xmlDocumentData, seqSearchString, &localError)) != NULL) |
{ |
// get name and uuid and duration |
nameString = RetrieveFirstStringElementFromQuery(foundSequence, @"./name", &localError); |
uuidString = RetrieveFirstStringElementFromQuery(foundSequence, @"./uuid", &localError); |
seqDuration = RetrieveFirstDoubleElementFromQuery(foundSequence, @"./duration", &localError); |
// get rate |
if ((sequenceRate = RetrieveFirstElementFromQuery(foundSequence, @"./rate", &localError)) != NULL) |
{ |
BOOL seqNTSC = RetrieveFirstBooleanElementFromQuery(sequenceRate, @"./ntsc", &localError); |
double seqFrameBase = tcFrameBase = RetrieveFirstDoubleElementFromQuery(sequenceRate, @"./timebase", &localError); |
seqFrameRate = FrameRateFromFrameBase(seqFrameBase, seqNTSC); // use the seq rate for the TC by default |
} |
// get rate and timecode elements |
if ((timecodeElement = RetrieveFirstElementFromQuery(foundSequence, @"./timecode", &localError)) != NULL) |
{ |
// get the rate |
if ((timecodeRate = RetrieveFirstElementFromQuery(timecodeElement, @"./rate/timebase", &localError)) != NULL) |
tcFrameBase = [[timecodeRate stringValue] doubleValue]; |
// get the format |
if ((tcFormatID = RetrieveFirstStringElementFromQuery(timecodeElement, @"./displayformat", &localError)) == NULL) |
tcFormatID = @"NDF"; |
// get the frame count |
if ((timecodeFrameCountElement = RetrieveFirstElementFromQuery(timecodeElement, @"./frame", &localError)) != NULL) |
tcFrameCount = [[timecodeFrameCountElement stringValue] doubleValue]; |
} |
// format up output string |
if ((workingBufferString = [NSMutableString stringWithCapacity:1024]) != NULL) |
{ |
// handle name/uuid |
if (nameString) [workingBufferString appendFormat:@"Name: %@\n", nameString, NULL]; |
if (uuidString) [workingBufferString appendFormat:@"UUID: %@\n", uuidString, NULL]; |
// handle rate/duration |
if (seqFrameRate > 0.0) |
{ |
[workingBufferString appendFormat:@"Frame Rate: %0.2f\n", seqFrameRate, NULL]; |
[workingBufferString appendFormat:@"Duration: %@\n", FormatTimecode(seqFrameRate, seqDuration, @"NDF"), NULL]; |
} |
// handle starting timecode |
if (timecodeElement) |
[workingBufferString appendFormat:@"Starting Timecode: %@\n", FormatTimecode(tcFrameBase, tcFrameCount, tcFormatID), NULL]; |
// make an immutable copy to return |
resultString = [NSString stringWithString:workingBufferString]; |
} |
} |
else |
{ |
resultString = [NSString stringWithString:@"ERROR: Sequence not found."]; |
} |
} |
// return the result |
return (resultString); |
} |
//---------------------------------------- |
- (NSString *)getEditListForSequence:(NSString *)sequenceName withFields:(NSArray *)fieldsArray |
{ |
// common locals |
NSMutableString *resultString = NULL; |
NSError *localError = NULL; |
NSXMLElement *tempElement = NULL; |
// ensure we have a sequence name before continuing |
if (sequenceName == NULL) return (NULL); |
// look for the passed sequence |
NSString *seqSearchString = [NSString stringWithFormat:@".//sequence[name=\"%@\"]", sequenceName]; |
NSXMLElement *foundSequence = RetrieveFirstElementFromQuery(xmlDocumentData, seqSearchString, &localError); |
if (foundSequence == NULL) return (NULL); |
// allocate a working buffer to output to |
if ((resultString = [NSMutableString stringWithCapacity:8192]) == NULL) |
return (NULL); |
// build a final list of fields/attributes to export |
// (list is pairs of output key names with internal label (key name, processing indicator) |
NSArray *finalExportKeys = [self getInternalFieldList:fieldsArray]; |
if (finalExportKeys == NULL) |
{ |
return (NULL); |
} |
else |
{ |
// walk the keys to build a label line, identifying the fields written out |
NSEnumerator *lineKeyEnumerator = [finalExportKeys objectEnumerator]; |
NSDictionary *lineExportKey = NULL; |
BOOL foundKey = NO; |
while ((lineExportKey = [lineKeyEnumerator nextObject]) != NULL) |
{ |
// add a tab seperator? |
if (foundKey == YES) |
[resultString appendString:@"\t"]; |
// add the key |
[resultString appendString:[lineExportKey valueForKey:@"name"]]; |
foundKey = YES; |
} |
// append an EOL |
[resultString appendString:@"\r"]; |
} |
// get frame rate and timecode format of sequence |
NSString *seqTimecodeFormat = @"NDF"; |
long seqTimebase = 30; |
if ((tempElement = RetrieveFirstElementFromQuery(foundSequence, @"./rate/timebase", &localError)) != NULL) |
seqTimebase = [[tempElement stringValue] intValue]; |
if ((tempElement = RetrieveFirstElementFromQuery(foundSequence, @"./timecode/displayformat", &localError)) != NULL) |
seqTimecodeFormat = [tempElement stringValue]; |
// enumerate the video tracks/clipitems |
NSArray *videoTracksArray = [foundSequence objectsForXQuery:@"./media/video/track" error:&localError]; |
NSEnumerator *tracksEnumerator = tracksEnumerator = [videoTracksArray objectEnumerator]; |
NSXMLElement *curTrackElement = NULL; |
while ((curTrackElement = [tracksEnumerator nextObject]) != NULL) |
{ |
NSArray *curItemsElementArray = [curTrackElement objectsForXQuery:@"./clipitem" error:&localError]; |
NSEnumerator *itemsEnumerator = [curItemsElementArray objectEnumerator]; |
NSXMLElement *curItemElement = NULL; |
while ((curItemElement = [itemsEnumerator nextObject]) != NULL) |
{ |
// working data for this entry |
NSEnumerator *exportKeyEnumerator = [finalExportKeys objectEnumerator]; |
NSDictionary *curExportKey = NULL; |
NSString *keySearchString = NULL; |
NSXMLElement *foundElement = NULL; |
// enumerate and process keys |
while ((curExportKey = [exportKeyEnumerator nextObject]) != NULL) |
{ |
// if this is not our first time through the loop, append a tab before continuing |
if (keySearchString) |
[resultString appendString:@"\t"]; |
// look for this particular key |
if ((keySearchString = [NSString stringWithFormat:@".//%@", [curExportKey valueForKey:@"name"]]) != NULL) |
foundElement = RetrieveFirstElementFromQuery(curItemElement, keySearchString, &localError); |
// output this key, either raw, formatted as a timecode |
if (foundElement) |
if ([[curExportKey valueForKey:@"formatTC"] boolValue] == YES) |
[resultString appendString:(FormatTimecode(seqTimebase, [[foundElement stringValue] doubleValue], seqTimecodeFormat))]; |
else |
[resultString appendString:[foundElement stringValue]]; |
} |
// append an EOL |
[resultString appendString:@"\r"]; |
} |
} |
// return final buffer (if we have one) |
if (resultString == NULL || [resultString length]<=0) return (NULL); |
return ([NSString stringWithString:resultString]); |
} |
//---------------------------------------- |
- (NSArray *)getExportOptionalKeyList |
{ |
// NOTE: implicit keys exported regardless: name, start |
return [NSArray arrayWithObjects:@"in", @"out", @"end", @"duration", @"anamorphic", |
@"scene", @"shottake", @"lognote", @"good", |
@"label", @"label2", @"comment1", @"comment2", |
@"mastercomment1", @"mastercomment2", NULL]; |
} |
//---------------------------------------- |
- (NSArray *)getInternalFieldList:(NSArray *)fieldsArray |
{ |
NSMutableArray *workingArray = [NSMutableArray arrayWithCapacity:16]; |
NSArray *tcFormatKeys = [NSArray arrayWithObjects:@"in", @"out", @"end", @"duration", NULL]; |
// ensure we were able to allocate the arrays |
if (workingArray == NULL) return (NULL); |
// add base keys |
[workingArray addObject:[NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"name", [NSNumber numberWithBool:NO], NULL] |
forKeys:[NSArray arrayWithObjects:@"name", @"formatTC", NULL]]]; |
[workingArray addObject:[NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"start", [NSNumber numberWithBool:YES], NULL] |
forKeys:[NSArray arrayWithObjects:@"name", @"formatTC", NULL]]]; |
// iterate through optional list, adding those keys to the output |
NSEnumerator *optEnumerator = [fieldsArray objectEnumerator]; |
NSString *curKey = NULL; |
while ((curKey = [optEnumerator nextObject]) != NULL) |
[workingArray addObject:[NSDictionary dictionaryWithObjects: |
[NSArray arrayWithObjects:curKey, [NSNumber numberWithBool:[tcFormatKeys containsObject:curKey]], NULL] |
forKeys:[NSArray arrayWithObjects:@"name", @"formatTC", NULL]]]; |
// return an immutable copy of the working array |
return ([NSArray arrayWithArray:workingArray]); |
} |
@end |
//======================================== |
// local utility function implementations |
//======================================== |
//---------------------------------------- |
double FrameRateFromFrameBase(long framebase, BOOL isNTSC) |
{ |
// is this an NTSC frame rate or not? |
if (isNTSC == NO) // non-ntsc, so just return the framebase |
{ |
return ((double)framebase); |
} |
else if (framebase == 24 || framebase == 30 || framebase == 60) // only allow valid NTSC combinations |
{ |
return (((double)framebase * 1000.0) / 1001.0); |
} |
// fallthrough: error case, so return 0 |
return (0); |
} |
//---------------------------------------- |
NSString *FormatTimecode(long framebase, double framecount, NSString *format) |
{ |
NSString *resultString = NULL; |
NSMutableString *workingBuffer = NULL; |
long timeIndex = framecount; |
// check that format is valid (drop-frame timecode is only valid for 30fps media) |
if (framebase != 30 && [format caseInsensitiveCompare:@"DF"] == NSOrderedSame) |
format = @"NDF"; |
// if the format is drop-frame, convert the frame count into a time index |
if ([format caseInsensitiveCompare:@"DF"] == NSOrderedSame) |
{ |
long calcMins = (framebase * 60); |
long tenMins = (timeIndex / ((calcMins * 10) - (2*9))); |
long tempFrames = (timeIndex - (tenMins * ((calcMins * 10) - (2*9)))); |
long tempMins = (tempFrames / (calcMins - 2)); |
long adjustValue = 0, roundTemp = 0; |
// drop frame removes 2 frames from each minute, except for the 10th minutes |
// (e.g. 18 frames from each 10 minutes) |
// find out the remainder minutes/frames from whole 10 minute chunks |
if (tempMins > 0) |
if (tempMins >= 10) |
{ |
adjustValue = (9*2); |
} |
else |
{ |
roundTemp = (tempFrames % (calcMins - 2)); |
if (roundTemp >=0 && roundTemp<2) |
adjustValue = ((tempMins - 1) * 2); |
else |
adjustValue = (tempMins * 2); |
} |
// add in adjustment |
timeIndex += ((tenMins * 2 * 9) + adjustValue); |
} |
// start filling out the buffer... |
if ((workingBuffer = [NSMutableString stringWithCapacity:32]) != NULL) |
{ |
// break out H:M:S:F |
long hours = ((timeIndex / (framebase * 60 * 60)) % 24); |
long minutes = ((timeIndex / (framebase * 60)) % 60); |
long seconds = ((timeIndex / framebase) % 60); |
/*long frames = (timeIndex % framebase); - not needed */ |
// format the components of the timecode |
[workingBuffer appendFormat:((hours < 10)?(@"0%i"):(@"%i")), hours]; |
[workingBuffer appendFormat:((minutes < 10)?(@":0%i"):(@":%i")), minutes]; |
[workingBuffer appendFormat:((seconds < 10)?(@":0%i"):(@":%i")), seconds]; |
if ([format caseInsensitiveCompare:@"DF"] == NSOrderedSame) |
[workingBuffer appendFormat:((seconds < 10)?(@";0%i"):(@";%i")), seconds]; |
else |
[workingBuffer appendFormat:((seconds < 10)?(@":0%i"):(@":%i")), seconds]; |
// make an immutable string to reuturn |
resultString = [NSString stringWithString:workingBuffer]; |
} |
// return final string |
return (resultString); |
} |
//---------------------------------------- |
id RetrieveFirstElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError) |
{ |
NSArray *resultsArray = [rootElement objectsForXQuery:queryString error:localError]; |
if (resultsArray != NULL) |
if ([resultsArray count] > 0) |
return ([resultsArray objectAtIndex:0]); |
return (NULL); |
} |
//---------------------------------------- |
NSString * RetrieveFirstStringElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError) |
{ |
id resultValue = RetrieveFirstElementFromQuery(rootElement, queryString, localError); |
if (resultValue) return ([resultValue stringValue]); |
return (NULL); |
} |
//---------------------------------------- |
double RetrieveFirstDoubleElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError) |
{ |
id resultValue = RetrieveFirstElementFromQuery(rootElement, queryString, localError); |
if (resultValue) return ([[resultValue stringValue] doubleValue]); |
return (0.0); |
} |
//---------------------------------------- |
BOOL RetrieveFirstBooleanElementFromQuery(NSXMLNode *rootElement, NSString *queryString, NSError **localError) |
{ |
id resultValue = (NSString*)RetrieveFirstElementFromQuery(rootElement, queryString, localError); |
if (resultValue) return (([[resultValue stringValue] caseInsensitiveCompare:@"TRUE"] == NSOrderedSame)?(YES):(NO)); |
return (NO); |
} |
Copyright © 2007 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2007-06-01