1 媒體的組合和編輯
AVFoundation提供了大量API來(lái)創(chuàng)建非線性、無(wú)損的編輯工具和應(yīng)用程序。
1.1 組合媒體核心類

組合媒體的核心類時(shí)AVComposition。AVAsset和具體的媒體文件是一一對(duì)應(yīng)的映射關(guān)系,組合更像是一個(gè)說(shuō)明,描述多個(gè)資源如何正確的呈現(xiàn)和處理。AVComposition沒(méi)有遵守NSCoding協(xié)議,因此不能直接保存數(shù)據(jù)庫(kù),只能保存必要的屬性,在需要時(shí)創(chuàng)建。
1.2 時(shí)間的處理
CMTime
AVFoundation中媒體資源時(shí)間的處理為了保存精度,使用CMTime結(jié)構(gòu)體,其中包含value、timeScale、flags、epoch四個(gè)屬性,value和timeScale分別是64和32位有符號(hào)的整形變量,具體的時(shí)間等于value/timeScale。Flags標(biāo)識(shí)數(shù)據(jù)是否有效、不確定或者是否出現(xiàn)舍入值等。對(duì)于視頻資源timeScale通常設(shè)置為常見視頻幀率的公倍數(shù)600,對(duì)于音頻資源常設(shè)置為采樣率,如44100(44.1Hz)等。時(shí)間的加減法使用CMTimeAdd和CMTimeSubtract,時(shí)間的比較使用CMTimeCompareIsInline。
CMTimeRange
CMTimeRange表示一個(gè)時(shí)間尺度,可以使用CMTimeRangeMake函數(shù)初始化,也可以使用CMTimeRangeFromTimeToTime初始化。時(shí)間尺度取交集使用GetInterSection,取并集使用GetUnion。
1.3 AVURLAsset
播放媒體資源的時(shí)候通常直接創(chuàng)建AVAsset,但是在編輯媒體資源時(shí)需要?jiǎng)?chuàng)建其子類對(duì)象AVURLAsset,并在實(shí)例方法的options中配置AVURL...Duration...TimeKey為YES,這樣在后續(xù)獲取asset時(shí)間相關(guān)屬性時(shí)更精確,盡管這需要增加開銷。
1.4 組合媒體

