diff --git a/AVFoundationExporter/LICENSE.txt b/AVFoundationExporter/LICENSE.txt new file mode 100644 index 00000000..d5df14fe --- /dev/null +++ b/AVFoundationExporter/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVFoundationExporter: Exporting and Transcoding Movies +Version: 3.0 + +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 (C) 2016 Apple Inc. All Rights Reserved. diff --git a/AVFoundationExporter/Objective-C/AVFoundationExporter.xcodeproj/project.pbxproj b/AVFoundationExporter/Objective-C/AVFoundationExporter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ff6310dd --- /dev/null +++ b/AVFoundationExporter/Objective-C/AVFoundationExporter.xcodeproj/project.pbxproj @@ -0,0 +1,237 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1E1D58BC1368D74F00D93743 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1D58BB1368D74F00D93743 /* Foundation.framework */; }; + 1E1D58BF1368D74F00D93743 /* AVFoundationExporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E1D58BE1368D74F00D93743 /* AVFoundationExporter.m */; }; + 1E1D58CF1368D7E600D93743 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1D58CD1368D7E600D93743 /* CoreMedia.framework */; }; + 1E1D58D01368D7E600D93743 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1D58CE1368D7E600D93743 /* AVFoundation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 1E1D58B51368D74F00D93743 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1E1D58B71368D74F00D93743 /* AVFoundationExporter */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = AVFoundationExporter; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E1D58BB1368D74F00D93743 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 1E1D58BE1368D74F00D93743 /* AVFoundationExporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AVFoundationExporter.m; sourceTree = ""; }; + 1E1D58CD1368D7E600D93743 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = ""; }; + 1E1D58CE1368D7E600D93743 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = ""; }; + 3EAA11C51B1B895500EC0006 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1E1D58B41368D74F00D93743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1E1D58BC1368D74F00D93743 /* Foundation.framework in Frameworks */, + 1E1D58CF1368D7E600D93743 /* CoreMedia.framework in Frameworks */, + 1E1D58D01368D7E600D93743 /* AVFoundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1E1D58AC1368D74F00D93743 = { + isa = PBXGroup; + children = ( + 3EAA11C51B1B895500EC0006 /* README.md */, + 1E1D58BD1368D74F00D93743 /* AVFoundationExporter */, + 1E1D58BA1368D74F00D93743 /* Frameworks */, + 1E1D58B81368D74F00D93743 /* Products */, + ); + sourceTree = ""; + }; + 1E1D58B81368D74F00D93743 /* Products */ = { + isa = PBXGroup; + children = ( + 1E1D58B71368D74F00D93743 /* AVFoundationExporter */, + ); + name = Products; + sourceTree = ""; + }; + 1E1D58BA1368D74F00D93743 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1E1D58BB1368D74F00D93743 /* Foundation.framework */, + 1E1D58CD1368D7E600D93743 /* CoreMedia.framework */, + 1E1D58CE1368D7E600D93743 /* AVFoundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1E1D58BD1368D74F00D93743 /* AVFoundationExporter */ = { + isa = PBXGroup; + children = ( + 1E1D58BE1368D74F00D93743 /* AVFoundationExporter.m */, + ); + path = AVFoundationExporter; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1E1D58B61368D74F00D93743 /* AVFoundationExporter */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1E1D58C61368D74F00D93743 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */; + buildPhases = ( + 1E1D58B31368D74F00D93743 /* Sources */, + 1E1D58B41368D74F00D93743 /* Frameworks */, + 1E1D58B51368D74F00D93743 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVFoundationExporter; + productName = AVFoundationExporter; + productReference = 1E1D58B71368D74F00D93743 /* AVFoundationExporter */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1E1D58AE1368D74F00D93743 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Apple, Inc"; + }; + buildConfigurationList = 1E1D58B11368D74F00D93743 /* Build configuration list for PBXProject "AVFoundationExporter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 1E1D58AC1368D74F00D93743; + productRefGroup = 1E1D58B81368D74F00D93743 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1E1D58B61368D74F00D93743 /* AVFoundationExporter */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 1E1D58B31368D74F00D93743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1E1D58BF1368D74F00D93743 /* AVFoundationExporter.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1E1D58C41368D74F00D93743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_VERSION = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 1E1D58C51368D74F00D93743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_VERSION = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + SDKROOT = macosx; + }; + name = Release; + }; + 1E1D58C71368D74F00D93743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = NO; + DEFINES_MODULE = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; + }; + name = Debug; + }; + 1E1D58C81368D74F00D93743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1E1D58B11368D74F00D93743 /* Build configuration list for PBXProject "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1E1D58C41368D74F00D93743 /* Debug */, + 1E1D58C51368D74F00D93743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1E1D58C61368D74F00D93743 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1E1D58C71368D74F00D93743 /* Debug */, + 1E1D58C81368D74F00D93743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1E1D58AE1368D74F00D93743 /* Project object */; +} diff --git a/AVFoundationExporter/Objective-C/AVFoundationExporter/AVFoundationExporter.m b/AVFoundationExporter/Objective-C/AVFoundationExporter/AVFoundationExporter.m new file mode 100644 index 00000000..2a37e38f --- /dev/null +++ b/AVFoundationExporter/Objective-C/AVFoundationExporter/AVFoundationExporter.m @@ -0,0 +1,534 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This file shows an example of using the export and metadata functions in AVFoundation as a part of a command line tool for simple exports. +*/ + +@import Foundation; +@import AVFoundation; + +// --------------------------------------------------------------------------- +// Convenience Functions +// --------------------------------------------------------------------------- + +static void printNSString(NSString *string); +static void printArgs(int argc, const char **argv); + + +// --------------------------------------------------------------------------- +// AAPLExporter Class Interface +// --------------------------------------------------------------------------- +@interface AAPLExporter: NSObject { + NSString *programName; + NSString *exportType; + NSString *preset; + NSString *sourcePath; + NSString *destinationPath; + NSString *fileType; + NSNumber *progress; + NSNumber *startSeconds; + NSNumber *durationSeconds; + BOOL showProgress; + BOOL verbose; + BOOL exportFailed; + BOOL exportComplete; + BOOL listTracks; + BOOL listMetadata; + BOOL removePreExistingFiles; +} + +@property (copy) NSString *programName; +@property (copy) NSString *exportType; +@property (copy) NSString *preset; +@property (copy) NSString *sourcePath; +@property (copy) NSString *destinationPath; +@property (copy) NSString *fileType; +@property (strong) NSNumber *progress; +@property (strong) NSNumber *startSeconds; +@property (strong) NSNumber *durationSeconds; +@property (getter=isVerbose) BOOL verbose; +@property BOOL showProgress; +@property BOOL exportFailed; +@property BOOL exportComplete; +@property BOOL listTracks; +@property BOOL listMetadata; +@property BOOL removePreExistingFiles; + +- (id)initWithArgs:(int)argc argv:(const char **)argv environ:(const char **)environ; +- (void)printUsage; + +- (int)run; + +- (NSArray *)addNewMetadata:(NSArray *)sourceMetadataList presetName:(NSString *)presetName; + ++ (void)doListPresets; +- (void)doListTracks:(NSString *)assetPath; +- (void)doListMetadata:(NSString *)assetPath; + + +@end + + +// --------------------------------------------------------------------------- +// AAPLExporter Class Implementation +// --------------------------------------------------------------------------- + +@implementation AAPLExporter + +@synthesize programName, exportType, preset; +@synthesize sourcePath, destinationPath, progress, fileType; +@synthesize startSeconds, durationSeconds; +@synthesize verbose, showProgress, exportComplete, exportFailed; +@synthesize listTracks, listMetadata; +@synthesize removePreExistingFiles; + +-(id) initWithArgs: (int) argc argv: (const char **) argv environ: (const char **) environ +{ + self = [super init]; + + if (self == nil) { + return nil; + } + + printArgs(argc,argv); + + BOOL gotpreset = NO; + BOOL gotsource = NO; + BOOL gotout = NO; + BOOL parseOK = NO; + BOOL listPresets = NO; + [self setProgramName:[NSString stringWithUTF8String: *argv++]]; + argc--; + while ( argc > 0 && **argv == '-' ) + { + const char* args = &(*argv)[1]; + + argc--; + argv++; + + if ( ! strcmp ( args, "source" ) ) + { + [self setSourcePath: [NSString stringWithUTF8String: *argv++] ]; + gotsource = YES; + argc--; + } + else if (( ! strcmp ( args, "dest" )) || ( ! strcmp ( args, "destination" )) ) + { + [self setDestinationPath: [NSString stringWithUTF8String: *argv++]]; + gotout = YES; + argc--; + } + else if ( ! strcmp ( args, "preset" ) ) + { + [self setPreset: [NSString stringWithUTF8String: *argv++]]; + gotpreset = YES; + argc--; + } + else if ( ! strcmp ( args, "replace" ) ) + { + [self setRemovePreExistingFiles: YES]; + } + else if ( ! strcmp ( args, "filetype" ) ) + { + [self setFileType: [NSString stringWithUTF8String: *argv++]]; + argc--; + } + else if ( ! strcmp ( args, "verbose" ) ) + { + [self setVerbose:YES]; + } + else if ( ! strcmp ( args, "progress" ) ) + { + [self setShowProgress: YES]; + } + else if ( ! strcmp ( args, "start" ) ) + { + [self setStartSeconds: [NSNumber numberWithFloat:[[NSString stringWithUTF8String: *argv++] floatValue]]]; + argc--; + } + else if ( ! strcmp ( args, "duration" ) ) + { + [self setDurationSeconds: [NSNumber numberWithFloat:[[NSString stringWithUTF8String: *argv++] floatValue]]]; + argc--; + } + else if ( ! strcmp ( args, "listpresets" ) ) + { + listPresets = YES; + parseOK = YES; + } + else if ( ! strcmp ( args, "listtracks" ) ) + { + [self setListTracks: YES]; + parseOK = YES; + } + else if ( ! strcmp ( args, "listmetadata" ) ) + { + [self setListMetadata: YES]; + parseOK = YES; + } + else if ( ! strcmp ( args, "help" ) ) + { + [self printUsage]; + } + else { + printf("Invalid input parameter: %s\n", args ); + [self printUsage]; + return nil; + } + } + [self setProgress: [NSNumber numberWithFloat:(float)0.0]]; + [self setExportFailed: NO]; + [self setExportComplete: NO]; + + if (listPresets) { + [AAPLExporter doListPresets]; + } + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Running: %@\n", [self programName]]); + } + + // There must be a source and either a preset and output (the normal case) or parseOK set for a listing + if ((gotsource == NO) || ((parseOK == NO) && ((gotpreset == NO) || (gotout == NO)))) { + [self printUsage]; + return nil; + } + return self; +} + + +-(void) printUsage +{ + printf("AVFoundationExporter - usage:\n"); + printf(" ./AVFoundationExporter [-parameter ...]\n"); + printf(" parameters are all preceded by a -. The order of the parameters is unimportant.\n"); + printf(" Required parameters are -preset -source -dest \n"); + printf(" Source and destination URL strings cannot contain spaces.\n"); + printf(" Available parameters are:\n"); + printf(" -preset . The preset name eg: AVAssetExportPreset640x480 AVAssetExportPresetAppleM4VWiFi. Use -listpresets to see a full list.\n"); + printf(" -destination (or -dest) \n"); + printf(" -source \n"); + printf(" -replace If there is a preexisting file at the destination location, remove it before exporting."); + printf(" -filetype The file type (eg com.apple.m4v-video) for the output file. If not specified, the first supported type will be used.\n"); + printf(" -start time in seconds (decimal are OK). Removes the startClip time from the beginning of the movie before exporting.\n"); + printf(" -duration time in seconds (decimal are OK). Trims the movie to this duration before exporting. \n"); + printf(" Also available are some setup options:\n"); + printf(" -verbose Print more information about the execution.\n"); + printf(" -progress Show progress information.\n"); + printf(" -listpresets For sourceMovieURL sources only, lists the tracks in the source movie before the export. \n"); + printf(" -listtracks For sourceMovieURL sources only, lists the tracks in the source movie before the export. \n"); + printf(" Always lists the tracks in the destination asset at the end of the export.\n"); + printf(" -listmetadata Lists the metadata in the source movie before the export. \n"); + printf(" Also lists the metadata in the destination asset at the end of the export.\n"); + printf(" Sample export lines:\n"); + printf(" ./AVFoundationExporter -dest /tmp/testOut.m4v -replace -preset AVAssetExportPresetAppleM4ViPod -listmetadata -source /path/to/myTestMovie.m4v\n"); + printf(" ./AVFoundationExporter -destination /tmp/testOut.mov -preset AVAssetExportPreset640x480 -listmetadata -listtracks -source /path/to/myTestMovie.mov\n"); +} + + +static dispatch_time_t getDispatchTimeFromSeconds(float seconds) { + long long milliseconds = seconds * 1000.0; + dispatch_time_t waitTime = dispatch_time( DISPATCH_TIME_NOW, 1000000LL * milliseconds ); + return waitTime; +} + +- (int)run +{ + NSURL *sourceURL = nil; + AVAssetExportSession *avsession = nil; + NSURL *destinationURL = nil; + BOOL success = YES; + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSParameterAssert( [self sourcePath] != nil ); + + if ([self listTracks] && [self sourcePath]) { + [self doListTracks:[self sourcePath]]; + } + if ([self listMetadata] && [self sourcePath]) { + [self doListMetadata:[self sourcePath]]; + } + if ([self destinationPath] == nil) { + NSLog(@"No output path specified, only listing tracks and/or metadata, export was not performed."); + goto bail; + } + if ([self preset] == nil) { + NSLog(@"No preset specified, only listing tracks and/or metadata, export was not performed."); + goto bail; + } + + if ( [self isVerbose] && [self sourcePath] ) { + printNSString([NSString stringWithFormat:@"all av asset presets:%@", [AVAssetExportSession allExportPresets]]); + } + + if ([self sourcePath] != nil) { + sourceURL = [[NSURL fileURLWithPath: [self sourcePath] isDirectory: NO] retain]; + } + + AVAsset *sourceAsset = nil; + NSError* error = nil; + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"AVAssetExport for preset:%@ to with source:%@", [self preset], [destinationURL path]]); + } + + destinationURL = [NSURL fileURLWithPath: [self destinationPath] isDirectory: NO]; + if ([self removePreExistingFiles] && [[NSFileManager defaultManager] fileExistsAtPath:[self destinationPath]]) { + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Removing re-existing destination file at:%@", destinationURL]); + } + [[NSFileManager defaultManager] removeItemAtURL:destinationURL error:&error]; + } + + sourceAsset = [[[AVURLAsset alloc] initWithURL:sourceURL options:nil] autorelease]; + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Compatible av asset presets:%@", [AVAssetExportSession exportPresetsCompatibleWithAsset:sourceAsset]]); + } + avsession = [[AVAssetExportSession alloc] initWithAsset:sourceAsset presetName:[self preset]]; + + [avsession setOutputURL:destinationURL]; + + if ([self fileType] != nil) { + [avsession setOutputFileType:[self fileType]]; + } + else { + [avsession setOutputFileType:[[avsession supportedFileTypes] objectAtIndex:0]]; + } + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Created AVAssetExportSession: %p", avsession]); + printNSString([NSString stringWithFormat:@"presetName:%@", [avsession presetName]]); + printNSString([NSString stringWithFormat:@"source URL:%@", [sourceURL path]]); + printNSString([NSString stringWithFormat:@"destination URL:%@", [[avsession outputURL] path]]); + printNSString([NSString stringWithFormat:@"output file type:%@", [avsession outputFileType]]); + } + + // Add a metadata item to indicate how thie destination file was created. + NSArray *sourceMetadataList = [avsession metadata]; + sourceMetadataList = [self addNewMetadata: sourceMetadataList presetName:[self preset]]; + [avsession setMetadata:sourceMetadataList]; + + // Set up the time range + CMTime startTime = kCMTimeZero; + CMTime durationTime = kCMTimePositiveInfinity; + + if ([self startSeconds] != nil) { + startTime = CMTimeMake([[self startSeconds] floatValue] * 1000, 1000); + } + if ([self durationSeconds] != nil) { + durationTime = CMTimeMake([[self durationSeconds] floatValue] * 1000, 1000); + } + CMTimeRange exportTimeRange = CMTimeRangeMake(startTime, durationTime); + [avsession setTimeRange:exportTimeRange]; + + // start a fresh pool for the export. + [pool drain]; + pool = [[NSAutoreleasePool alloc] init]; + + // Set up a semaphore for the completion handler and progress timer + dispatch_semaphore_t sessionWaitSemaphore = dispatch_semaphore_create( 0 ); + + void (^completionHandler)(void) = ^(void) + { + dispatch_semaphore_signal(sessionWaitSemaphore); + }; + + // do it. + [avsession exportAsynchronouslyWithCompletionHandler:completionHandler]; + + do { + dispatch_time_t dispatchTime = DISPATCH_TIME_FOREVER; // if we dont want progress, we will wait until it finishes. + if ([self showProgress]) { + dispatchTime = getDispatchTimeFromSeconds((float)1.0); + printNSString([NSString stringWithFormat:@"AVAssetExport running progress=%3.2f%%", [avsession progress]*100]); + } + dispatch_semaphore_wait(sessionWaitSemaphore, dispatchTime); + } while( [avsession status] < AVAssetExportSessionStatusCompleted ); + + if ([self showProgress]) { + printNSString([NSString stringWithFormat:@"AVAssetExport finished progress=%3.2f", [avsession progress]*100]); + } + + [avsession release]; + avsession = nil; + + if ([self listMetadata] && [self destinationPath]) { + [self doListMetadata:[self destinationPath]]; + } + if ([self listTracks] && [self destinationPath]) { + [self doListTracks:[self destinationPath]]; + } + + printNSString([NSString stringWithFormat:@"Finished export of %@ to %@ using preset:%@ success=%s\n", [self sourcePath], [self destinationPath], [self preset], (success ? "YES" : "NO")]); + +bail: + [sourceURL release]; + + [pool drain]; + + return success; +} + + +- (NSArray *) addNewMetadata: (NSArray *)sourceMetadataList presetName:(NSString *)presetName +{ + // This method creates a few new metadata items in different keySpaces to be inserted into the exported file along with the metadata that + // was in the original source. + // Depending on the output file format, not all of these items will be valid and not all of them will come through to the destination. + + AVMutableMetadataItem *newUserDataCommentItem = [[[AVMutableMetadataItem alloc] init] autorelease]; + [newUserDataCommentItem setKeySpace:AVMetadataKeySpaceQuickTimeUserData]; + [newUserDataCommentItem setKey:AVMetadataQuickTimeUserDataKeyComment]; + [newUserDataCommentItem setValue:[NSString stringWithFormat:@"QuickTime userdata: Exported to preset %@ using AVFoundationExporter at: %@", presetName, + [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle: NSDateFormatterShortStyle]]]; + + AVMutableMetadataItem *newMetaDataCommentItem = [[[AVMutableMetadataItem alloc] init] autorelease]; + [newMetaDataCommentItem setKeySpace:AVMetadataKeySpaceQuickTimeMetadata]; + [newMetaDataCommentItem setKey:AVMetadataQuickTimeMetadataKeyComment]; + [newMetaDataCommentItem setValue:[NSString stringWithFormat:@"QuickTime metadata: Exported to preset %@ using AVFoundationExporter at: %@", presetName, + [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle: NSDateFormatterShortStyle]]]; + + AVMutableMetadataItem *newiTunesCommentItem = [[[AVMutableMetadataItem alloc] init] autorelease]; + [newiTunesCommentItem setKeySpace:AVMetadataKeySpaceiTunes]; + [newiTunesCommentItem setKey:AVMetadataiTunesMetadataKeyUserComment]; + [newiTunesCommentItem setValue:[NSString stringWithFormat:@"iTunes metadata: Exported to preset %@ using AVFoundationExporter at: %@", presetName, + [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle: NSDateFormatterShortStyle]]]; + + NSArray *newMetadata = [NSArray arrayWithObjects:newUserDataCommentItem, newMetaDataCommentItem, newiTunesCommentItem, nil]; + NSArray *newMetadataList = (sourceMetadataList == nil ? newMetadata : [sourceMetadataList arrayByAddingObjectsFromArray:newMetadata]); + return newMetadataList; +} + + ++ (void) doListPresets +{ + // A simple listing of the presets available for export + printNSString(@""); + printNSString(@"Presets available for AVFoundation export:"); + printNSString(@" QuickTime movie presets:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset640x480]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset960x540]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset1280x720]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset1920x1080]); + printNSString(@" Audio only preset:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4A]); + printNSString(@" Apple device presets:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4VCellular]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4ViPod]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4V480pSD]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4VAppleTV]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4VWiFi]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4V720pHD]); + printNSString(@" Interim format (QuickTime movie) preset:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleProRes422LPCM]); + printNSString(@" Passthrough preset:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetPassthrough]); + printNSString(@""); +} + + +- (void)doListTracks:(NSString *)assetPath +{ + // A simple listing of the tracks in the asset provided + NSURL *sourceURL = [NSURL fileURLWithPath: assetPath isDirectory: NO]; + if (sourceURL) { + AVURLAsset *sourceAsset = [[[AVURLAsset alloc] initWithURL:sourceURL options:nil] autorelease]; + printNSString([NSString stringWithFormat:@"Listing tracks for asset from url:%@", [sourceURL path]]); + NSInteger index = 0; + for (AVAssetTrack *track in [sourceAsset tracks]) { + [track retain]; + printNSString([ NSString stringWithFormat:@" Track index:%ld, trackID:%d, mediaType:%@, enabled:%d, isSelfContained:%d", index, [track trackID], [track mediaType], [track isEnabled], [track isSelfContained] ] ); + index++; + [track release]; + } + } +} + +enum { + kMaxMetadataValueLength = 80, +}; + +- (void)doListMetadata:(NSString *)assetPath +{ + // A simple listing of the metadata in the asset provided + NSURL *sourceURL = [NSURL fileURLWithPath: assetPath isDirectory: NO]; + if (sourceURL) { + AVURLAsset *sourceAsset = [[[AVURLAsset alloc] initWithURL:sourceURL options:nil] autorelease]; + NSLog(@"Listing metadata for asset from url:%@", [sourceURL path]); + for (NSString *format in [sourceAsset availableMetadataFormats]) { + NSLog(@"Metadata for format:%@", format); + for (AVMetadataItem *item in [sourceAsset metadataForFormat:format]) { + NSObject *key = [item key]; + NSString *itemValue = [[item value] description]; + if ([itemValue length] > kMaxMetadataValueLength) { + itemValue = [NSString stringWithFormat:@"%@ ...", [itemValue substringToIndex:kMaxMetadataValueLength-4]]; + } + if ([key isKindOfClass: [NSNumber class]]) { + NSInteger longValue = [(NSNumber *)key longValue]; + char *charSource = (char *)&longValue; + char charValue[5] = {0}; + charValue[0] = charSource[3]; + charValue[1] = charSource[2]; + charValue[2] = charSource[1]; + charValue[3] = charSource[0]; + NSString *stringKey = [[[NSString alloc] initWithBytes: charValue length:4 encoding:NSMacOSRomanStringEncoding] autorelease]; + printNSString([NSString stringWithFormat:@" metadata item key:%@ (%ld), keySpace:%@ commonKey:%@ value:%@", stringKey, longValue, [item keySpace], [item commonKey], itemValue]); + } + else { + printNSString([NSString stringWithFormat:@" metadata item key:%@, keySpace:%@ commonKey:%@ value:%@", [item key], [item keySpace], [item commonKey], itemValue]); + } + } + } + } +} + + +@end + + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + + +int main (int argc, const char * argv[], const char* environ[]) +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + AAPLExporter* exportObj = [[AAPLExporter alloc] initWithArgs:argc argv:argv environ:environ]; + BOOL success; + if (exportObj) + success = [exportObj run]; + else { + success = NO; + } + + [exportObj release]; + [pool release]; + + return ((success == YES) ? 0 : -1); +} + + +// --------------------------------------------------------------------------- +// printNSString +// --------------------------------------------------------------------------- +static void printNSString(NSString *string) +{ + printf("%s\n", [string cStringUsingEncoding:NSUTF8StringEncoding]); +} + +// --------------------------------------------------------------------------- +// printArgs +// --------------------------------------------------------------------------- +static void printArgs(int argc, const char **argv) +{ + int i; + for( i = 0; i < argc; i++ ) + printf("%s ", argv[i]); + printf("\n"); +} + diff --git a/AVFoundationExporter/README.md b/AVFoundationExporter/README.md new file mode 100644 index 00000000..e99daf9d --- /dev/null +++ b/AVFoundationExporter/README.md @@ -0,0 +1,39 @@ +# AVFoundationExporter + +## Description + +Demonstrates use of AVFoundation export APIs with a simple command line utility. The command line application will list some information about the asset, transcode the asset in accord with one of the AVAssetExportSession presets, and demonstrates simple manipulation of the metadata that is exported with the source. + +## Build Requirements + +Xcode 8.0, macOS 10.12 + +## Runtime Requirements + +OS X 10.11 + +## Structure + +The main files associates with this project are: + +Objective-C Version: + Source file: AVFoundationExporter.m + Project bundle: Objective-C/AVFoundationExporter.xcodeproj + +Swift Version: + Source files: main.swift, ArgumentParsing.swift + Project bundle: Swift/AVFoundationExporter.xcodeproj + +## Changes + +Version 1.0 +- First version. + +Version 2.0 +- Add Swift version. + +Version 3.0 +- Updated project for Swift 2.3. + + +Copyright (C) 2015, 2016 Apple Inc. All rights reserved. diff --git a/AVFoundationExporter/Swift/AVFoundationExporter.xcodeproj/project.pbxproj b/AVFoundationExporter/Swift/AVFoundationExporter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..01d0e08b --- /dev/null +++ b/AVFoundationExporter/Swift/AVFoundationExporter.xcodeproj/project.pbxproj @@ -0,0 +1,261 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 004967D71AE9751900B10C98 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 004967D61AE9751900B10C98 /* main.swift */; }; + 00599BBF1B1CFCC20093572A /* ArgumentParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00599BBE1B1CFCC20093572A /* ArgumentParsing.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 004967CA1AE974A600B10C98 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 004967CC1AE974A600B10C98 /* AVFoundationExporter */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = AVFoundationExporter; sourceTree = BUILT_PRODUCTS_DIR; }; + 004967D61AE9751900B10C98 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 00599BBE1B1CFCC20093572A /* ArgumentParsing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentParsing.swift; sourceTree = ""; }; + 3EAA11C31B1B894900EC0006 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 004967C91AE974A600B10C98 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 004967C31AE974A600B10C98 = { + isa = PBXGroup; + children = ( + 3EAA11C31B1B894900EC0006 /* README.md */, + 004967CE1AE974A600B10C98 /* AVFoundationExporter */, + 004967CD1AE974A600B10C98 /* Products */, + ); + sourceTree = ""; + }; + 004967CD1AE974A600B10C98 /* Products */ = { + isa = PBXGroup; + children = ( + 004967CC1AE974A600B10C98 /* AVFoundationExporter */, + ); + name = Products; + sourceTree = ""; + }; + 004967CE1AE974A600B10C98 /* AVFoundationExporter */ = { + isa = PBXGroup; + children = ( + 004967D61AE9751900B10C98 /* main.swift */, + 00599BBE1B1CFCC20093572A /* ArgumentParsing.swift */, + ); + path = AVFoundationExporter; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 004967CB1AE974A600B10C98 /* AVFoundationExporter */ = { + isa = PBXNativeTarget; + buildConfigurationList = 004967D31AE974A600B10C98 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */; + buildPhases = ( + 004967C81AE974A600B10C98 /* Sources */, + 004967C91AE974A600B10C98 /* Frameworks */, + 004967CA1AE974A600B10C98 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVFoundationExporter; + productName = AVFoundationExporter; + productReference = 004967CC1AE974A600B10C98 /* AVFoundationExporter */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 004967C41AE974A600B10C98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + TargetAttributes = { + 004967CB1AE974A600B10C98 = { + CreatedOnToolsVersion = 6.3; + }; + }; + }; + buildConfigurationList = 004967C71AE974A600B10C98 /* Build configuration list for PBXProject "AVFoundationExporter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 004967C31AE974A600B10C98; + productRefGroup = 004967CD1AE974A600B10C98 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 004967CB1AE974A600B10C98 /* AVFoundationExporter */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 004967C81AE974A600B10C98 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00599BBF1B1CFCC20093572A /* ArgumentParsing.swift in Sources */, + 004967D71AE9751900B10C98 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 004967D11AE974A600B10C98 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + 004967D21AE974A600B10C98 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_VERSION = 2.3; + }; + name = Release; + }; + 004967D41AE974A600B10C98 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + 004967D51AE974A600B10C98 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 2.3; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 004967C71AE974A600B10C98 /* Build configuration list for PBXProject "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 004967D11AE974A600B10C98 /* Debug */, + 004967D21AE974A600B10C98 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 004967D31AE974A600B10C98 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 004967D41AE974A600B10C98 /* Debug */, + 004967D51AE974A600B10C98 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 004967C41AE974A600B10C98 /* Project object */; +} diff --git a/AVFoundationExporter/Swift/AVFoundationExporter/ArgumentParsing.swift b/AVFoundationExporter/Swift/AVFoundationExporter/ArgumentParsing.swift new file mode 100644 index 00000000..499f0726 --- /dev/null +++ b/AVFoundationExporter/Swift/AVFoundationExporter/ArgumentParsing.swift @@ -0,0 +1,246 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Parses command-line arguments and invokes the appropriate command +*/ + +import CoreMedia +import AVFoundation + +// Use enums to enforce uniqueness of option labels. +enum LongLabel: String { + case FileType = "filetype" + case PresetName = "preset" + case DeleteExistingFile = "replace" + case LogEverything = "verbose" + case TrimStartTime = "trim-start-time" + case TrimEndTime = "trim-end-time" + case FilterMetadata = "filter-metadata" + case InjectMetadata = "inject-metadata" +} + +enum ShortLabel: String { + case FileType = "f" + case PresetName = "p" + case DeleteExistingFile = "r" + case LogEverything = "v" +} + +let executableName = NSString(string: Process.arguments.first!).pathComponents.last! + +func usage() { + print("Usage:") + print("\t\(executableName) [options]") + print("\t\(executableName) list-presets []") + print("") // newline + print("In the first form, \(executableName) performs an export of the file at , writing the result to a file at . If no options are given, a passthrough export to a QuickTime Movie file is performed.") + print("") + print("In the second form, \(executableName) lists the available parameters to the -preset option. If is specified, only the presets compatible with the file at will be listed.") + print("") + print("Options for first form:") + print("\t-f, -filetype ") + print("\t\tThe file type (e.g. com.apple.m4v-video) for the output file") + print("") + print("\t-p, -preset ") + print("\t\tThe preset name; use commmand list-presets to see available preset names") + print("") + print("\t-r, -replace YES") + print("\t\tIf there is a pre-existing file at the destination location, remove it before exporting") + print("") + print("\t-v, -verbose YES") + print("\t\tPrint more information about the execution") + print("") + print("\t-trim-start-time ") + print("\t\tWhen specified, all media before the start time will be trimmed out") + print("") + print("\t-trim-end-time ") + print("\t\tWhen specified, all media after the end time will be trimmed out") + print("") + print("\t-filter-metadata YES") + print("\t\tFilter out privacy-sensitive metadata") + print("") + print("\t-inject-metadata YES") + print("\t\tAdd simple metadata during export") +} + +// Errors that can occur during argument parsing. +enum CommandLineError: ErrorType, CustomStringConvertible { + case TooManyArguments + case TooFewArguments(descriptionOfRequiredArguments: String) + case InvalidArgument(reason: String) + + var description: String { + switch self { + case .TooManyArguments: + return "Too many arguments" + + case .TooFewArguments(let descriptionOfRequiredArguments): + return "Missing argument(s). Must specify \(descriptionOfRequiredArguments)." + + case .InvalidArgument(let reason): + return "Invalid argument. \(reason)." + } + } +} + +/// A set of convenience methods to use with our specific command line arguments. +extension NSUserDefaults { + func stringForLongLabel(longLabel: LongLabel) -> String? { + return stringForKey(longLabel.rawValue) + } + + func stringForShortLabel(shortLabel: ShortLabel) -> String? { + return stringForKey(shortLabel.rawValue) + } + + func boolForLongLabel(longLabel: LongLabel) -> Bool { + return boolForKey(longLabel.rawValue) + } + + func boolForShortLabel(shortLabel: ShortLabel) -> Bool { + return boolForKey(shortLabel.rawValue) + } + + func timeForLongLabel(longLabel: LongLabel) throws -> CMTime? { + if let timeAsString = stringForLongLabel(longLabel) { + guard let timeAsSeconds = Float64(timeAsString) else { + throw CommandLineError.InvalidArgument(reason: "Non-numeric time \"\(timeAsString)\".") + } + + return CMTimeMakeWithSeconds(timeAsSeconds, 600) + } + + return nil + } + + func timeForShortLabel(shortLabel: ShortLabel) throws -> CMTime? { + if let timeAsString = stringForShortLabel(shortLabel) { + guard let timeAsSeconds = Float64(timeAsString) else { + throw CommandLineError.InvalidArgument(reason: "Non-numeric time \"\(timeAsString)\".") + } + + return CMTimeMakeWithSeconds(timeAsSeconds, 600) + } + + return nil + } +} + +// Lists all presets, or the presets compatible with the file at the given path +func listPresets(sourcePath: String? = nil) { + let presets: [String] + + switch sourcePath { + case let sourcePath?: + print("Presets compatible with \(sourcePath):.") + + let sourceURL = NSURL(fileURLWithPath: sourcePath) + let asset = AVAsset(URL: sourceURL) + presets = AVAssetExportSession.exportPresetsCompatibleWithAsset(asset) + + case nil: + print("Available presets:") + presets = AVAssetExportSession.allExportPresets() + } + + let presetsDescription = presets.joinWithSeparator("\n\t") + + print("\t\(presetsDescription)") +} + +/// The main function that handles all of the command line argument parsing. +func actOnCommandLineArguments() { + let arguments = Process.arguments + let firstArgumentAfterExecutablePath: String? = (arguments.count >= 2) ? arguments[1] : nil + + if arguments.contains("-help") || arguments.contains("-h") { + usage() + exit(0) + } + + do { + switch firstArgumentAfterExecutablePath { + case nil, "help"?: + usage() + exit(0) + + case "list-presets"?: + if arguments.count == 3 { + listPresets(arguments[2]) + } + else if arguments.count > 3 { + throw CommandLineError.TooManyArguments + } + else { + listPresets() + } + + default: + guard arguments.count >= 3 else { + throw CommandLineError.TooFewArguments(descriptionOfRequiredArguments: "source and dest paths") + } + + let sourceURL = NSURL(fileURLWithPath: arguments[1]) + let destinationURL = NSURL(fileURLWithPath: arguments[2]) + + var exporter = Exporter(sourceURL: sourceURL, destinationURL: destinationURL) + + let options = NSUserDefaults.standardUserDefaults() + + if let fileType = options.stringForLongLabel(.FileType) ?? options.stringForShortLabel(.FileType) { + exporter.destinationFileType = fileType + } + + if let presetName = options.stringForLongLabel(.PresetName) ?? options.stringForShortLabel(.PresetName) { + exporter.presetName = presetName + } + + exporter.deleteExistingFile = options.boolForLongLabel(.DeleteExistingFile) || options.boolForShortLabel(.DeleteExistingFile) + + exporter.isVerbose = options.boolForLongLabel(.LogEverything) || options.boolForShortLabel(.LogEverything) + + let trimStartTime = try options.timeForLongLabel(.TrimStartTime) + let trimEndTime = try options.timeForLongLabel(.TrimEndTime) + + switch (trimStartTime, trimEndTime) { + case (nil, nil): + exporter.timeRange = nil + + case (let realStartTime?, nil): + exporter.timeRange = CMTimeRange(start: realStartTime, duration: kCMTimePositiveInfinity) + + case (nil, let realEndTime?): + exporter.timeRange = CMTimeRangeFromTimeToTime(kCMTimeZero, realEndTime) + + case (let realStartTime?, let realEndTime?): + exporter.timeRange = CMTimeRangeFromTimeToTime(realStartTime, realEndTime) + } + + exporter.filterMetadata = options.boolForLongLabel(.FilterMetadata) + + exporter.injectMetadata = options.boolForLongLabel(.InjectMetadata) + + try exporter.export() + } + } + catch let error as CommandLineError { + print("error parsing arguments: \(error).") + print("") // newline + usage() + exit(1) + } + catch let error as NSError { + let highLevelFailure = error.localizedDescription + var errorOutput = highLevelFailure + + if let detailedFailure = error.localizedRecoverySuggestion ?? error.localizedFailureReason { + errorOutput += ": \(detailedFailure)" + } + + print("error: \(errorOutput).") + + exit(1) + } +} \ No newline at end of file diff --git a/AVFoundationExporter/Swift/AVFoundationExporter/main.swift b/AVFoundationExporter/Swift/AVFoundationExporter/main.swift new file mode 100644 index 00000000..91d31a17 --- /dev/null +++ b/AVFoundationExporter/Swift/AVFoundationExporter/main.swift @@ -0,0 +1,228 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Demonstrates how to use AVAssetExportSession to export and transcode media files +*/ + +import AVFoundation + +/* + Perform all of the argument parsing / set up. The interesting AV exporting + code is done in the `Exporter` type. +*/ +actOnCommandLineArguments() + +/// The type that performs all of the asset exporting. +struct Exporter { + // MARK: Properties + + let sourceURL: NSURL + + let destinationURL: NSURL + + var destinationFileType = AVFileTypeQuickTimeMovie + + var presetName = AVAssetExportPresetPassthrough + + var timeRange: CMTimeRange? + + var filterMetadata = false + + var injectMetadata = false + + var deleteExistingFile = false + + var isVerbose = false + + // MARK: Initialization + + init(sourceURL: NSURL, destinationURL: NSURL) { + self.sourceURL = sourceURL + self.destinationURL = destinationURL + } + + func export() throws { + let asset = AVURLAsset(URL: sourceURL) + + printVerbose("Exporting \"\(sourceURL)\" to \"\(destinationURL)\" (file type \(destinationFileType)), using preset \(presetName).") + + // Set up export session. + let exportSession = try setUpExportSession(asset, destinationURL: destinationURL) + + // AVAssetExportSession will not overwrite existing files. + try deleteExistingFile(destinationURL) + + describeSourceFile(asset) + + // Kick off asynchronous export operation. + let group = dispatch_group_create() + dispatch_group_enter(group) + exportSession.exportAsynchronouslyWithCompletionHandler { + dispatch_group_leave(group) + } + + waitForExportToFinish(exportSession, group: group) + + if exportSession.status == .Failed { + // `error` is non-nil when in the "failed" status. + throw exportSession.error! + } + else { + describeDestFile(destinationURL) + } + + printVerbose("Export completed successfully.") + } + + func setUpExportSession(asset: AVAsset, destinationURL: NSURL) throws -> AVAssetExportSession { + guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else { + throw CommandLineError.InvalidArgument(reason: "Invalid preset \(presetName).") + } + + // Set required properties. + exportSession.outputURL = destinationURL + exportSession.outputFileType = destinationFileType + + if let timeRange = timeRange { + exportSession.timeRange = timeRange + + printVerbose("Trimming to time range \(CMTimeRangeCopyDescription(nil, timeRange)!).") + } + + if filterMetadata { + printVerbose("Filtering metadata.") + + exportSession.metadataItemFilter = AVMetadataItemFilter.metadataItemFilterForSharing() + } + + if injectMetadata { + printVerbose("Injecting metadata") + + let now = NSDate() + let currentDate = NSDateFormatter.localizedStringFromDate(now, dateStyle: .MediumStyle, timeStyle: .ShortStyle) + + let userDataCommentItem = AVMutableMetadataItem() + userDataCommentItem.identifier = AVMetadataIdentifierQuickTimeUserDataComment + userDataCommentItem.value = "QuickTime userdata: Exported to preset \(presetName) using AVFoundationExporter at: \(currentDate)." + + let metadataCommentItem = AVMutableMetadataItem() + metadataCommentItem.identifier = AVMetadataIdentifierQuickTimeMetadataComment + metadataCommentItem.value = "QuickTime metadata: Exported to preset \(presetName) using AVFoundationExporter at: \(currentDate)." + + let iTunesCommentItem = AVMutableMetadataItem() + iTunesCommentItem.identifier = AVMetadataIdentifieriTunesMetadataUserComment + iTunesCommentItem.value = "iTunes metadata: Exported to preset \(presetName) using AVFoundationExporter at: \(currentDate)." + + /* + To avoid replacing metadata from the asset: + 1. Fetch existing metadata from the asset. + 2. Combine it with the new metadata. + 3. Set the result on the export session. + */ + exportSession.metadata = asset.metadata + [ + userDataCommentItem, + metadataCommentItem, + iTunesCommentItem + ] + } + + return exportSession + } + + func deleteExistingFile(destinationURL: NSURL) throws { + let fileManager = NSFileManager() + + if let destinationPath = destinationURL.path { + if deleteExistingFile && fileManager.fileExistsAtPath(destinationPath) { + printVerbose("Removing pre-existing file at destination path \"\(destinationPath)\".") + + try fileManager.removeItemAtURL(destinationURL) + } + } + } + + func describeSourceFile(asset: AVAsset) { + guard isVerbose else { return } + + printVerbose("Tracks in source file:") + + let trackDescriptions = trackDescriptionsForAsset(asset) + let tracksDescription = trackDescriptions.joinWithSeparator("\n\t") + printVerbose("\t\(tracksDescription)") + + printVerbose("Metadata in source file:") + let metadataDescriptions = metadataDescriptionsForAsset(asset) + let metadataDescription = metadataDescriptions.joinWithSeparator("\n\t") + + printVerbose("\t\(metadataDescription)") + } + + // Periodically polls & prints export session progress while waiting for the export to finish. + func waitForExportToFinish(exportSession: AVAssetExportSession, group: dispatch_group_t) { + while exportSession.status == .Waiting || exportSession.status == .Exporting { + printVerbose("Progress: \(exportSession.progress * 100.0)%.") + + dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, Int64(500 * NSEC_PER_MSEC))) + } + + printVerbose("Progress: \(exportSession.progress * 100.0)%.") + } + + func describeDestFile(destinationURL: NSURL) { + guard isVerbose else { return } + + let destinationAsset = AVAsset(URL:destinationURL) + + printVerbose("Tracks in written file:") + + let trackDescriptions = trackDescriptionsForAsset(destinationAsset) + let tracksDescription = trackDescriptions.joinWithSeparator("\n\t") + printVerbose("\t\(tracksDescription)") + + printVerbose("Metadata in written file:") + + let metadataDescriptions = metadataDescriptionsForAsset(destinationAsset) + let metadataDescription = metadataDescriptions.joinWithSeparator("\n\t") + printVerbose("\t\(metadataDescription)") + } + + func trackDescriptionsForAsset(asset: AVAsset) -> [String] { + return asset.tracks.map { track in + let enabledString = track.enabled ? "YES" : "NO" + + let selfContainedString = track.selfContained ? "YES" : "NO" + + let formatDescriptions = track.formatDescriptions as! [CMFormatDescriptionRef] + + let formatStrings = formatDescriptions.map { formatDescription -> String in + let mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription) + + let mediaSubTypeString = NSFileTypeForHFSTypeCode(mediaSubType) + + return "'\(track.mediaType)'/\(mediaSubTypeString)" + } + + let formatString = !formatStrings.isEmpty ? formatStrings.joinWithSeparator(", ") : "'\(track.mediaType)'" + + return "Track ID \(track.trackID): \(formatString), data length: \(track.totalSampleDataLength), enabled: \(enabledString), self-contained: \(selfContainedString)" + } + } + + func metadataDescriptionsForAsset(asset: AVAsset) -> [String] { + return asset.metadata.map { item in + let identifier = item.identifier ?? "" + + let value = item.value?.description ?? "" + + return "metadata item \(identifier): \(value)" + } + } + + func printVerbose(string: String) { + if isVerbose { + print(string) + } + } +}