avmetadataeditor.m
/* |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Command line tool for editing metadata |
*/ |
@import Foundation; |
@import AVFoundation; |
#import <dispatch/dispatch.h> |
#import <getopt.h> |
static NSString * stringForDataDescription(NSData *data); |
static void printMetadata(AVURLAsset *asset, BOOL doDescriptionOut); |
static void printMetadataItems(NSArray *items, NSString *metadataFormat, BOOL doDescriptionOut); |
static void printMetadataItemsToURL(NSArray *items, NSString *metadataFormat, NSURL *printURL); |
static NSArray * metadataFromAssetDictionary(NSArray *sourceMetadata, NSDictionary *metadataDict, BOOL editingMode, NSString *metadataFormat); |
static BOOL processMetadata(NSURL *sourceURL, NSURL *destURL, NSString *outputFileType, NSURL *printURL, NSDictionary *writeMetadata, NSDictionary *appendMetadata, BOOL doPrintOut, BOOL doDescriptionOut, NSString *metadataFormat); |
static void PrintUsage() |
{ |
printf("\n\nUsage:"); |
printf("\navmetadataeditor [-w] [-a] [ <options> ] src dst"); |
printf("\navmetadataeditor [-p] [-o] [ <options> ] src"); |
printf("\nsrc is a path to a local file."); |
printf("\ndst is a path to a destination file."); |
printf("\nOptions:\n"); |
printf("\n -w, --write-metadata=PLISTFILE"); |
printf("\n\t\t Use a PLISTFILE as metadata for the destination file"); |
printf("\n -a, --append-metadata=PLISTFILE"); |
printf("\n\t\t Use a PLISTFILE as metadata to merge with the source metadata for the destination file"); |
printf("\n -p, --print-metadata=PLISTFILE"); |
printf("\n\t\t Write in a PLISTFILE the metadata from the source file"); |
printf("\n -f, --file-type=UTI"); |
printf("\n\t\t Use UTI as output file type"); |
printf("\n -o, --output-metadata"); |
printf("\n\t\t Output the metadata from the source file"); |
printf("\n -d, --description-metadata"); |
printf("\n\t\t Output the metadata description from the source file"); |
printf("\n -q, --quicktime-metadata"); |
printf("\n\t\t Quicktime metadata format"); |
printf("\n -u, --quicktime-user-metadata"); |
printf("\n\t\t Quicktime user metadata format"); |
printf("\n -i, --iTunes-metadata"); |
printf("\n\t\t iTunes metadata format"); |
printf("\n -h, --help"); |
printf("\n\t\t Print this message and exit\n"); |
exit(1); |
} |
static BOOL processMetadata(NSURL *sourceURL, NSURL *destURL, NSString *outputFileType, NSURL *printURL, NSDictionary *writeMetadata, NSDictionary *appendMetadata, BOOL doPrintOut, BOOL doDescriptionOut, NSString *metadataFormat) |
{ |
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:sourceURL options:nil]; |
if (!asset) { |
printf("\nInvalid source, asset creation failure"); |
return NO; |
} |
/* |
Print to the standard output the metadata from the source URL |
*/ |
if (doPrintOut || doDescriptionOut) { |
printMetadata(asset, doDescriptionOut); |
} |
NSArray *sourceMetadata = nil; |
if (nil != metadataFormat) { |
sourceMetadata = [asset metadataForFormat:metadataFormat]; |
} |
else { |
sourceMetadata = [asset commonMetadata]; |
} |
/* |
Save to a plist the metadata from the source URL |
*/ |
if (printURL) { |
printMetadataItemsToURL(sourceMetadata, metadataFormat, printURL); |
} |
if (!destURL) |
return YES; |
if (![asset isExportable]) |
return NO; |
if (nil == writeMetadata && nil == appendMetadata) |
return NO; |
/* |
Create an export session to export the new metadata |
*/ |
AVAssetExportSession *session = [AVAssetExportSession exportSessionWithAsset:asset presetName:AVAssetExportPresetPassthrough]; |
if (![[session supportedFileTypes] containsObject:outputFileType]) |
return NO; |
[session setOutputFileType:outputFileType]; |
[session setOutputURL:destURL]; |
if (writeMetadata) { |
[session setMetadata:metadataFromAssetDictionary(sourceMetadata, writeMetadata, NO, metadataFormat)]; |
} |
else { |
[session setMetadata:metadataFromAssetDictionary(sourceMetadata, appendMetadata, YES, metadataFormat)]; |
} |
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); |
__block NSError *error = nil; |
__block BOOL succeeded = NO; |
[session exportAsynchronouslyWithCompletionHandler:^{ |
if (AVAssetExportSessionStatusCompleted == session.status) { |
succeeded = YES; |
} |
else { |
succeeded = NO; |
if (session.error) |
error = session.error; |
} |
dispatch_semaphore_signal(semaphore); |
}]; |
printf("\n0--------------------100%%\n"); |
float progress = 0.; |
long resSemaphore = 0; |
/* |
Monitor the progress |
*/ |
do { |
resSemaphore = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); |
float curProgress = session.progress; |
while (curProgress > progress) { |
fprintf(stderr, "*"); // Force to be flush without end of line |
progress += 0.05; |
} |
} while( resSemaphore ); |
if (succeeded) { |
printf("\nSuccess\n"); |
} |
else { |
printf("\nError: %s", [[error localizedDescription] UTF8String]); |
printf("\nFailure\n"); |
} |
return succeeded; |
} |
/* |
Get a string from a NSData value formatted as follow: [ data length = ??, bytes = 0x?? ... ?? ] |
*/ |
static NSString * stringForDataDescription(NSData *data) |
{ |
NSMutableString *str = [NSMutableString stringWithCapacity:64]; |
NSUInteger length = [data length]; |
const unsigned char *bytes = (const unsigned char *)[data bytes]; |
int i; |
[str appendFormat:@"[ data length = %u, bytes = 0x", (unsigned int)length]; |
// Dump 24 bytes of data in hex |
if (length <= 24) { |
for (i = 0; i < length; i++) { |
[str appendFormat:@"%02x", bytes[i]]; |
} |
} else { |
for (i = 0; i < 16; i++) { |
[str appendFormat:@"%02x", bytes[i]]; |
} |
[str appendFormat:@" ... "]; |
for (i = length - 8; i < length; i++) { |
[str appendFormat:@"%02x", bytes[i]]; |
} |
} |
[str appendFormat:@" ]"]; |
return str; |
} |
static void printMetadata(AVURLAsset *asset, BOOL doDescriptionOut) |
{ |
/* |
Print the common metadata |
*/ |
NSArray *commonMetadata = [asset commonMetadata]; |
if ([commonMetadata count] > 0) { |
printf("\n\n\nCommon metadata:\n"); |
printMetadataItems(commonMetadata, nil, doDescriptionOut); |
} |
/* |
Print all the metadata formats |
*/ |
for (NSString *format in [asset availableMetadataFormats]) { |
NSArray *items = [asset metadataForFormat:format]; |
if ([items count] > 0) { |
printf("\n\n\nMetadata format: %s\n", [format UTF8String]); |
printMetadataItems(items, format, doDescriptionOut); |
} |
} |
printf("\n\n"); |
} |
static void printMetadataItems(NSArray *items, NSString *metadataFormat, BOOL doDescriptionOut) |
{ |
for (AVMetadataItem *item in items) { |
if (doDescriptionOut) { |
printf("\n%s", [[item description] UTF8String]); |
} |
if (nil != metadataFormat) { |
NSString *identifier = [item identifier]; |
id value = [item value]; |
if ([value isKindOfClass:[NSData class]]) { |
printf("\n%s: %s", [identifier UTF8String], [stringForDataDescription(value) UTF8String]); |
} |
else { |
printf("\n%s: %s", [identifier UTF8String], [[item stringValue] UTF8String]); |
} |
} |
else { |
NSString *commonIdentifier = [AVMetadataItem identifierForKey:[item commonKey] keySpace:AVMetadataKeySpaceCommon]; |
printf("\n%s: %s", [commonIdentifier UTF8String], [[item stringValue] UTF8String]); |
} |
} |
} |
static void printMetadataItemsToURL(NSArray *items, NSString *metadataFormat, NSURL *printURL) |
{ |
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; |
if ([items count]) { |
for (AVMetadataItem *item in items) { |
if (nil != metadataFormat) { |
dictionary[item.identifier] = item.value; |
} |
else { |
NSString *commonIdentifier = [AVMetadataItem identifierForKey:[item commonKey] keySpace:AVMetadataKeySpaceCommon]; |
dictionary[commonIdentifier] = item.value; |
} |
} |
} |
[dictionary writeToURL:printURL atomically:YES]; |
} |
static NSArray * metadataFromAssetDictionary(NSArray *sourceMetadata, NSDictionary *metadataDict, BOOL editingMode, NSString *metadataFormat) |
{ |
NSMutableDictionary *mutableMetadataDict = [NSMutableDictionary dictionaryWithDictionary:metadataDict]; |
NSMutableArray *newMetadata = [NSMutableArray array]; |
if (editingMode) { |
if ([sourceMetadata count]) { |
/* |
Find the identifiers that exist in the dictionary and the metadata |
*/ |
for (AVMetadataItem *item in sourceMetadata) { |
AVMutableMetadataItem *newItem = [item mutableCopy]; |
if (nil != metadataFormat) { |
NSString *identifier = [newItem identifier]; |
/* |
If the identifier is present in the dictionary, change the value to the one from the dictionary |
*/ |
if (mutableMetadataDict[identifier]) { |
newItem.value = mutableMetadataDict[identifier]; |
[mutableMetadataDict removeObjectForKey:identifier]; |
} |
} |
else { |
/* |
If the identifier is present in the dictionary, change the value to the one from the dictionary |
*/ |
NSString *commonIdentifier = [AVMetadataItem identifierForKey:[newItem commonKey] keySpace:AVMetadataKeySpaceCommon]; |
if (mutableMetadataDict[commonIdentifier]) { |
newItem.value = mutableMetadataDict[commonIdentifier]; |
[mutableMetadataDict removeObjectForKey:commonIdentifier]; |
} |
} |
if (newItem.value) { |
[newMetadata addObject:newItem]; |
} |
} |
} |
} |
for (NSString *identifier in [mutableMetadataDict keyEnumerator]) { |
id value = [mutableMetadataDict objectForKey:identifier]; |
if (value) { |
AVMutableMetadataItem *newItem = [AVMutableMetadataItem metadataItem]; |
[newItem setIdentifier:identifier]; |
[newItem setLocale:[NSLocale currentLocale]]; |
[newItem setValue:value]; |
[newItem setExtraAttributes:nil]; |
[newMetadata addObject:newItem]; |
} |
} |
return newMetadata; |
} |
int main (int argc, const char * argv[]) |
{ |
BOOL result = YES; |
@autoreleasepool |
{ |
NSFileManager *fm = [NSFileManager defaultManager]; |
static struct option longopts[] = { |
{"write-metadata", required_argument, NULL, 'w'}, |
{"append-metadata", required_argument, NULL, 'a'}, |
{"print-metadata", required_argument, NULL, 'p'}, |
{"file-type", required_argument, NULL, 'f'}, |
{"output-metadata", no_argument, NULL, 'o'}, |
{"description-metadata", no_argument, NULL, 'd'}, |
{"quicktime-metadata", no_argument, NULL, 'q'}, |
{"quicktime-user-metadata", no_argument, NULL, 'u'}, |
{"itunes-metadata", no_argument, NULL, 'i'}, |
{"help", no_argument, NULL, 'h'}, |
{0, 0, 0, 0} |
}; |
const char *shortopts = "w:a:p:f:odquih"; |
int c = -1; |
NSURL *sourceURL = nil; |
NSURL *destURL = nil; |
NSURL *printURL = nil; |
NSString *outputFileType = AVFileTypeQuickTimeMovie; |
NSDictionary *writeMetadata = nil; |
NSDictionary *appendMetadata = nil; |
NSString *metadataFormat = nil; |
BOOL doPrintOut = NO; |
BOOL doDescriptionOut = NO; |
BOOL needDest = NO; |
c = getopt_long(argc, (char * const *)argv, shortopts, longopts, NULL); |
while (c != -1) { |
switch (c) |
{ |
case 'w': |
{ |
needDest = YES; |
NSString *filePath = [fm stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; |
writeMetadata = [NSDictionary dictionaryWithContentsOfFile:filePath]; |
if (!writeMetadata) { |
printf("\nError: '%s' does not point to a valid property list file", optarg); |
PrintUsage(); |
} |
break; |
} |
case 'a': |
{ |
needDest = YES; |
NSString *filePath = [fm stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; |
appendMetadata = [NSDictionary dictionaryWithContentsOfFile:filePath]; |
if (!appendMetadata) { |
printf("\nError: '%s' does not point to a valid property list file", optarg); |
PrintUsage(); |
} |
break; |
} |
case 'p': |
{ |
NSString *filePath = [fm stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; |
printURL = [NSURL fileURLWithPath:filePath isDirectory:NO]; |
break; |
} |
case 'o': |
{ |
doPrintOut = YES; |
break; |
} |
case 'd': |
{ |
doDescriptionOut = YES; |
break; |
} |
case 'q': |
{ |
// QuickTime metadata format |
metadataFormat = AVMetadataFormatQuickTimeMetadata; |
break; |
} |
case 'u': |
{ |
// QuickTime user metadata (udta) format |
metadataFormat = AVMetadataFormatQuickTimeUserData; |
break; |
} |
case 'i': |
{ |
// iTunes format |
metadataFormat = AVMetadataFormatiTunesMetadata; |
break; |
} |
case 'f': |
{ |
/* |
Output file format use during export, could be the following: |
com.apple.quicktime-movie |
public.mpeg-4 |
com.apple.m4v-video |
com.apple.m4a-audio |
public.3gpp |
*/ |
outputFileType = [NSString stringWithCString:optarg encoding:NSMacOSRomanStringEncoding]; |
if (!outputFileType) |
{ |
printf("Error: '%s' is not a valid UTI\n", optarg); |
PrintUsage(); |
} |
break; |
} |
case 'h': |
default: |
PrintUsage(); |
break; |
} |
c = getopt_long(argc, (char * const *)argv, shortopts, longopts, NULL); |
} |
if (argc <= 2) { |
printf("\nMissing arguments"); |
PrintUsage(); |
} |
int nextArgIndex = optind; |
if (nextArgIndex >= argc) { |
printf("\nMissing source"); |
PrintUsage(); |
} |
const char *sourceInput = argv[nextArgIndex]; |
NSString *filePath = [fm stringWithFileSystemRepresentation:sourceInput length:strlen(sourceInput)]; |
sourceURL = [NSURL URLWithString:filePath]; |
if (![sourceURL scheme]) { |
// No URL scheme, assuming file path |
sourceURL = [NSURL fileURLWithPath:filePath isDirectory:NO]; |
} |
if (!sourceURL) { |
printf("\nInvalid source"); |
PrintUsage(); |
} |
++nextArgIndex; |
if (needDest) { |
if (nextArgIndex >= argc) { |
printf("\nMissing destination"); |
PrintUsage(); |
} |
const char *destInput = argv[nextArgIndex]; |
NSString *destPath = [fm stringWithFileSystemRepresentation:destInput length:strlen(destInput)]; |
destURL = [NSURL fileURLWithPath:destPath isDirectory:NO]; |
if (nil == destURL) { |
printf("\nInvalid destination"); |
PrintUsage(); |
} |
} |
result = processMetadata(sourceURL, destURL, outputFileType, printURL, writeMetadata, appendMetadata, doPrintOut, doDescriptionOut, metadataFormat); |
} |
return (!result); |
} |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-10-16