首先需要定義組合媒體中需要用到的類,CompositionBuilderFactory負(fù)責(zé)管理CompositionBuilder,CompositionBuilder負(fù)責(zé)具體的創(chuàng)建組合對(duì)象并將創(chuàng)建好的AVComposition封裝到THBaseComposition中,THBaseComposition負(fù)責(zé)將返回可播放的AVPlayerItem對(duì)象或者導(dǎo)出的預(yù)設(shè)值。THCompositionExporter包含一個(gè)THBaseComposition對(duì)象,負(fù)責(zé)將組合導(dǎo)出成mov文件。
THTimeLine對(duì)象表示一個(gè)時(shí)間軸對(duì)象,由多個(gè)THTimelineItem組成,每個(gè)THTimelineItem對(duì)象表示時(shí)間軸上的一個(gè)資源。其中鍵值著資源在時(shí)間軸上的位置。為了更好的載入媒體資源,創(chuàng)建子類THMediaItem,它封裝了AVAsset對(duì)象,可以載入track等資源,為視頻編輯做好準(zhǔn)備。為了區(qū)分媒體類型,創(chuàng)建THVideoItem和THAudioItem子類。
THTimeline
typedef NS_ENUM(NSInteger, THTrack) {
THVideoTrack = 0,
THTitleTrack,
THCommentaryTrack,
THMusicTrack
};
@interface THTimeline : NSObject
@property (strong, nonatomic) NSArray *videos;
@property (strong, nonatomic) NSArray *transitions;
@property (strong, nonatomic) NSArray *titles;
@property (strong, nonatomic) NSArray *voiceOvers;
@property (strong, nonatomic) NSArray *musicItems;
- (BOOL)isSimpleTimeline;
@end
THTimelineItem
@interface THTimelineItem : NSObject
@property (nonatomic) CMTimeRange timeRange;
@property (nonatomic) CMTime startTimeInTimeline;
@end
THMediaItem
typedef void(^THPreparationCompletionBlock)(BOOL complete);
@interface THMediaItem : THTimelineItem
@property (strong, nonatomic) AVAsset *asset;
@property (nonatomic, readonly) BOOL prepared;
@property (nonatomic, readonly) NSString *mediaType;
@property (nonatomic, copy, readonly) NSString *title;
- (id)initWithURL:(NSURL *)url;
// 預(yù)加載static NSString *const AVAssetTracksKey = @"tracks";
// static NSString *const AVAssetDurationKey = @"duration";
// static NSString *const AVAssetCommonMetadataKey = @"commonMetadata";等屬性
- (void)prepareWithCompletionBlock:(THPreparationCompletionBlock)completionBlock;
- (void)performPostPrepareActionsWithCompletionBlock:(THPreparationCompletionBlock)completionBlock;
- (BOOL)isTrimmed;
- (AVPlayerItem *)makePlayable;
@end
THAudioItem
@interface THAudioItem : THMediaItem
@property (strong, nonatomic) NSArray *volumeAutomation;
+ (id)audioItemWithURL:(NSURL *)url;
@end
THVideoItem
@interface THVideoItem : THMediaItem
@property (strong, nonatomic) NSArray *thumbnails;
@property (strong, nonatomic) THVideoTransition *startTransition;
@property (strong, nonatomic) THVideoTransition *endTransition;
@property (nonatomic, readonly) CMTimeRange playthroughTimeRange;
@property (nonatomic, readonly) CMTimeRange startTransitionTimeRange;
@property (nonatomic, readonly) CMTimeRange endTransitionTimeRange;
+ (id)videoItemWithURL:(NSURL *)url;
@end
1-初始化THComposition協(xié)議
@protocol THComposition <NSObject>
- (AVPlayerItem *)makePlayable;
- (AVAssetExportSession *)makeExportable;
@end
2-初始化THBasicComposition
@interface THBasicComposition : NSObject <THComposition>
@property (strong, readonly, nonatomic) AVComposition *composition;
+ (instancetype)compositionWithComposition:(AVComposition *)composition;
- (instancetype)initWithComposition:(AVComposition *)composition;
@end
@interface THBasicComposition ()
@property (strong, nonatomic) AVComposition *composition;
@end
@implementation THBasicComposition
+ (id)compositionWithComposition:(AVComposition *)composition {
return [[self alloc] initWithComposition:composition];
}
- (id)initWithComposition:(AVComposition *)composition {
if (self = [super init]) {
_composition = composition;
}
return self;
}
- (AVPlayerItem *)makePlayable {
return [AVPlayerItem playerItemWithAsset:[self.composition copy]];
}
- (AVAssetExportSession *)makeExportable {
NSString *presset = AVAssetExportPresetHighestQuality;
return [AVAssetExportSession exportSessionWithAsset:self.composition.copy presetName:presset];
}
@end
3-初始化** THCompositionBuilder**
@protocol THCompositionBuilder <NSObject>
- (id <THComposition>)buildComposition;
@end
4-初始化** THBasicCompositionBuilder**
@interface THBasicCompositionBuilder : NSObject <THCompositionBuilder>
- (id)initWithTimeline:(THTimeline *)timeline;
@end
@interface THBasicCompositionBuilder ()
@property (strong, nonatomic) THTimeline *timeline;
@property (strong, nonatomic) AVMutableComposition *composition;
@end
@implementation THBasicCompositionBuilder
- (id)initWithTimeline:(THTimeline *)timeline {
if (self = [super init]) {
_timeline = timeline;
}
return self;
}
- (id <THComposition>)buildComposition {
self.composition = [AVMutableComposition composition];
[self addCompositionTrackOfType:AVMediaTypeVideo withMediaItems:self.timeline.videos];
[self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.voiceOvers];
[self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.musicItems];
return [THBasicComposition compositionWithComposition:self.composition];
}
- (void)addCompositionTrackOfType:(NSString *)mediaType
withMediaItems:(NSArray *)mediaItems {
if (!THIsEmpty(mediaItems)) {
// 使用TrackID_Invalid時(shí)候,AVFoundation會(huì)自動(dòng)管理軌道id從1到n
CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
AVMutableCompositionTrack *compositionTrack = [self.composition addMutableTrackWithMediaType:mediaType preferredTrackID:trackID];
CMTime cursorTime = kCMTimeZero;
for (THMediaItem *item in mediaItems) {
// 視頻、音頻、配音的mediaType都包含startTimeInTimeline屬性,視頻和音頻必須是連續(xù)的,因此此屬性為kCMTimeInvalid,配音可是是在任意位置插入,并且可以不連續(xù),因此其屬性有具體的時(shí)間
if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline, !=, kCMTimeInvalid)) {
cursorTime = item.startTimeInTimeline;
}
AVAssetTrack *assetTrack = [item.asset tracksWithMediaType:mediaType].firstObject;
[compositionTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
}
}
}
@end
1.4 導(dǎo)出媒體
**初始化**
@interface THCompositionExporter : NSObject
@property (nonatomic) BOOL exporting;
@property (nonatomic) CGFloat progress;
- (instancetype)initWithComposition:(id <THComposition>)composition;
- (void)beginExport;
@end
@interface THCompositionExporter ()
@property (strong, nonatomic) id <THComposition> composition;
@property (strong, nonatomic) AVAssetExportSession *exportSession;
@end
@implementation THCompositionExporter
- (instancetype)initWithComposition:(id <THComposition>)composition {
if (self = [super init]) {
_composition = composition;
}
return self;
}
- (void)beginExport {
self.exportSession = [self.composition makeExportable];
self.exportSession.outputURL = [self exportURL];
self.exportSession.outputFileType = AVFileTypeMPEG4;
[self.exportSession exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
AVAssetExportSessionStatus status = self.exportSession.status;
if (status == AVAssetExportSessionStatusCompleted) {
[self writeExportedVideoToAssetsLibrary];
} else {
[UIAlertView showAlertWithTitle:@"Export falied" message:@"The requested export failed"];
}
});
}];
self.exporting = YES;
[self monitorExportProgress];
}
- (void)monitorExportProgress {
double delayInSeconds = 0.1;
int64_t delta = (int64_t)delayInSeconds *NSEC_PER_SEC;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delta);
dispatch_after(popTime, dispatch_get_main_queue(), ^{
AVAssetExportSessionStatus status = self.exportSession.status;
if (status == AVAssetExportSessionStatusExporting) {
self.progress = self.exportSession.progress;
[self monitorExportProgress];
} else {
self.exporting = NO;
}
});
}
- (void)writeExportedVideoToAssetsLibrary {
NSURL *exportURL = self.exportSession.outputURL;
NSError *error = nil;
__block PHObjectPlaceholder *createdAsset = nil;
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
createdAsset = [PHAssetCreationRequest creationRequestForAssetFromVideoAtFileURL:exportURL].placeholderForCreatedAsset;
} error:&error];
if (error || !createdAsset) {
NSString *message = @"Unable to write to Photos Library";
[UIAlertView showAlertWithTitle:@"Write Failed" message:message];
}
[[NSFileManager defaultManager] removeItemAtURL:exportURL error:nil];
}
- (NSURL *)exportURL {
NSString *filePath = nil;
NSUInteger count = 0;
do {
filePath = NSTemporaryDirectory();
NSString *numberString = count > 0 ?
[NSString stringWithFormat:@"-%li", (unsigned long) count] : @"";
NSString *fileNameString =
[NSString stringWithFormat:@"Masterpiece-%@.m4v", numberString];
filePath = [filePath stringByAppendingPathComponent:fileNameString];
count++;
} while ([[NSFileManager defaultManager] fileExistsAtPath:filePath]);
return [NSURL fileURLWithPath:filePath];
}
@end
2 混合音頻
當(dāng)有多個(gè)音頻軌道時(shí),如音樂(lè)軌道和配音軌道,通常希望在有配音時(shí),背景音樂(lè)音量降低。在AVFoundation中AVAudioMix及其相關(guān)類負(fù)責(zé)音頻軌道的音量處理。在AVAudioMix中軌道的音量大小在0~1之間,默認(rèn)的行為是每個(gè)軌道都以最大音量1播放。AVMutable...Parameters提供了兩個(gè)方法用于立即設(shè)置音量到某個(gè)值,和在某個(gè)范圍內(nèi)將值由一個(gè)值設(shè)置到另外一個(gè)值。對(duì)于組合,對(duì)于本地媒體資源,對(duì)于媒體輸出都可以設(shè)置AVAudioMix來(lái)控制音頻播放和輸出的行為。

