AVCustomEdit/APLSimpleEditor.m
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Simple editor sets up an AVMutableComposition using supplied clips and time ranges. It also sets up an AVVideoComposition to perform custom compositor rendering. |
*/ |
#import "APLSimpleEditor.h" |
#import "APLTransitionTypeController.h" |
#import "APLCustomVideoCompositor.h" |
#import "APLCustomVideoCompositionInstruction.h" |
#import <CoreMedia/CoreMedia.h> |
#import <AVFoundation/AVFoundation.h> |
@interface APLSimpleEditor () |
@property (nonatomic, strong) AVMutableComposition *composition; |
@property (nonatomic, strong) AVMutableVideoComposition *videoComposition; |
@end |
@implementation APLSimpleEditor |
- (void)buildTransitionComposition:(AVMutableComposition *)composition andVideoComposition:(AVMutableVideoComposition *)videoComposition |
{ |
CMTime nextClipStartTime = kCMTimeZero; |
NSInteger i; |
NSUInteger clipsCount = [_clips count]; |
// Make transitionDuration no greater than half the shortest clip duration. |
CMTime transitionDuration = self.transitionDuration; |
for (i = 0; i < clipsCount; 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) * clipsCount); |
CMTimeRange *transitionTimeRanges = alloca(sizeof(CMTimeRange) * clipsCount); |
// Place clips into alternating video & audio tracks in composition, overlapped by transitionDuration. |
for (i = 0; i < clipsCount; 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. |
// First clip ends with a transition. |
// Second clip begins with a transition. |
// Exclude that transition 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 < clipsCount) { |
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. |
if (i+1 < clipsCount) { |
transitionTimeRanges[i] = CMTimeRangeMake(nextClipStartTime, transitionDuration); |
} |
} |
// Set up the video composition to perform cross dissolve or diagonal wipe transitions between clips. |
NSMutableArray *instructions = [NSMutableArray array]; |
// Cycle between "pass through A", "transition from A to B", "pass through B" |
for (i = 0; i < clipsCount; i++ ) { |
NSInteger alternatingIndex = i % 2; // alternating targets |
if (videoComposition.customVideoCompositorClass) { |
APLCustomVideoCompositionInstruction *videoInstruction = [[APLCustomVideoCompositionInstruction alloc] initPassThroughTrackID:compositionVideoTracks[alternatingIndex].trackID forTimeRange:passThroughTimeRanges[i]]; |
[instructions addObject:videoInstruction]; |
} |
else { |
// 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 < clipsCount) { |
// Add transition from clip i to clip i+1. |
if (videoComposition.customVideoCompositorClass) { |
APLCustomVideoCompositionInstruction *videoInstruction = [[APLCustomVideoCompositionInstruction alloc] initTransitionWithSourceTrackIDs:@[[NSNumber numberWithInt:compositionVideoTracks[0].trackID], [NSNumber numberWithInt:compositionVideoTracks[1].trackID]] forTimeRange:transitionTimeRanges[i]]; |
if (alternatingIndex == 0) { |
// First track -> Foreground track while compositing |
videoInstruction.foregroundTrackID = compositionVideoTracks[alternatingIndex].trackID; |
// Second track -> Background track while compositing |
videoInstruction.backgroundTrackID = compositionVideoTracks[1-alternatingIndex].trackID; |
} |
[instructions addObject:videoInstruction]; |
} |
else { |
AVMutableVideoCompositionInstruction *transitionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction]; |
transitionInstruction.timeRange = transitionTimeRanges[i]; |
AVMutableVideoCompositionLayerInstruction *fromLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTracks[alternatingIndex]]; |
AVMutableVideoCompositionLayerInstruction *toLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:compositionVideoTracks[1-alternatingIndex]]; |
transitionInstruction.layerInstructions = [NSArray arrayWithObjects:fromLayer, toLayer, nil]; |
[instructions addObject:transitionInstruction]; |
} |
} |
} |
videoComposition.instructions = instructions; |
} |
- (void)buildCompositionObjectsForPlayback:(BOOL)forPlayback |
{ |
if ( (_clips == nil) || [_clips count] == 0 ) { |
self.composition = nil; |
self.videoComposition = nil; |
return; |
} |
CGSize videoSize = [[_clips objectAtIndex:0] naturalSize]; |
AVMutableComposition *composition = [AVMutableComposition composition]; |
AVMutableVideoComposition *videoComposition = nil; |
composition.naturalSize = videoSize; |
// 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". |
videoComposition = [AVMutableVideoComposition videoComposition]; |
if (self.transitionType == kDiagonalWipeTransition) { |
videoComposition.customVideoCompositorClass = [APLDiagonalWipeCompositor class]; |
} else { |
videoComposition.customVideoCompositorClass = [APLCrossDissolveCompositor class]; |
} |
[self buildTransitionComposition:composition andVideoComposition:videoComposition]; |
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; |
} |
- (AVAssetExportSession*)assetExportSessionWithPreset:(NSString*)presetName |
{ |
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:self.composition presetName:presetName]; |
session.videoComposition = self.videoComposition; |
return session; |
} |
- (AVPlayerItem *)playerItem |
{ |
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:self.composition]; |
playerItem.videoComposition = self.videoComposition; |
return playerItem; |
} |
@end |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-08-17