diff options
| author | Brian Jordan | 2012-03-28 19:12:02 -0400 |
|---|---|---|
| committer | Brian Jordan | 2012-03-28 19:12:02 -0400 |
| commit | b9a950fcaf16c48942efb1e9e9f97cea359610be (patch) | |
| tree | cecdafade70262d34aeb6e2c3e3a0d1f6f9e30d4 | |
| parent | 691dd6be8a3924bc5cca09c5263945f4659ccac0 (diff) | |
| download | Video-Tuneup-b9a950fcaf16c48942efb1e9e9f97cea359610be.tar.bz2 | |
implement video export
| -rw-r--r-- | Video Tuneup.xcodeproj/project.pbxproj | 16 | ||||
| -rw-r--r-- | Video Tuneup/SimpleEditor.h | 119 | ||||
| -rw-r--r-- | Video Tuneup/SimpleEditor.m | 525 | ||||
| -rw-r--r-- | Video Tuneup/ViewController.h | 7 | ||||
| -rw-r--r-- | Video Tuneup/ViewController.m | 112 | ||||
| -rw-r--r-- | Video Tuneup/en.lproj/ViewController_iPad.xib | 78 |
6 files changed, 849 insertions, 8 deletions
diff --git a/Video Tuneup.xcodeproj/project.pbxproj b/Video Tuneup.xcodeproj/project.pbxproj index decab7a..622f1e3 100644 --- a/Video Tuneup.xcodeproj/project.pbxproj +++ b/Video Tuneup.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ D3531C94152380D800E286B8 /* song.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = D3531C93152380D800E286B8 /* song.mp3 */; }; + D3531C9C1523B7BB00E286B8 /* SimpleEditor.m in Sources */ = {isa = PBXBuildFile; fileRef = D3531C9B1523B7BB00E286B8 /* SimpleEditor.m */; }; + D3531CA01523B8DA00E286B8 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3531C9F1523B8DA00E286B8 /* QuartzCore.framework */; }; + D3531CA21523BE2900E286B8 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3531CA11523BE2900E286B8 /* AssetsLibrary.framework */; }; D36CCD3615227D06003CCAFC /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D36CCD3515227D06003CCAFC /* UIKit.framework */; }; D36CCD3815227D06003CCAFC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D36CCD3715227D06003CCAFC /* Foundation.framework */; }; D36CCD3A15227D06003CCAFC /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D36CCD3915227D06003CCAFC /* CoreGraphics.framework */; }; @@ -40,6 +43,11 @@ /* Begin PBXFileReference section */ D3531C93152380D800E286B8 /* song.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = song.mp3; path = ../../../../code_others/python/remix/examples/music/song.mp3; sourceTree = "<group>"; }; + D3531C9A1523B7BB00E286B8 /* SimpleEditor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SimpleEditor.h; sourceTree = "<group>"; }; + D3531C9B1523B7BB00E286B8 /* SimpleEditor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SimpleEditor.m; sourceTree = "<group>"; }; + D3531C9D1523B89200E286B8 /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; }; + D3531C9F1523B8DA00E286B8 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + D3531CA11523BE2900E286B8 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; D36CCD3115227D05003CCAFC /* Video Tuneup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Video Tuneup.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D36CCD3515227D06003CCAFC /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; D36CCD3715227D06003CCAFC /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -72,6 +80,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D3531CA21523BE2900E286B8 /* AssetsLibrary.framework in Frameworks */, + D3531CA01523B8DA00E286B8 /* QuartzCore.framework in Frameworks */, D36CCD781522920D003CCAFC /* CoreMedia.framework in Frameworks */, D36CCD6E15228700003CCAFC /* AVFoundation.framework in Frameworks */, D36CCD3615227D06003CCAFC /* UIKit.framework in Frameworks */, @@ -96,6 +106,9 @@ D36CCD2615227D05003CCAFC = { isa = PBXGroup; children = ( + D3531CA11523BE2900E286B8 /* AssetsLibrary.framework */, + D3531C9F1523B8DA00E286B8 /* QuartzCore.framework */, + D3531C9D1523B89200E286B8 /* CoreAudio.framework */, D36CCD771522920D003CCAFC /* CoreMedia.framework */, D36CCD6D15228700003CCAFC /* AVFoundation.framework */, D36CCD3B15227D06003CCAFC /* Video Tuneup */, @@ -130,6 +143,8 @@ children = ( D36CCD6F1522893F003CCAFC /* Resources */, D36CCD4415227D06003CCAFC /* AppDelegate.h */, + D3531C9A1523B7BB00E286B8 /* SimpleEditor.h */, + D3531C9B1523B7BB00E286B8 /* SimpleEditor.m */, D36CCD4515227D06003CCAFC /* AppDelegate.m */, D36CCD7615228B04003CCAFC /* Controllers */, D36CCD7515228ADC003CCAFC /* Views */, @@ -310,6 +325,7 @@ D36CCD4615227D06003CCAFC /* AppDelegate.m in Sources */, D36CCD4915227D06003CCAFC /* ViewController.m in Sources */, D36CCD7415228ACE003CCAFC /* PlayerView.m in Sources */, + D3531C9C1523B7BB00E286B8 /* SimpleEditor.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Video Tuneup/SimpleEditor.h b/Video Tuneup/SimpleEditor.h new file mode 100644 index 0000000..1809c44 --- /dev/null +++ b/Video Tuneup/SimpleEditor.h @@ -0,0 +1,119 @@ + +/* + File: SimpleEditor.h + Abstract: Demonstrates construction of AVComposition, AVAudioMix, and AVVideoComposition. + + Version: 1.1 + + 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 (C) 2010 Apple Inc. All Rights Reserved. + + */ + +#import <Foundation/Foundation.h> +#import <AVFoundation/AVFoundation.h> +#import <CoreMedia/CMTime.h> + + +typedef enum { + SimpleEditorTransitionTypeNone, + SimpleEditorTransitionTypeCrossFade, + SimpleEditorTransitionTypePush +} SimpleEditorTransitionType; + + +@interface SimpleEditor : NSObject +{ + // Configuration + + NSArray *_clips; // array of AVURLAssets + NSArray *_clipTimeRanges; // array of CMTimeRanges stored in NSValues. + + AVURLAsset *_commentary; + CMTime _commentaryStartTime; + + SimpleEditorTransitionType _transitionType; + CMTime _transitionDuration; + + NSString *_titleText; + + + // Composition objects. + + AVComposition *_composition; + AVVideoComposition *_videoComposition; + AVAudioMix *_audioMix; + + AVPlayerItem *_playerItem; + AVSynchronizedLayer *_synchronizedLayer; +} + +// Set these properties before building the composition objects. +@property (nonatomic, retain) NSArray *clips; +@property (nonatomic, retain) NSArray *clipTimeRanges; + +@property (nonatomic, retain) AVURLAsset *commentary; +@property (nonatomic) CMTime commentaryStartTime; + +@property (nonatomic) SimpleEditorTransitionType transitionType; +@property (nonatomic) CMTime transitionDuration; + +@property (nonatomic, retain) NSString *titleText; + + +// Build the composition, videoComposition, and audioMix. +// If the composition is being built for playback then a synchronized layer and player item are also constructed. +// All of these objects can be retrieved all of these objects with the accessors below. +// Calling buildCompositionObjectsForPlayback: will get rid of any previously created composition objects. +- (void)buildCompositionObjectsForPlayback:(BOOL)forPlayback; + +@property (nonatomic, readonly, retain) AVComposition *composition; +@property (nonatomic, readonly, retain) AVVideoComposition *videoComposition; +@property (nonatomic, readonly, retain) AVAudioMix *audioMix; + +- (void)getPlayerItem:(AVPlayerItem**)playerItemOut andSynchronizedLayer:(AVSynchronizedLayer**)synchronizedLayerOut; +// The synchronized layer contains a layer tree which is synchronized with the provided player item. +// Inside the layer tree there is a playerLayer along with other layers related to titling. + +- (AVAssetImageGenerator*)assetImageGenerator; +- (AVAssetExportSession*)assetExportSessionWithPreset:(NSString*)presetName; + + +@end diff --git a/Video Tuneup/SimpleEditor.m b/Video Tuneup/SimpleEditor.m new file mode 100644 index 0000000..f666d6c --- /dev/null +++ b/Video Tuneup/SimpleEditor.m @@ -0,0 +1,525 @@ + +/* + File: SimpleEditor.m + Abstract: Demonstrates construction of AVComposition, AVAudioMix, and AVVideoComposition. + + Version: 1.1 + + 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 (C) 2010 Apple Inc. All Rights Reserved. + + */ + +#import "SimpleEditor.h" + +#import <CoreMedia/CoreMedia.h> +#import <AVFoundation/AVFoundation.h> + +@interface SimpleEditor () +@property (nonatomic, readwrite, retain) AVComposition *composition; +@property (nonatomic, readwrite, retain) AVVideoComposition *videoComposition; +@property (nonatomic, readwrite, retain) AVAudioMix *audioMix; +@property (nonatomic, readwrite, retain) AVPlayerItem *playerItem; +@property (nonatomic, readwrite, retain) AVSynchronizedLayer *synchronizedLayer; + +@end + + +@implementation SimpleEditor + +- (id)init +{ + if (self = [super init]) { + _commentaryStartTime = CMTimeMake(2, 1); // Default start time for the commentary is two seconds. + + _transitionDuration = CMTimeMake(1, 1); // Default transition duration is one second. + + // just until we have the UI for this wired up + NSMutableArray *clipTimeRanges = [[NSMutableArray alloc] initWithCapacity:3]; + CMTimeRange defaultTimeRange = CMTimeRangeMake(kCMTimeZero, CMTimeMake(5, 1)); + NSValue *defaultTimeRangeValue = [NSValue valueWithCMTimeRange:defaultTimeRange]; + [clipTimeRanges addObject:defaultTimeRangeValue]; + [clipTimeRanges addObject:defaultTimeRangeValue]; + [clipTimeRanges addObject:defaultTimeRangeValue]; + _clipTimeRanges = clipTimeRanges; + } + return self; +} + +// Configuration + +@synthesize clips = _clips, clipTimeRanges = _clipTimeRanges; +@synthesize commentary = _commentary, commentaryStartTime = _commentaryStartTime; +@synthesize transitionType = _transitionType, transitionDuration = _transitionDuration; +@synthesize titleText = _titleText; + +// Composition objects. + +@synthesize composition = _composition; +@synthesize videoComposition =_videoComposition; +@synthesize audioMix = _audioMix; +@synthesize playerItem = _playerItem; +@synthesize synchronizedLayer = _synchronizedLayer; + +static CGImageRef createStarImage(CGFloat radius) +{ + int i, count = 5; +#if TARGET_OS_IPHONE + CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); +#else // not TARGET_OS_IPHONE + CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); +#endif // not TARGET_OS_IPHONE + CGImageRef image = NULL; + size_t width = 2*radius; + size_t height = 2*radius; + size_t bytesperrow = width * 4; + CGContextRef context = CGBitmapContextCreate((void *)NULL, width, height, 8, bytesperrow, colorspace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); + CGContextClearRect(context, CGRectMake(0, 0, 2*radius, 2*radius)); + CGContextSetLineWidth(context, radius / 15.0); + + for( i = 0; i < 2 * count; i++ ) { + CGFloat angle = i * M_PI / count; + CGFloat pointradius = (i % 2) ? radius * 0.37 : radius * 0.95; + CGFloat x = radius + pointradius * cos(angle); + CGFloat y = radius + pointradius * sin(angle); + if (i == 0) + CGContextMoveToPoint(context, x, y); + else + CGContextAddLineToPoint(context, x, y); + } + CGContextClosePath(context); + + CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0); + CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0); + CGContextDrawPath(context, kCGPathFillStroke); + CGColorSpaceRelease(colorspace); + image = CGBitmapContextCreateImage(context); + CGContextRelease(context); + return image; +} + +- (void)buildSequenceComposition:(AVMutableComposition *)composition +{ + CMTime nextClipStartTime = kCMTimeZero; + NSInteger i; + + // No transitions: place clips into one video track and one audio track in composition. + NSLog(@"Building sequence composition. Count is %i", [_clips count]); + + AVMutableCompositionTrack *compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; + AVMutableCompositionTrack *compositionAudioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; + + for (i = 0; i < [_clips count]; i++ ) { + AVURLAsset *asset = [_clips objectAtIndex:i]; + NSValue *clipTimeRange = [_clipTimeRanges objectAtIndex:i]; + CMTimeRange timeRangeInAsset; + if (clipTimeRange) + timeRangeInAsset = [clipTimeRange CMTimeRangeValue]; + else + timeRangeInAsset = CMTimeRangeMake(kCMTimeZero, [asset duration]); + + AVAssetTrack *clipVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; + [compositionVideoTrack insertTimeRange:timeRangeInAsset ofTrack:clipVideoTrack atTime:nextClipStartTime error:nil]; + + NSLog(@"Composition audio?"); + if ([[asset tracksWithMediaType:AVMediaTypeAudio] count] != 0) { + AVAssetTrack *clipAudioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0]; + NSLog(@"Composition audio!"); + [compositionAudioTrack insertTimeRange:timeRangeInAsset ofTrack:clipAudioTrack atTime:nextClipStartTime error:nil]; + } + + // Note: This is largely equivalent: + // [composition insertTimeRange:timeRangeInAsset ofAsset:asset atTime:nextClipStartTime error:NULL]; + // except that if the video tracks dimensions do not match, additional video tracks will be added to the composition. + + nextClipStartTime = CMTimeAdd(nextClipStartTime, timeRangeInAsset.duration); + } +} + +- (void)buildTransitionComposition:(AVMutableComposition *)composition andVideoComposition:(AVMutableVideoComposition *)videoComposition +{ + CMTime nextClipStartTime = kCMTimeZero; + NSInteger i; + + // Make transitionDuration no greater than half the shortest clip duration. + CMTime transitionDuration = self.transitionDuration; + for (i = 0; i < [_clips count]; i++ ) { + NSValue *clipTimeRange = [_clipTimeRanges objectAtIndex:i]; + if (clipTimeRange) { + CMTime halfClipDuration = [clipTimeRange CMTimeRangeValue].duration; + halfClipDuration.timescale *= 2; // You can halve a rational by doubling its denominator. + transitionDuration = CMTimeMinimum(transitionDuration, halfClipDuration); + } + } + + // Add two video tracks and two audio tracks. + AVMutableCompositionTrack *compositionVideoTracks[2]; + AVMutableCompositionTrack *compositionAudioTracks[2]; + compositionVideoTracks[0] = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; + compositionVideoTracks[1] = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid]; + compositionAudioTracks[0] = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; + compositionAudioTracks[1] = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; + + CMTimeRange *passThroughTimeRanges = alloca(sizeof(CMTimeRange) * [_clips count]); + CMTimeRange *transitionTimeRanges = alloca(sizeof(CMTimeRange) * [_clips count]); + + // Place clips into alternating video & audio tracks in composition, overlapped by transitionDuration. + for (i = 0; i < [_clips count]; i++ ) { + NSInteger alternatingIndex = i % 2; // alternating targets: 0, 1, 0, 1, ... + AVURLAsset *asset = [_clips objectAtIndex:i]; + NSValue *clipTimeRange = [_clipTimeRanges objectAtIndex:i]; + CMTimeRange timeRangeInAsset; + if (clipTimeRange) + timeRangeInAsset = [clipTimeRange CMTimeRangeValue]; + else + timeRangeInAsset = CMTimeRangeMake(kCMTimeZero, [asset duration]); + + AVAssetTrack *clipVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; + [compositionVideoTracks[alternatingIndex] insertTimeRange:timeRangeInAsset ofTrack:clipVideoTrack atTime:nextClipStartTime error:nil]; + + AVAssetTrack *clipAudioTrack = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0]; + [compositionAudioTracks[alternatingIndex] insertTimeRange:timeRangeInAsset ofTrack:clipAudioTrack atTime:nextClipStartTime error:nil]; + + // Remember the time range in which this clip should pass through. + // Every clip after the first begins with a transition. + // Every clip before the last ends with a transition. + // Exclude those transitions from the pass through time ranges. + passThroughTimeRanges[i] = CMTimeRangeMake(nextClipStartTime, timeRangeInAsset.duration); + if (i > 0) { + passThroughTimeRanges[i].start = CMTimeAdd(passThroughTimeRanges[i].start, transitionDuration); + passThroughTimeRanges[i].duration = CMTimeSubtract(passThroughTimeRanges[i].duration, transitionDuration); + } + if (i+1 < [_clips count]) { + passThroughTimeRanges[i].duration = CMTimeSubtract(passThroughTimeRanges[i].duration, transitionDuration); + } + + // The end of this clip will overlap the start of the next by transitionDuration. + // (Note: this arithmetic falls apart if timeRangeInAsset.duration < 2 * transitionDuration.) + nextClipStartTime = CMTimeAdd(nextClipStartTime, timeRangeInAsset.duration); + nextClipStartTime = CMTimeSubtract(nextClipStartTime, transitionDuration); + + // Remember the time range for the transition to the next item. + transitionTimeRanges[i] = CMTimeRangeMake(nextClipStartTime, transitionDuration); + } + + // Set up the video composition if we are to perform crossfade or push transitions between clips. + NSMutableArray *instructions = [NSMutableArray array]; + + // Cycle between "pass through A", "transition from A to B", "pass through B", "transition from B to A". + for (i = 0; i < [_clips count]; i++ ) { + NSInteger alternatingIndex = i % 2; // alternating targets + + // Pass through clip i. + AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; + passThroughInstruction.timeRange = passThroughTimeRanges[i]; + AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTracks[alternatingIndex]]; + + passThroughInstruction.layerInstructions = [NSArray arrayWithObject:passThroughLayer]; + [instructions addObject:passThroughInstruction]; + + if (i+1 < [_clips count]) { + // Add transition from clip i to clip i+1. + + AVMutableVideoCompositionInstruction *transitionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; + transitionInstruction.timeRange = transitionTimeRanges[i]; + AVMutableVideoCompositionLayerInstruction *fromLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTracks[alternatingIndex]]; + AVMutableVideoCompositionLayerInstruction *toLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTracks[1-alternatingIndex]]; + + if (self.transitionType == SimpleEditorTransitionTypeCrossFade) { + // Fade out the fromLayer by setting a ramp from 1.0 to 0.0. + [fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:transitionTimeRanges[i]]; + } + else if (self.transitionType == SimpleEditorTransitionTypePush) { + // Set a transform ramp on fromLayer from identity to all the way left of the screen. + [fromLayer setTransformRampFromStartTransform:CGAffineTransformIdentity toEndTransform:CGAffineTransformMakeTranslation(-composition.naturalSize.width, 0.0) timeRange:transitionTimeRanges[i]]; + // Set a transform ramp on toLayer from all the way right of the screen to identity. + [toLayer setTransformRampFromStartTransform:CGAffineTransformMakeTranslation(+composition.naturalSize.width, 0.0) toEndTransform:CGAffineTransformIdentity timeRange:transitionTimeRanges[i]]; + } + + transitionInstruction.layerInstructions = [NSArray arrayWithObjects:fromLayer, toLayer, nil]; + [instructions addObject:transitionInstruction]; + } + } + + videoComposition.instructions = instructions; +} + +- (void)addCommentaryTrackToComposition:(AVMutableComposition *)composition withAudioMix:(AVMutableAudioMix *)audioMix +{ + NSInteger i; + NSArray *tracksToDuck = [composition tracksWithMediaType:AVMediaTypeAudio]; // before we add the commentary + + // Clip commentary duration to composition duration. + CMTimeRange commentaryTimeRange = CMTimeRangeMake(self.commentaryStartTime, self.commentary.duration); + if (CMTIME_COMPARE_INLINE(CMTimeRangeGetEnd(commentaryTimeRange), >, [composition duration])) + commentaryTimeRange.duration = CMTimeSubtract([composition duration], commentaryTimeRange.start); + + // Add the commentary track. + AVMutableCompositionTrack *compositionCommentaryTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid]; + [compositionCommentaryTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, commentaryTimeRange.duration) ofTrack:[[self.commentary tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:commentaryTimeRange.start error:nil]; + + + NSMutableArray *trackMixArray = [NSMutableArray array]; + CMTime rampDuration = CMTimeMake(1, 2); // half-second ramps + for (i = 0; i < [tracksToDuck count]; i++) { + AVMutableAudioMixInputParameters *trackMix = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:[tracksToDuck objectAtIndex:i]]; + [trackMix setVolumeRampFromStartVolume:1.0 toEndVolume:0.2 timeRange:CMTimeRangeMake(CMTimeSubtract(commentaryTimeRange.start, rampDuration), rampDuration)]; + [trackMix setVolumeRampFromStartVolume:0.2 toEndVolume:1.0 timeRange:CMTimeRangeMake(CMTimeRangeGetEnd(commentaryTimeRange), rampDuration)]; + [trackMixArray addObject:trackMix]; + } + audioMix.inputParameters = trackMixArray; +} + +- (void)buildPassThroughVideoComposition:(AVMutableVideoComposition *)videoComposition forComposition:(AVMutableComposition *)composition +{ + // Make a "pass through video track" video composition. + AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; + passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [composition duration]); + + AVAssetTrack *videoTrack = [[composition tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; + AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack]; + + passThroughInstruction.layerInstructions = [NSArray arrayWithObject:passThroughLayer]; + videoComposition.instructions = [NSArray arrayWithObject:passThroughInstruction]; +} + +- (CALayer *)buildAnimatedTitleLayerForSize:(CGSize)videoSize +{ + // Create a layer for the overall title animation. + CALayer *animatedTitleLayer = [CALayer layer]; + + // Create a layer for the text of the title. + CATextLayer *titleLayer = [CATextLayer layer]; + titleLayer.string = self.titleText; + titleLayer.font = @"Helvetica"; + titleLayer.fontSize = videoSize.height / 6; + //?? titleLayer.shadowOpacity = 0.5; + titleLayer.alignmentMode = kCAAlignmentCenter; + titleLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height / 6); + + // Add it to the overall layer. + [animatedTitleLayer addSublayer:titleLayer]; + + // Create a layer that contains a ring of stars. + CALayer *ringOfStarsLayer = [CALayer layer]; + + NSInteger starCount = 9, s; + CGFloat starRadius = videoSize.height / 10; + CGFloat ringRadius = videoSize.height * 0.8 / 2; + CGImageRef starImage = createStarImage(starRadius); + for (s = 0; s < starCount; s++) { + CALayer *starLayer = [CALayer layer]; + CGFloat angle = s * 2 * M_PI / starCount; + starLayer.bounds = CGRectMake(0, 0, 2 * starRadius, 2 * starRadius); + starLayer.position = CGPointMake(ringRadius * cos(angle), ringRadius * sin(angle)); +// starLayer.contents = (id)starImage; + starLayer.contents = (__bridge id)starImage; + [ringOfStarsLayer addSublayer:starLayer]; + } + CGImageRelease(starImage); + + // Rotate the ring of stars. + CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; + rotationAnimation.repeatCount = 1e100; // forever + rotationAnimation.fromValue = [NSNumber numberWithFloat:0.0]; + rotationAnimation.toValue = [NSNumber numberWithFloat:2 * M_PI]; + rotationAnimation.duration = 10.0; // repeat every 10 seconds + rotationAnimation.additive = YES; + rotationAnimation.removedOnCompletion = NO; + rotationAnimation.beginTime = 1e-100; // CoreAnimation automatically replaces zero beginTime with CACurrentMediaTime(). The constant AVCoreAnimationBeginTimeAtZero is also available. + [ringOfStarsLayer addAnimation:rotationAnimation forKey:nil]; + + // Add the ring of stars to the overall layer. + animatedTitleLayer.position = CGPointMake(videoSize.width / 2.0, videoSize.height / 2.0); + [animatedTitleLayer addSublayer:ringOfStarsLayer]; + + // Animate the opacity of the overall layer so that it fades out from 3 sec to 4 sec. + CABasicAnimation *fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeAnimation.fromValue = [NSNumber numberWithFloat:1.0]; + fadeAnimation.toValue = [NSNumber numberWithFloat:0.0]; + fadeAnimation.additive = NO; + fadeAnimation.removedOnCompletion = NO; + fadeAnimation.beginTime = 10.0; + fadeAnimation.duration = 2.0; + fadeAnimation.fillMode = kCAFillModeBoth; + [animatedTitleLayer addAnimation:fadeAnimation forKey:nil]; + + return animatedTitleLayer; +} + +- (void)buildCompositionObjectsForPlayback:(BOOL)forPlayback +{ + NSLog(@"Building. Count is %i", [_clips count]); + CGSize videoSize = [[_clips objectAtIndex:0] naturalSize]; + AVMutableComposition *composition = [AVMutableComposition composition]; + AVMutableVideoComposition *videoComposition = nil; + AVMutableAudioMix *audioMix = nil; + CALayer *animatedTitleLayer = nil; + + composition.naturalSize = videoSize; + + if (self.transitionType == SimpleEditorTransitionTypeNone) { + // No transitions: place clips into one video track and one audio track in composition. + + [self buildSequenceComposition:composition]; + } + else { + // With transitions: + // Place clips into alternating video & audio tracks in composition, overlapped by transitionDuration. + // Set up the video composition to cycle between "pass through A", "transition from A to B", + // "pass through B", "transition from B to A". + + videoComposition = [AVMutableVideoComposition videoComposition]; + [self buildTransitionComposition:composition andVideoComposition:videoComposition]; + } + + // If one is provided, add a commentary track and duck all other audio during it. + if (self.commentary) { + // Add the commentary track and duck all other audio during it. + + audioMix = [AVMutableAudioMix audioMix]; + [self addCommentaryTrackToComposition:composition withAudioMix:audioMix]; + } + + // Set up Core Animation layers to contribute a title animation overlay if we have a title set. +// if (self.titleText) { +// animatedTitleLayer = [self buildAnimatedTitleLayerForSize:videoSize]; +// +// if (! forPlayback) { +// // For export: build a Core Animation tree that contains both the animated title and the video. +// CALayer *parentLayer = [CALayer layer]; +// CALayer *videoLayer = [CALayer layer]; +// parentLayer.frame = CGRectMake(0, 0, videoSize.width, videoSize.height); +// videoLayer.frame = CGRectMake(0, 0, videoSize.width, videoSize.height); +// [parentLayer addSublayer:videoLayer]; +// [parentLayer addSublayer:animatedTitleLayer]; +// +// if (! videoComposition) { +// // No transition set -- make a "pass through video track" video composition so we can include the Core Animation tree as a post-processing stage. +// videoComposition = [AVMutableVideoComposition videoComposition]; +// +// [self buildPassThroughVideoComposition:videoComposition forComposition:composition]; +// } +// +// videoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer]; +// } +// } + + if (videoComposition) { + // Every videoComposition needs these properties to be set: + videoComposition.frameDuration = CMTimeMake(1, 30); // 30 fps + videoComposition.renderSize = videoSize; + } + + self.composition = composition; + self.videoComposition = videoComposition; + self.audioMix = audioMix; + + self.synchronizedLayer = nil; + + NSLog(@"Almost done building"); + + if (forPlayback) { +#if TARGET_OS_EMBEDDED + // Render high-def movies at half scale for real-time playback (device-only). + if (videoSize.width > 640) + videoComposition.renderScale = 0.5; +#endif // TARGET_OS_EMBEDDED + + AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:composition]; + playerItem.videoComposition = videoComposition; + playerItem.audioMix = audioMix; + self.playerItem = playerItem; + +// if (animatedTitleLayer) { +// // Build an AVSynchronizedLayer that contains the animated title. +// self.synchronizedLayer = [AVSynchronizedLayer synchronizedLayerWithPlayerItem:self.playerItem]; +// self.synchronizedLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height); +// [self.synchronizedLayer addSublayer:animatedTitleLayer]; +// } + } +} + +- (void)getPlayerItem:(AVPlayerItem**)playerItemOut andSynchronizedLayer:(AVSynchronizedLayer**)synchronizedLayerOut +{ + if (playerItemOut) { + *playerItemOut = _playerItem; + } + if (synchronizedLayerOut) { + *synchronizedLayerOut = _synchronizedLayer; + } +} + +- (AVAssetImageGenerator*)assetImageGenerator +{ + AVAssetImageGenerator *generator = [AVAssetImageGenerator assetImageGeneratorWithAsset:self.composition]; + generator.videoComposition = self.videoComposition; + return generator; +} + +- (AVAssetExportSession*)assetExportSessionWithPreset:(NSString*)presetName +{ + AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:self.composition presetName:presetName]; + session.videoComposition = self.videoComposition; + session.audioMix = self.audioMix; +// return [session autorelease]; + return session; +} + +//- (void)dealloc +//{ +// [_clips release]; +// [_clipTimeRanges release]; +// +// [_commentary release]; +// [_titleText release]; +// +// +// [_composition release]; +// [_videoComposition release]; +// [_audioMix release]; +// +// [_playerItem release]; +// [_synchronizedLayer release]; +// +// [super dealloc]; +//} + +@end diff --git a/Video Tuneup/ViewController.h b/Video Tuneup/ViewController.h index 9ad27d8..0f8279c 100644 --- a/Video Tuneup/ViewController.h +++ b/Video Tuneup/ViewController.h @@ -8,13 +8,17 @@ #import <UIKit/UIKit.h> #import <AVFoundation/AVFoundation.h> +#import <AssetsLibrary/AssetsLibrary.h> +#import "SimpleEditor.h" @class PlayerView; @interface ViewController : UIViewController { + AVURLAsset *asset; } @property (nonatomic, retain) AVPlayer *player; +@property (nonatomic, retain) SimpleEditor *editor; @property (retain) AVPlayerItem *playerItem; @property (nonatomic, retain) IBOutlet PlayerView *playerView; @property (nonatomic, retain) IBOutlet UIButton *playButton; @@ -26,5 +30,8 @@ - (IBAction)play:sender; - (IBAction)pause:sender; - (IBAction)rewind:sender; +- (IBAction)edit:sender; - (void)syncUI; + +- (void)exportDidFinish:(AVAssetExportSession*)session; @end
\ No newline at end of file diff --git a/Video Tuneup/ViewController.m b/Video Tuneup/ViewController.m index 0c4279a..fe509c2 100644 --- a/Video Tuneup/ViewController.m +++ b/Video Tuneup/ViewController.m @@ -8,13 +8,14 @@ #import "ViewController.h" #import "PlayerView.h" +#import "SimpleEditor.h" // Define this constant for the key-value observation context. static const NSString *ItemStatusContext; @implementation ViewController -@synthesize player, playerItem, playerView, playButton, pauseButton, rewindButton; +@synthesize player, playerItem, playerView, playButton, pauseButton, rewindButton, editor; #pragma mark - Video playback @@ -40,7 +41,7 @@ static const NSString *ItemStatusContext; NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"airplane" withExtension:@"m4v"]; - AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil]; + asset = [AVURLAsset URLAssetWithURL:fileURL options:nil]; NSLog(@"Asset duration is %f", CMTimeGetSeconds([asset duration])); @@ -103,13 +104,116 @@ static const NSString *ItemStatusContext; } - (IBAction)pause:sender { + NSLog(@"Pausing..."); + [player pause]; } - (IBAction)rewind:sender { [player seekToTime:kCMTimeZero]; } + +- (IBAction)edit:sender { + + NSLog(@"Editing..."); + + // Initialize video editor + self.editor = [[SimpleEditor alloc] init]; + + NSMutableArray *clips = [NSMutableArray arrayWithCapacity:3]; + + if(asset) { + [clips addObject:asset]; + [clips addObject:asset]; + [clips addObject:asset]; + } else {NSLog(@"Error! No Asset!"); return;} + + // Copy clips into editor +// self.editor.clips = [clips copy]; + self.editor.clips = clips; + + NSLog(@"Put clips in. Count is %i", [clips count]); + + // Begin export + [self.editor buildCompositionObjectsForPlayback:NO]; + NSLog(@"Put clips in. Build."); + AVAssetExportSession *session = [self.editor assetExportSessionWithPreset:AVAssetExportPresetHighestQuality]; + NSLog(@"Session"); + + NSLog(@"begin export"); + NSString *filePath = nil; + NSUInteger count = 0; + do { + NSLog(@"Filepath"); + filePath = NSTemporaryDirectory(); + + NSString *numberString = count > 0 ? [NSString stringWithFormat:@"-%i", count] : @""; + filePath = [filePath stringByAppendingPathComponent:[NSString stringWithFormat:@"Output-%@.mov", numberString]]; + count++; + } while([[NSFileManager defaultManager] fileExistsAtPath:filePath]); + + NSLog(@"Setting stuff."); + + session.outputURL = [NSURL fileURLWithPath:filePath]; + session.outputFileType = AVFileTypeQuickTimeMovie; + + NSLog(@"Exporting asynchronously %i.", [clips count]); + + [session exportAsynchronouslyWithCompletionHandler:^ + { + dispatch_async(dispatch_get_main_queue(), ^{ + NSLog(@"Finished exporting."); + [self exportDidFinish:session]; + }); + }]; +} + +- (void)exportDidFinish:(AVAssetExportSession*)session +{ + NSLog(@"Finished export, attempting photo album"); + + NSURL *outputURL = session.outputURL; + +// _exporting = NO; +// NSIndexPath *exportCellIndexPath = [NSIndexPath indexPathForRow:2 inSection:kProjectSection]; +// ExportCell *cell = (ExportCell*)[self.tableView cellForRowAtIndexPath:exportCellIndexPath]; +// cell.progressView.progress = 1.0; +// [cell setProgressViewHidden:YES animated:YES]; +// [self updateCell:cell forRowAtIndexPath:exportCellIndexPath]; + + ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; + if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputURL]) { + [library writeVideoAtPathToSavedPhotosAlbum:outputURL + completionBlock:^(NSURL *assetURL, NSError *error){ + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + NSLog(@"writeVideoToAssestsLibrary failed: %@", error); + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[error localizedDescription] + message:[error localizedRecoverySuggestion] + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil]; + [alertView show]; +// [alertView release]; + } + else { + NSLog(@"Completed photo album add"); +// _showSavedVideoToAssestsLibrary = YES; +// ExportCell *cell = (ExportCell*)[self.tableView cellForRowAtIndexPath:exportCellIndexPath]; +// [cell setDetailTextLabelHidden:NO animated:YES]; +// [self updateCell:cell forRowAtIndexPath:exportCellIndexPath]; +// NSArray *modes = [[[NSArray alloc] initWithObjects:NSDefaultRunLoopMode, UITrackingRunLoopMode, nil] autorelease]; +// [self performSelector:@selector(hideCameraRollText) withObject:nil afterDelay:5.0 inModes:modes]; + } + }); + + }]; + } else { + NSLog(@"Video format is not compatible with saved photos album."); + } +} + - (void)playerItemDidReachEnd:(NSNotification *)notification { [player seekToTime:kCMTimeZero]; } @@ -131,9 +235,9 @@ static const NSString *ItemStatusContext; NSLog(@"viewDidLoad"); - [self syncUI]; - + // Sync video player controls NSLog(@"syncUI"); + [self syncUI]; // Register with the notification center after creating the player item. [[NSNotificationCenter defaultCenter] diff --git a/Video Tuneup/en.lproj/ViewController_iPad.xib b/Video Tuneup/en.lproj/ViewController_iPad.xib index c914faa..89c22e1 100644 --- a/Video Tuneup/en.lproj/ViewController_iPad.xib +++ b/Video Tuneup/en.lproj/ViewController_iPad.xib @@ -97,9 +97,27 @@ <object class="IBUIView" id="1061865793"> <reference key="NSNextResponder" ref="766721923"/> <int key="NSvFlags">319</int> - <string key="NSFrame">{{181, 369}, {384, 502}}</string> + <string key="NSFrame">{{174, 217}, {395, 371}}</string> <reference key="NSSuperview" ref="766721923"/> <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="440536013"/> + <string key="NSReuseIdentifierKey">_NS:212</string> + <object class="NSColor" key="IBUIBackgroundColor"> + <int key="NSColorSpace">1</int> + <bytes key="NSRGB">MC44MDAwMDAwMTE5IDEgMC40MDAwMDAwMDYAA</bytes> + </object> + <object class="IBUIAccessibilityConfiguration" key="IBUIAccessibilityConfiguration"> + <integer value="512" key="IBUIAccessibilityTraits"/> + </object> + <string key="targetRuntimeIdentifier">IBIPadFramework</string> + </object> + <object class="IBUIView" id="440536013"> + <reference key="NSNextResponder" ref="766721923"/> + <int key="NSvFlags">319</int> + <string key="NSFrame">{{174, 613}, {395, 371}}</string> + <reference key="NSSuperview" ref="766721923"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView"/> <string key="NSReuseIdentifierKey">_NS:212</string> <object class="NSColor" key="IBUIBackgroundColor"> <int key="NSColorSpace">1</int> @@ -156,13 +174,36 @@ <reference key="IBUIFontDescription" ref="165390451"/> <reference key="IBUIFont" ref="564959921"/> </object> + <object class="IBUIButton" id="739741806"> + <reference key="NSNextResponder" ref="766721923"/> + <int key="NSvFlags">301</int> + <string key="NSFrame">{{89, 580}, {72, 37}}</string> + <reference key="NSSuperview" ref="766721923"/> + <reference key="NSWindow"/> + <reference key="NSNextKeyView" ref="1061865793"/> + <string key="NSReuseIdentifierKey">_NS:241</string> + <bool key="IBUIOpaque">NO</bool> + <string key="targetRuntimeIdentifier">IBIPadFramework</string> + <int key="IBUIContentHorizontalAlignment">0</int> + <int key="IBUIContentVerticalAlignment">0</int> + <int key="IBUIButtonType">1</int> + <string key="IBUINormalTitle">Edit</string> + <reference key="IBUIHighlightedTitleColor" ref="804940373"/> + <object class="NSColor" key="IBUINormalTitleColor"> + <int key="NSColorSpace">1</int> + <bytes key="NSRGB">MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA</bytes> + </object> + <reference key="IBUINormalTitleShadowColor" ref="752673392"/> + <reference key="IBUIFontDescription" ref="165390451"/> + <reference key="IBUIFont" ref="564959921"/> + </object> <object class="IBUIButton" id="913102771"> <reference key="NSNextResponder" ref="766721923"/> <int key="NSvFlags">301</int> <string key="NSFrame">{{498, 148}, {78, 37}}</string> <reference key="NSSuperview" ref="766721923"/> <reference key="NSWindow"/> - <reference key="NSNextKeyView" ref="1061865793"/> + <reference key="NSNextKeyView" ref="739741806"/> <string key="NSReuseIdentifierKey">_NS:241</string> <bool key="IBUIOpaque">NO</bool> <string key="targetRuntimeIdentifier">IBIPadFramework</string> @@ -296,6 +337,15 @@ </object> <int key="connectionID">27</int> </object> + <object class="IBConnectionRecord"> + <object class="IBCocoaTouchEventConnection" key="connection"> + <string key="label">edit:</string> + <reference key="source" ref="739741806"/> + <reference key="destination" ref="841351856"/> + <int key="IBEventType">7</int> + </object> + <int key="connectionID">31</int> + </object> </array> <object class="IBMutableOrderedSet" key="objectRecords"> <array key="orderedObjects"> @@ -320,12 +370,14 @@ <int key="objectID">2</int> <reference key="object" ref="766721923"/> <array class="NSMutableArray" key="children"> - <reference ref="1061865793"/> <reference ref="1049445720"/> <reference ref="761978491"/> <reference ref="380079406"/> <reference ref="959867271"/> <reference ref="913102771"/> + <reference ref="1061865793"/> + <reference ref="440536013"/> + <reference ref="739741806"/> </array> <reference key="parent" ref="0"/> </object> @@ -364,6 +416,16 @@ <reference key="object" ref="761978491"/> <reference key="parent" ref="766721923"/> </object> + <object class="IBObjectRecord"> + <int key="objectID">28</int> + <reference key="object" ref="440536013"/> + <reference key="parent" ref="766721923"/> + </object> + <object class="IBObjectRecord"> + <int key="objectID">29</int> + <reference key="object" ref="739741806"/> + <reference key="parent" ref="766721923"/> + </object> </array> </object> <dictionary class="NSMutableDictionary" key="flattenedProperties"> @@ -377,6 +439,9 @@ <string key="14.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="2.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="25.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="28.CustomClassName">PlayerView</string> + <string key="28.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> + <string key="29.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="4.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> <string key="7.CustomClassName">PlayerView</string> <string key="7.IBPluginDependency">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string> @@ -385,7 +450,7 @@ <nil key="activeLocalization"/> <dictionary class="NSMutableDictionary" key="localizations"/> <nil key="sourceID"/> - <int key="maxID">27</int> + <int key="maxID">31</int> </object> <object class="IBClassDescriber" key="IBDocument.Classes"> <array class="NSMutableArray" key="referencedPartialClassDescriptions"> @@ -401,6 +466,7 @@ <string key="className">ViewController</string> <string key="superclassName">UIViewController</string> <dictionary class="NSMutableDictionary" key="actions"> + <string key="edit:">id</string> <string key="loadAssetFromFile:">id</string> <string key="loadAudioFromFile:">id</string> <string key="pause:">id</string> @@ -408,6 +474,10 @@ <string key="rewind:">id</string> </dictionary> <dictionary class="NSMutableDictionary" key="actionInfosByName"> + <object class="IBActionInfo" key="edit:"> + <string key="name">edit:</string> + <string key="candidateClassName">id</string> + </object> <object class="IBActionInfo" key="loadAssetFromFile:"> <string key="name">loadAssetFromFile:</string> <string key="candidateClassName">id</string> |