1- 初始化THAudioMixComposition負(fù)責(zé)提供可播放對(duì)象和導(dǎo)出預(yù)設(shè)值
@interface THAudioMixComposition : NSObject <THComposition>
@property (strong, nonatomic, readonly) AVAudioMix *audioMix;
@property (strong, nonatomic, readonly) AVComposition *composition;
+ (instancetype)compositionWithComposition:(AVComposition *)composition
audioMix:(AVAudioMix *)audioMix;
- (instancetype)initWithComposition:(AVComposition *)composition
audioMix:(AVAudioMix *)audioMix;
@end
@interface THAudioMixComposition ()
@property (strong, nonatomic) AVAudioMix *audioMix;
@property (strong, nonatomic) AVComposition *composition;
@end
@implementation THAudioMixComposition
+ (instancetype)compositionWithComposition:(AVComposition *)composition audioMix:(AVAudioMix *)audioMix {
return [[self alloc] initWithComposition:composition audioMix:audioMix];
}
- (instancetype)initWithComposition:(AVComposition *)composition audioMix:(AVAudioMix *)audioMix {
if (self = [super init]) {
_composition = composition;
_audioMix = audioMix;
}
return self;
}
- (AVPlayerItem *)makePlayable {
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:[self.composition copy]];
playerItem.audioMix = self.audioMix;
return playerItem;
}
- (AVAssetExportSession *)makeExportable {
NSString *preset = AVAssetExportPresetHighestQuality;
AVAssetExportSession *session = [AVAssetExportSession exportSessionWithAsset:[self.composition copy] presetName:preset];
session.audioMix = self.audioMix;
return session;
}
@end
2- 初始化THAudioMixCompositionBuilder負(fù)責(zé)編輯媒體
@interface THAudioMixCompositionBuilder : NSObject <THCompositionBuilder>
- (id)initWithTimeline:(THTimeline *)timeline;
@end
@interface THAudioMixCompositionBuilder ()
@property (strong, nonatomic) THTimeline *timeline;
@property (strong, nonatomic) AVMutableComposition *composition;
@end
@implementation THAudioMixCompositionBuilder
- (id)initWithTimeline:(THTimeline *)timeLine {
if (self = [super init]) {
_timeline = timeLine;
}
return self;
}
- (id<THComposition>)buildComposition {
self.composition = [AVMutableComposition composition];
[self addCompositionTrackOfType:AVMediaTypeVideo withMediaItems:self.timeline.videos];
[self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.voiceOvers];
AVMutableCompositionTrack *musicTrack = [self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.musicItems];
AVAudioMix *audioMix = [self buildAudioMixWithTrack:musicTrack];
return [THAudioMixComposition compositionWithComposition:self.composition.copy audioMix:audioMix];
}
- (AVMutableCompositionTrack *)addCompositionTrackOfType:(NSString *)type withMediaItems:(NSArray *)mediaItems {
if (!THIsEmpty(mediaItems)) {
CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
AVMutableCompositionTrack *compositionTrack = [self.composition addMutableTrackWithMediaType:type preferredTrackID:trackID];
CMTime cursorTime = kCMTimeZero;
for (THMediaItem *item in mediaItems) {
if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline, !=, kCMTimeInvalid)) {
cursorTime = item.startTimeInTimeline;
}
AVAssetTrack *assetTrack = [[item.asset tracksWithMediaType:type] firstObject];
[compositionTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
}
return compositionTrack;
}
return nil;
}
- (AVAudioMix *)buildAudioMixWithTrack:(AVMutableCompositionTrack *)track {
THAudioItem *item = [self.timeline.musicItems firstObject];
if (item) {
AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
AVMutableAudioMixInputParameters *parameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:track];
for (THVolumeAutomation *automation in item.volumeAutomation) {
[parameters setVolumeRampFromStartVolume:automation.startVolume toEndVolume:automation.endVolume timeRange:automation.timeRange];
}
audioMix.inputParameters = @[parameters];
return audioMix;
}
return nil;
}
@end
3 創(chuàng)建視頻過(guò)度效果及組合視頻軌道
3.1 核心類

AVFoundation中AVVideoComposition是描述視頻組合各個(gè)視頻軌道應(yīng)該具體如何呈現(xiàn)的核心類。它可以被設(shè)置到AVPlayerItem、AVAssetExportSession等中,以用于播放和導(dǎo)出特定軌道組合效果的視頻對(duì)象。AVVideoComposition由多個(gè)Instruction構(gòu)成,每個(gè)Instruction描述了一個(gè)時(shí)間段timeRange,其屬性layerInstructions中每一個(gè)對(duì)象都描述了對(duì)應(yīng)軌道視頻資源呈現(xiàn)方式。
在實(shí)際操作時(shí),通常先建立AVComposition對(duì)象,并在軌道上添加必要的媒體信息,對(duì)于多個(gè)視頻軌道,在導(dǎo)出或者播放AVComposition時(shí),都會(huì)在軌道上沒(méi)有媒體數(shù)據(jù)地方插入一個(gè)空片段。并且導(dǎo)出后視頻僅含有一個(gè)視頻軌道。Apple Developer Center中含有一個(gè)AVCompositionDebugView的APP示例,可以顯示軌道信息。幫助調(diào)試程序。
在一個(gè)AVComposition中使用多個(gè)視頻軌道并且沒(méi)有配置AVVideoComposition屬性時(shí),在播放時(shí)只有索引為1的軌道會(huì)被渲染,導(dǎo)出同樣。創(chuàng)建AVVideoComposition的方式有兩種。
1)手動(dòng)創(chuàng)建:直接通過(guò)AVVideoComposition的init方法創(chuàng)建,再為其添加Instruction數(shù)組,Instruction包含時(shí)間信息,再為Instruction添加layerInstructions屬性,設(shè)置每個(gè)軌道的層展示方式。設(shè)置AVVideoComposition的rendersize、frameduration和renderScale,renderScale通常使用默認(rèn)縮放比1.0,frameduration通常不設(shè)置,使用媒體默認(rèn)幀率。
2)快捷創(chuàng)建:通過(guò)AVVideoComposition的videoComposition...OfAsset:類方法創(chuàng)建,這種方式穿件的videoComposition會(huì)包含。
- Instructions:包含完整的基于組合視頻軌道及其中包含片段空間布局的組合和層指令。通常其中默認(rèn)的層布局指令layerInstruction都是全屏展示,需要對(duì)過(guò)度的Instruction和layerInstruction重新設(shè)置展示方式。
- redersize:設(shè)置為AVComposition的naturalSize,若其為空,則設(shè)置為最大視頻維度的尺寸值。
- frameDuration:設(shè)置為組合中最大軌道的nominalFrameRate,如果所有軌道改值都為0,則設(shè)置為1/30.(30FPS)。
- renderScale:始終為1。
3.1 邏輯實(shí)現(xiàn)
3.1.1 TransitionComposition
@interface THTransitionComposition : NSObject <THComposition>
@property (strong, nonatomic, readonly) AVComposition *composition;
@property (strong, nonatomic, readonly) AVVideoComposition *videoComposition;
@property (strong, nonatomic, readonly) AVAudioMix *audioMix;
- (id)initWithComposition:(AVComposition *)composition videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix;
@end
@implementation THTransitionComposition
- (id)initWithComposition:(AVComposition *)composition videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix {
if (self = [super init]) {
_composition = composition;
_videoComposition = videoComposition;
_audioMix = audioMix;
}
return self;
}
- (AVPlayerItem *)makePlayable {
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:[self.composition copy]];
playerItem.audioMix = self.audioMix;
playerItem.videoComposition = self.videoComposition;
return playerItem;
}
- (AVAssetExportSession *)makeExportable {
NSString *preset = AVAssetExportPresetHighestQuality;
AVAssetExportSession *session = [AVAssetExportSession exportSessionWithAsset:[self.composition copy] presetName:preset];
session.audioMix = self.audioMix;
session.videoComposition = self.videoComposition;
return session;
}
@end
3.1.2 TransitionCompositionBuilder
@interface THTransitionCompositionBuilder : NSObject <THCompositionBuilder>
- (id)initWithTimeline:(THTimeline *)timeline;
@end
@interface THTransitionCompositionBuilder()
@property (nonatomic, strong) THTimeline *timeline;
@property (nonatomic, strong) AVMutableComposition *composition;
@property (nonatomic, weak) AVMutableCompositionTrack *musicTrack;
@end
@implementation THTransitionCompositionBuilder
- (id)initWithTimeline:(THTimeline *)timeline {
if (self = [super init]) {
_timeline = timeline;
}
return self;
}
- (id<THComposition>)buildComposition {
self.composition = [AVMutableComposition composition];
[self buildCompositionTracks];
AVVideoComposition *videoComposition = [self buildVideoComposition];
AVAudioMix *audioMix = [self buildAudioMix];
return [[THTransitionComposition alloc] initWithComposition:self.composition.copy videoComposition:videoComposition audioMix:audioMix];
}
- (void)buildCompositionTracks {}
- (AVVideoComposition *)buildVideoComposition {
AVVideoComposition *videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:self.composition].copy;
NSArray *transitionInstructions = [self transitionInstructionsInVideoComposition:videoComposition];
for (THTransitionInstructions *instructions in transitionInstructions) {
CMTimeRange timeRange = instructions.compositionInstruction.timeRange;
AVMutableVideoCompositionLayerInstruction *fromLayer = instructions.fromLayerInstruction;
AVMutableVideoCompositionLayerInstruction *toLayer = instructions.toLayerInstruction;
THVideoTransitionType type = instructions.transition.type;
// 動(dòng)畫處理
if (type == THVideoTransitionTypeDissolve) {
} else if (type == THVideoTransitionTypePush) {
} else if (type == THVideoTransitionTypeWipe) {
}
instructions.compositionInstruction.layerInstructions = @[fromLayer,toLayer];
}
return videoComposition;
}
- (AVMutableCompositionTrack *)addCompositionTrackOfType:(NSString *)type withMediaItems:(NSArray *)mediaItems {
if (!THIsEmpty(mediaItems)) {
CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
AVMutableCompositionTrack *compositionTrack = [self.composition addMutableTrackWithMediaType:type preferredTrackID:trackID];
CMTime cursorTime = kCMTimeZero;
for (THMediaItem *item in mediaItems) {
if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline, !=, kCMTimeInvalid)) {
cursorTime = item.startTimeInTimeline;
}
AVAssetTrack *assetTrack = [[item.asset tracksWithMediaType:type] firstObject];
[compositionTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
}
return compositionTrack;
}
return nil;
}
- (AVAudioMix *)buildAudioMix {}
- (NSArray *)transitionInstructionsInVideoComposition:(AVVideoComposition *)videoComposition {}
@end
創(chuàng)建視頻軌道
- (void)buildCompositionTracks {
CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
AVMutableCompositionTrack *compositionTrackA = [self.composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:trackID];
AVMutableCompositionTrack *compositionTrackB = [self.composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:trackID];
NSArray *videoTracks = @[compositionTrackA,compositionTrackB];
CMTime cursorTime = kCMTimeZero;
CMTime transitionDuration = kCMTimeZero;
if (!THIsEmpty(self.timeline.transitions)) {
transitionDuration = THDefaultTransitionDuration;
}
NSArray *videos = self.timeline.videos;
for (NSUInteger i = 0; i < videos.count; i++) {
NSUInteger trackIndex = i%2;
THVideoItem *item = videos[i];
AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
AVAssetTrack *assetTrack = [[item.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
[currentTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
}
[self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.voiceOvers];
NSArray *musicItems = self.timeline.musicItems;
self.musicTrack = [self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:musicItems];
}
混合音頻軌道
- (AVAudioMix *)buildAudioMix {
NSArray *items = self.timeline.musicItems;
if (items.count == 1) {
THAudioItem *item = self.timeline.musicItems[0];
AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];
AVMutableAudioMixInputParameters *parameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:self.musicTrack];
for (THVolumeAutomation *automation in item.volumeAutomation) {
[parameters setVolumeRampFromStartVolume:automation.startVolume toEndVolume:automation.endVolume timeRange:automation.timeRange];
}
audioMix.inputParameters = @[parameters];
return audioMix;
}
return nil;
}
獲取視頻轉(zhuǎn)場(chǎng)指令
- (NSArray *)transitionInstructionsInVideoComposition:(AVVideoComposition *)videoComposition {
NSMutableArray *transitionInstructions = [NSMutableArray array];
int layerInstructionIndex = 1;
NSArray *compositionInstructions = videoComposition.instructions;
for (AVMutableVideoCompositionInstruction *vci in compositionInstructions) {
if (vci.layerInstructions.count == 2) {
THTransitionInstructions *instructions = [[THTransitionInstructions alloc] init];
instructions.compositionInstruction = vci;
instructions.fromLayerInstruction = (AVMutableVideoCompositionLayerInstruction *)vci.layerInstructions[1 - layerInstructionIndex];
instructions.toLayerInstruction = (AVMutableVideoCompositionLayerInstruction *)vci.layerInstructions[layerInstructionIndex];
[transitionInstructions addObject:instructions];
layerInstructionIndex = layerInstructionIndex == 1 ? 0 : 1;
}
}
NSArray *transitions = self.timeline.transitions;
if (THIsEmpty(transitions)) {
return transitionInstructions;
}
NSAssert(transitionInstructions.count == transitions.count, @"Instruction count and transition count do not match.");
for (NSUInteger i = 0 ; i < transitionInstructions.count; i++) {
THTransitionInstructions *tis = transitionInstructions[i];
tis.transition = self.timeline.transitions[i];
}
return transitionInstructions;
}
多段視頻轉(zhuǎn)場(chǎng)效果實(shí)現(xiàn)
- (AVVideoComposition *)buildVideoComposition {
AVVideoComposition *videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:self.composition].copy;
NSArray *transitionInstructions = [self transitionInstructionsInVideoComposition:videoComposition];
for (THTransitionInstructions *instructions in transitionInstructions) {
CMTimeRange timeRange = instructions.compositionInstruction.timeRange;
AVMutableVideoCompositionLayerInstruction *fromLayer = instructions.fromLayerInstruction;
AVMutableVideoCompositionLayerInstruction *toLayer = instructions.toLayerInstruction;
THVideoTransitionType type = instructions.transition.type;
if (type == THVideoTransitionTypeDissolve) {
[fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:timeRange];
} else if (type == THVideoTransitionTypePush) {
CGAffineTransform identityTransform = CGAffineTransformIdentity;
CGFloat videoWidth = videoComposition.renderSize.width;
CGAffineTransform fromDestTransform = CGAffineTransformMakeTranslation(-videoWidth, 0.0f);
CGAffineTransform toStartTransform = CGAffineTransformMakeTranslation(videoWidth, 0.0);
[fromLayer setTransformRampFromStartTransform:identityTransform toEndTransform:fromDestTransform timeRange:timeRange];
[toLayer setTransformRampFromStartTransform:toStartTransform toEndTransform:identityTransform timeRange:timeRange];
} else if (type == THVideoTransitionTypeWipe) {
CGFloat videoWidth = videoComposition.renderSize.width;
CGFloat videoHight = videoComposition.renderSize.height;
CGRect startRect = CGRectMake(0.0f, 0.0f, videoWidth, videoHight);
CGRect endRect = CGRectMake(0.0f, videoHight, videoWidth, 0);
[fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];
}
instructions.compositionInstruction.layerInstructions = @[fromLayer,toLayer];
}
return videoComposition;
}
4 動(dòng)畫圖層內(nèi)容
在視頻處理時(shí),有時(shí)候常常希望添加上一些疊加效果,如水印、標(biāo)題、下沿字母等。此時(shí)需要結(jié)合AVFoundation框架好Core Animation兩個(gè)框架來(lái)實(shí)現(xiàn)這個(gè)功能。
4.1 Core Animation簡(jiǎn)介
Core Animation是基于GPU提供硬件加速的圖像渲染框架。其主要分為L(zhǎng)ayers和Animations兩部分。Core Animation將另起文章研究。另Lockwood的iOS Core Animation。
- Layers:基本類為CALayer,用于管理屏幕中可視內(nèi)容元素。一般指圖片或者Bezier線條。Layer自身也可以設(shè)置可視化屬性,如背景色等。CATextLayer和CAShapeLayer分別用于文字和Bezier曲線的渲染。
- Animations:其基本類是一個(gè)抽象類CAAnimation,根據(jù)不同的需要定義了CABasicAnimation、CAKeyFrameAnimation等。這里需要注意CAAnimationGroup最好不要結(jié)合AVFoundation使用。
4.2 使用Core Animation
4.2.1 非AVFoundation環(huán)境
在普通環(huán)境下,Core Animation渲染圖像的時(shí)間是基于主機(jī)時(shí)間渲染。主機(jī)時(shí)間是從系統(tǒng)啟動(dòng)時(shí)開始計(jì)算并單向向前推進(jìn)。

4.2.2 AVFoundation環(huán)境
在AVFoundation中的時(shí)時(shí)播放時(shí),我們需要在預(yù)覽視圖上添加一個(gè)AVSynchronizedLayer,并將其關(guān)聯(lián)對(duì)應(yīng)的AVPlayerItem,這樣在AVSynchronizedLayer上所有的子視圖上的動(dòng)畫都將依據(jù)對(duì)應(yīng)的AVPlayerItem的時(shí)間執(zhí)行,當(dāng)AVPlayerItem暫停、倒退時(shí)動(dòng)畫都將做出相應(yīng)反饋。

在AVFoundation中的導(dǎo)出CoreAnimation時(shí),AVFoundation提供了一個(gè)可以整合視頻圖層和動(dòng)畫圖層的AVVideoCompositionCoreAnimationTool工具類。

AVVideoCompositionCoreAnimationTool可以將組合視頻幀整合于視頻圖層中,并在其中渲染動(dòng)畫效果,但是使用時(shí)應(yīng)注意以下兩點(diǎn)。
- 不能移除動(dòng)畫:Core Animation的默認(rèn)行為是執(zhí)行動(dòng)畫,然后移除動(dòng)畫,但是在AVFoundation中動(dòng)畫會(huì)被反復(fù)執(zhí)行,因此其removedOnCompetition必須設(shè)置為NO。
- 開始時(shí)間不能設(shè)置為0.0:動(dòng)畫的開始時(shí)間設(shè)置為0.0時(shí)將會(huì)轉(zhuǎn)換為當(dāng)前主機(jī)的時(shí)間CACurrentMeidaTIme()。這樣的時(shí)間不會(huì)和AVPlayerItem關(guān)聯(lián),動(dòng)畫將永遠(yuǎn)不會(huì)執(zhí)行,需要設(shè)置為AVCoreAnimationBeginTimeAtZero常量。
4.3 在視頻組合中創(chuàng)建動(dòng)畫
為了整合前幾章內(nèi)容,設(shè)計(jì)THTimeLineItem子類THTitleItem來(lái)負(fù)責(zé)創(chuàng)建和管理具體需要渲染的動(dòng)畫Layer。

4.3.1 初始化TitleItem
@interface THTitleItem : THTimelineItem
@property (copy, nonatomic) NSString *identifier;
@property (nonatomic) BOOL animateImage;
@property (nonatomic) BOOL useLargeFont;
+ (instancetype)titleItemWithText:(NSString *)text image:(UIImage *)image;
- (instancetype)initWithText:(NSString *)text image:(UIImage *)image;
- (CALayer *)buildLayer;
@end
@interface THTitleItem ()
@property (copy, nonatomic) NSString *text;
@property (strong, nonatomic) UIImage *image;
@property (nonatomic) CGRect bounds;
@end
@implementation THTitleItem
+ (instancetype)titleItemWithText:(NSString *)text image:(UIImage *)image {
return [[self alloc] initWithText:text image:image];
}
- (instancetype)initWithText:(NSString *)text image:(UIImage *)image {
if (self = [super init]) {
_text = text;
_image = image;
_bounds = TH720pVideoRect;
}
return self;
}
- (CALayer *)buildLayer {
CALayer *presentLayer = [CALayer layer];
presentLayer.frame = self.bounds;
presentLayer.opacity = 0.0f;
CALayer *imageLayer = [self makeImageLayer];
[presentLayer addSublayer:imageLayer];
CALayer *textLayer = [self makeTextLayer];
[presentLayer addSublayer:textLayer];
CAAnimation *fadeInFadeOutAnimation = [self makeFadeInFadeOutAnimation];
[presentLayer addAnimation:fadeInFadeOutAnimation forKey:nil];
if (self.animateImage) {
presentLayer.sublayerTransform = THMakePerspectiveTransform(1000);
CAAnimation *spinAnimation = [self make3DSpinAnimation];
NSTimeInterval offset = spinAnimation.beginTime + spinAnimation.duration - 0.5f;
CAAnimation *popAnimation = [self makePopAnimationWithTimingOffset:offset];
[imageLayer addAnimation:spinAnimation forKey:nil];
[imageLayer addAnimation:popAnimation forKey:nil];
}
return presentLayer;
}
- (CALayer *)makeImageLayer {
CGSize imageSize = self.image.size;
CALayer *layer = [CALayer layer];
layer.contents = (id)self.image.CGImage;
// 圖片邊緣應(yīng)用抗鋸齒效果
layer.allowsEdgeAntialiasing = YES;
layer.bounds = CGRectMake(0.0f, 0.0f, imageSize.width, imageSize.height);
layer.position = CGPointMake(CGRectGetMidX(self.bounds)-20.0f, 270.0f);
return layer;
}
- (CALayer *)makeTextLayer {
CGFloat fontSize = self.useLargeFont ? 64.0f : 54.0f;
UIFont *font = [UIFont fontWithName:@"GillSans-Bold" size:fontSize];
NSDictionary *attrs = @{NSFontAttributeName : font, NSForegroundColorAttributeName : (id)[UIColor whiteColor].CGColor};
NSAttributedString *string = [[NSAttributedString alloc] initWithString:self.text attributes:attrs];
CGSize textSize = [self.text sizeWithAttributes:attrs];
CATextLayer *layer = [CATextLayer layer];
layer.string = string;
layer.bounds = CGRectMake(0.0f, 0.0f, textSize.width, textSize.height);
layer.position = CGPointMake(CGRectGetMidX(self.bounds), 470.0f);
layer.backgroundColor = [UIColor clearColor].CGColor;
return layer;
}
static CATransform3D THMakePerspectiveTransform(CGFloat eyePosition) {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / eyePosition;
return transform;
}
@end
4.3.2 添加動(dòng)畫效果
- (CAAnimation *)makeFadeInFadeOutAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"];
animation.values = @[@0.0f, @1.0, @1.0, @0.0];
animation.keyTimes = @[@0.0, @0.2, @0.8, @1.0];
animation.beginTime = CMTimeGetSeconds(self.startTimeInTimeline);
animation.duration = CMTimeGetSeconds(self.timeRange.duration);
animation.removedOnCompletion = NO;
return animation;
}
- (CAAnimation *)make3DSpinAnimation {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
animation.toValue = @((4*M_PI) * -1);
animation.beginTime = CMTimeGetSeconds(self.startTimeInTimeline) + 0.2;
animation.duration = CMTimeGetSeconds(self.timeRange.duration) * 0.4;
animation.removedOnCompletion = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
return animation;
}
- (CAAnimation *)makePopAnimationWithTimingOffset:(NSTimeInterval)offset {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
animation.toValue = @1.3f;
animation.beginTime = offset;
animation.duration = 0.35f;
animation.autoreverses = YES;
animation.removedOnCompletion = NO;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
return animation;
}
@end
4.3.3 準(zhǔn)備視頻組合負(fù)責(zé)生產(chǎn)可導(dǎo)出Session和可播放PlayerItem
@interface THOverlayComposition : NSObject <THComposition>
@property (strong, nonatomic, readonly) AVComposition *composition;
@property (strong, nonatomic, readonly) AVVideoComposition *videoComposition;
@property (strong, nonatomic, readonly) AVAudioMix *audioMix;
@property (strong, nonatomic, readonly) CALayer *titleLayer;
- (id)initWithComposition:(AVComposition *)composition
videoComposition:(AVVideoComposition *)videoComposition
audioMix:(AVAudioMix *)audioMix
titleLayer:(CALayer *)titleLayer;
@end
- (AVPlayerItem *)makePlayable {
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:[self.composition copy]];
playerItem.videoComposition = self.videoComposition;
playerItem.audioMix = self.audioMix;
if (self.titleLayer) {
AVSynchronizedLayer *syncLayer = [AVSynchronizedLayer synchronizedLayerWithPlayerItem:playerItem];
[syncLayer addSublayer:self.titleLayer];
playerItem.syncLayer = syncLayer;
}
return playerItem;
}
- (AVAssetExportSession *)makeExportable {
if (self.titleLayer) {
CALayer *animationLayer = [CALayer layer];
animationLayer.frame = TH720pVideoRect;
CALayer *videoLayer = [CALayer layer];
videoLayer.frame = TH720pVideoRect;
[animationLayer addSublayer:videoLayer];
[animationLayer addSublayer:self.titleLayer];
// 設(shè)置幾何翻轉(zhuǎn)為YES,否則圖片文字會(huì)顛倒
animationLayer.geometryFlipped = YES;
AVVideoCompositionCoreAnimationTool *animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:animationLayer];
AVMutableVideoComposition *mvc = (AVMutableVideoComposition *)self.videoComposition;
mvc.animationTool = animationTool;
}
NSString *presetName = AVAssetExportPresetHighestQuality;
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:[self.composition copy] presetName:presetName];
session.audioMix = session.audioMix;
session.videoComposition = self.videoComposition;
return session;
}
4.3.4 準(zhǔn)備視頻組合Builder負(fù)責(zé)生成組合對(duì)象
此處大部分邏輯實(shí)現(xiàn)和前一章一樣,直接參照。
@interface THOverlayCompositionBuilder : NSObject <THCompositionBuilder>
- (id)initWithTimeline:(THTimeline *)timeline;
@end
- (id <THComposition>)buildComposition {
self.composition = [AVMutableComposition composition];
[self buildCompositionTracks];
AVVideoComposition *videoComposition = [self buildVideoComposition];
return [[THOverlayComposition alloc] initWithComposition:self.composition.copy videoComposition:videoComposition audioMix:[self buildAudioMix] titleLayer:[self buildTitleLayer]];
}
- (CALayer *)buildTitleLayer {
if (!THIsEmpty(self.timeline.titles)) {
CALayer *titleLayer = [CALayer layer];
titleLayer.bounds = TH720pVideoRect;
titleLayer.position = CGPointMake(CGRectGetMidX(TH720pVideoRect), CGRectGetMidY(TH720pVideoRect));
for (THTitleItem *titleItem in self.timeline.titles) {
[titleLayer addSublayer:[titleItem buildLayer]];
}
return titleLayer;
}
return nil;
}